Transcript
Sophie Koonin: I’m sure we’ve all seen our fair share of migrations in our careers. It’s an inevitability of being a developer. Some, no doubt, are going to have been more successful than others. If you’ve ever struggled with a technical migration, I’m here to tell you that you’re not alone, because migrations are really hard. It’s very easy to start out thinking that it’s going to be just a straight road to the end, where everything’s going to be better right away. In reality, of course, it’s often a lot more like this, an uphill path full of twists and turns and surprises, unforeseen hiccups, bends in the road, and interruptions that take away your focus. What seems like a really short distance, that actually, deceptively, takes a lot longer than you might think. I’m going to talk to you about the bends in the road that we faced at Monzo embarking on a big technical migration, how we navigated them, and the lessons that we learned along the way. Then I’m going to talk about whether you should be going down this road at all.
My name is Sophie. I’m a staff engineer and a web discipline lead at Monzo. We’re based in London. I have a website where I write about tech things, gardening, baking, also.
Monzo – Overview
Monzo is a bank that lives on your phone and makes managing your money super easy, we like to think. You’ve probably seen some of our brightly colored Hot Coral cards around. Some of you might also have them. We’re going in the U.S. and the EU as soon as well. As an app-based bank, our main product is just that, is a mobile app. We do have web things as well. Monzo.com is a website, obviously, where we have all our product information and help articles and things like that. We’ve also got a web banking app for our business customers where they can do things like manage their transactions, manage their pots, and get paid, make payments, all of those things. By far the biggest web app that we have is actually internal tooling, this is BizOps, and it’s our customer service system. It’s entirely bespoke. Things like chat, email, all go through the system, anything that our customer service agents need to do to help our customers. All the back office stuff also goes through here. As you can imagine, it’s very complex indeed.
Migration: Flow to Typescript
We recently migrated our web properties to TypeScript. It took a really long time. This is actually the sticker that we got made after we finished. It actually took us just over two years. It did come out pretty well in the end. Needless to say, migrations are not without challenge, especially in an organization of our size. In the grand scheme of things, especially considering some of the folks I’ve met here, Monzo isn’t even that big an organization. It can be really difficult.
If you’re not familiar with TypeScript, I’m not going to go into the depths of TypeScript in this talk, because this is not a talk about TypeScript. This is a talk about organizations and people and stakeholders and all of the fun stuff. Just as a bit of background, TypeScript is JavaScript with added static typing. Regular JavaScript is dynamically typed, which means that we only know what the types of variables are at runtime, when you actually run the code. That’s in contrast to static typing, where the types are actually at build time, and so, generally, in the code itself. With regular JavaScript, you could write a function that took two arguments and performed arbitrary mathematical operations on them.
Then you could call that function at runtime with variables of any type, like an object or a string. You only know that something had gone wrong when you actually run the code. With static typing, you add these type annotations to variables, arguments and functions to specify what type they should be. If you try and pass in the wrong kind of argument when you’re writing the code, the compiler will give you an error. Static type checking does require compilation, unlike plain old JavaScript, which just runs inside the browser. You do get that additional build step.
In my experience, it can help to have some kind of static type checking in a large codebase, so that you can be certain that you’re passing in the correct arguments everywhere and be less likely to get unexpected results, because passing in the wrong type to a function can give you something completely different to what you were expecting. In our case, we have a lot of code in a monorepo that’s worked on by a lot of different people across the organization. We need to make sure that people know exactly what kind of parameters and variables all of our functions and components are expecting.
These days, if you want to write statically typed JavaScript, you’re going to choose TypeScript. It’s basically the de facto option. It’s the most well-known way of adding static types to JavaScript, developed by Microsoft. A huge community as well. When we started our web codebase back in 2017, not even that long ago, it wasn’t quite as ubiquitous as it is now. There was a viable alternative, and that was Flow. Flow is Facebook’s type checker for JavaScript. For people like us who were building in React in 2017, it seemed like the most sensible option, because Facebook built React, they built Flow. It seems like the two of those will go together really well. It seemed like a safe bet, and it worked well for a time.
Flash forward to 2021, and we realized that we’d chosen the wrong option, because Flow, the public fork of Flow was becoming more and more poorly maintained. It wasn’t updated very much. TypeScript, because it had become so much more popular, we found that there weren’t any Flow types for the third-party libraries that we were using. It became really hard to keep dependencies up to date. On top of that, the Flow server crashed all the time when we were working, which slowed us down massively. We used Next.js internally as well. At the time, they just released a new version with a new compiler, so a new Rust-based compiler, which was a lot faster than Babel, which they’d been using before. Because we relied on Babel to transpile our Flow code, we were stuck with it and we couldn’t upgrade.
Experiment and Document Everything
The code itself wasn’t that different. It’s just a handful of syntactic differences. Flow files are JavaScript files with a Flow annotation right at the top. Whereas TypeScript files have got their own extension, which is .ts. We knew that migrating our web properties to TypeScript wouldn’t be as simple as just running a codemod across the whole repo and just being done with it, because there was just so much of it. There’s a ton of shared libraries and a ton of dependencies as well. It can seem really intimidating when you’re right at the beginning and you feel like you’ve got this massive job ahead of you. Where do you even start? In our case, we started really small. A couple of our web engineers decided it’s time. We’re going to see if this is going to be possible for us. They spun up a little side project and had a bit of a hack. They went out to see what they could find on GitHub in the way of scripts. They decided to see if they could migrate one of our packages to TypeScript.
Then when they did that, they decided to have a look and see if they could get it building with everything else as well. Flow to TypeScript is a path that’s really well trodden at this point. In fact, a while after we started, Stripe open sourced their Flow to TypeScript migration code. With any technical migration like this, the first thing you should do is see what other people have done to see if you can save yourself a bit of time. You want to prove that this can work before you commit to doing anything else, because if you’re trying to get buy-in for a migration before you’ve even started looking into a solution, you run the risk of it being shut down before you’ve even been able to do anything.
Without a really clearly defined plan and timeline, you’re asking people with very little context to let you focus on what they might see as less important maintenance work over prioritized teamwork. It’s really important to have that space to experiment with something before you need to go and make a case for it. Time box your exploration and communicate your progress really clearly as well. Keep a log of any decisions that you made that led to you choosing a particular path or technology, because someone in the future will thank you for it, whether they’re doing something similar or whether it’s somebody trying to understand what the thought process was behind the implementation.
Document everything right from the start. For every new engineer who joins your team, you’re going to need to get them up to speed with what you’re doing and why you’re doing it. I actually left Monzo in 2021 and I came back in 2022. When I left, those engineers were just having a little hack to see if they could migrate the library. When I came back, the migration was in full swing, so I needed to get up to speed. Thankfully, the team had put together this lovely Notion space with all the information that I needed to know. It was things like, what were we doing? Why are we doing it? How are we doing it? What are the milestones that we’re aiming for? Your documentation should cover every aspect of the migration: past, present, and future. You should treat it like a project, because, of course, your projects are all meticulously documented, I assume. It should be easy for somebody with no context to come along and know, why are you doing this? How are you doing this? What approaches have you looked at? Who should they speak to if they have questions?
Centralization and Tooling
Once you’ve decided to go ahead, who should actually do the work? Should you have one team working on the migration, focusing on nothing else, or should you spread the work across a load of engineers and teams? In an ideal world, you’d have one team doing this migration, laser-focused on it and do nothing else. For one thing, this vastly reduces the risk of things not getting done. In our case, we didn’t have that option. We shared the load across a lot of different teams. With sharing the load, you don’t need to ring fence one team for a long period of time to get the migration done.
On the flip side, it does take a lot longer. Ultimately, you’re at the mercy of these teams’ planning cycles. They all have their own squad goals, their own aims, and you’re effectively coming along and saying, can you do this as well? The bigger your organization gets, the more complicated this becomes. If you’re driving that migration centrally, you can just do it for them. We went for something in the middle. We chose to centralize it amongst teams with web engineers, but we spread it across the teams. We started with just a small group of volunteers who wanted to have a go, basically. We acknowledged that it would probably take a while.
How should you tackle the work itself? Should you do it all at once or should you go bit by bit? When you’re doing the work shared across multiple teams like we were, you don’t really have a choice. You have to go incrementally. If you do have the luxury of a dedicated team doing the migration, you have a choice here. For things like major version upgrades of React, for example, you can’t have multiple versions running in the same app. You’ll want to do that all in one go. If you’re replatforming your site on some new static site generator or something, then you’d probably want to do it all at once to avoid multiple build steps. If your codebase is small enough, then there’s probably very little risk. Ultimately, a big bang migration may be quicker in some cases, but it is a lot riskier for larger codebases.
If there’s an incident, it makes it a lot easier to identify what’s gone wrong and roll back if you’re just going a little bit at a time. It’s a much safer approach. In our monorepo, we have a load of apps that all use React, they all use Flow. Changes to shared libraries in the repository can affect multiple apps as well and even multiple different parts of the same app. We knew we couldn’t afford to just migrate the entire customer service app and just chuck it into production when our ability to service our customers depended on it. We knew we’d have to be very diligent, go slowly, and test everything very carefully. The flip side of this is you have to be really disciplined and make sure that you actually keep going, because it can be really challenging to maintain enthusiasm and pace, as we discovered. There’s a real risk here that you go bit by bit and then something else comes up and then you put it on hold, only to never come back to it.
Of course, like with any migration, we hit a bend in the road pretty soon after we’d started. People across the organization would contribute to the repository. The way that they would spin up new libraries is by copying and pasting old ones. Unfortunately, those old ones were in Flow. They were actually adding more Flow code into the repository and we were trying to get rid of it. The solution to that was to make it so easy to build new things in TypeScript that it was the most obvious way. We did that with tooling.
We built a package generator that would spin up a new library for you in TypeScript with all the dependencies and things that you need. This removed a lot of the manual steps that engineers have been struggling with. It meant that all the new things coming into the library were TypeScript. We could have also added some static checks here. Use something like Semgrep to fail CI if new Flow code was added. If you’re trying to deprecate a particular library or something, if you’re using JavaScript, you could use ESLint or something like that to say, no, you’re not allowed to import that library anymore.
I cannot say this enough, tooling is your friend here. You need tooling. If we’d done this all manually, it would have taken twice, three times as long. Lean on tooling as much as you humanly can. The original script that those two engineers had hacked away at became a migration script. You just run it from the command line with a particular library or package or app that you wanted to migrate. After running that, we’d give the output a manual lookover afterwards to fix any type errors that came out. They documented it because they’re good people. We had a space there also to document any issues that people came across and any workarounds for those as well.
Those scripts also generated Flow types for the libraries that we just migrated. Because there were other apps and libraries that hadn’t yet been migrated that would still have these imports, so they needed types as well, because the new libraries were now in TypeScript. We’ve got these shared libraries and we have this dependency tree of libraries depending on other libraries. This is obviously a massive simplification. The transitive dependencies like these caused a problem for migrating things, because obviously we didn’t want to be in a position where we migrated a library to TypeScript and then we found that it still depended on libraries that were in Flow. We wrote another script, this is definitely a theme, to find packages that were ready to migrate, because all of their dependencies had already been migrated to TypeScript. As we did more and more, we’d unlock packages further down the tree.
Make it Measurable and Set Milestones
Especially with an incremental migration like this, it became really important to measure our progress, not only to keep morale up, but so that we could see how close we were or not to actually finishing. I put together a dashboard that showed the number of Flow packages in the repository versus the number of TypeScript packages. It was a very exciting day when those lines finally crossed over. I wrote another script that piggybacked off that, which packages are ready to migrate script. That gave me a percentage of the files that had been migrated. I’d post this in Slack every month.
Towards the end, I posted it every time the percentage changed as well. Along with that measurement, you need milestones. Checkpoints along the way to indicate significant achievements in your migration. Setting milestones not only made us feel like we were getting somewhere, but also helped us communicate our progress to stakeholders in terms that they could easily understand. The first app we migrated was a milestone. Migrating the business banking web app was another one. Migrating BizOps, our customer service app, by far the biggest app in our repository, was the last one. If you’re not setting milestones like these for projects, you should definitely try it, because it really helps to break down a project into much more easily digestible chunks.
Another bend in the road showed up around mid-2023. By this point, we had the whole web discipline working on the migration across the company, but we were still quite a long way from the end. We’d said that we’d get it done by the end of 2023, but it really wasn’t looking likely. At Monzo, we try and foster an environment where the web engineers can do what we call discipline work, which is improvements and maintenance to our web stack at Monzo. They do it on the side of their squad work. You should be able to go to your planning and say, I’m also going to pick up this web ticket this week. We found that that was happening less and less. Some teams had external deadlines that they had to focus on. There was a lot of people who felt like they couldn’t necessarily step away from their squad goals to pick this thing up.
Either way, things were moving very slowly, and there was a risk that we just wouldn’t finish it at all. We’d been roughly aiming for the end of 2023, but with the support of my engineering director, I turned this into a hard deadline. We had to get it done by the end of the year so we could finish it and just move on with our lives. We ticketed up the remaining work. We broke down the remaining apps into tickets which were then given to teams based on their team size and who owns what app in their repo. Then we got buy-in from the engineering managers and the tech leads of the teams that had web engineers to make sure that they bought those TypeScript tickets into planning.
Every month, I counted up the remaining tickets for each team. I posted that update with that progress and also made sure to celebrate the teams who’d migrated the most that month. We also reduced the scope of the migration so that we wouldn’t have to migrate every single app to consider it done. This was a big compromise. It was one that we weren’t super happy about. It was the only way that we were going to get this done. Yes, this did mean that there would be a few apps left in Flow at the end, but they rarely got touched. They were in maintenance mode. We figured that if anyone did need to work on them, they could migrate them as and when they needed to. We also wondered whether we could just strip out Flow completely and leave a load of stuff in plain JavaScript, because, ultimately, that removes the need for a compilation step. We realized that in order to strip out Flow, we’d have to run a codemod. Then we’d have to look through the code to make sure that there weren’t any errors. We figured we might as well just migrate it to TypeScript, take just as long.
Get Buy-In
We didn’t have to get particular buy-in for quite a long time, because as I said, at Monzo, there is this certain expectation that there is this maintenance work that happens along the side. For a long while, it was just a small group of web engineers doing this as an extracurricular project. Getting buy-in became really important later in the project once we’d proven the potential value, and things were obviously progressing slowly, and we knew we had to get things done. Without the backing of senior stakeholders, there was a real chance that the work wouldn’t be deemed important enough to actually finish.
My engineering director was a great support here. With his backing, other less technical leaders in the organization were much more likely to let us get on with it. I brought managers and leadership on board by clearly articulating the problems we were having now and the benefits that we were expecting at the end. I had to make it really clear that we wouldn’t see the vast majority of these benefits, like compile time improvements, until we were actually finished. It really helps if you know what your stakeholders care the most about.
Some of them are going to be really focused on risk. What is the risk if you don’t do this migration now? Others might be more focused on cost or developer velocity. Is being on an old technology making you less able to ship things? Is this holding you back from scaling your organization? Know who you’re pitching to and make sure you tailor your argument appropriately. Stakeholders will also want to know when this migration is going to deliver value. As I said, some migrations really are zero to one, and you don’t see the value until it’s completely done. For a lot of the TypeScript stuff, that was the case. We could migrate 99% of packages, but as long as there was still some Flow code left in the app, we’d still have to have Babel on the Babel compiler to be able to transpile it. There was a small amount of incremental value in being able to work in TypeScript. Since all the new packages we were building were TypeScript, that means that we could effectively work entirely in TypeScript for some new features. Documentation was better, our code editors worked a lot faster, and we were able to upgrade some of our libraries a lot sooner than we’d anticipated.
Don’t Leave Things Unfinished
In my opinion, the greatest threat to any migration is not complexity, it’s actually changing organizational priorities that leave you unable to finish. Make sure the powers that be understand the risks of not completing the migration once you’ve started. It’s very easy to think that you can just pause it and move on with your lives, but in some cases, you’ll end up in a worse position than you were before you started. I am contractually obliged to include this xkcd comic about standards here, because you might introduce one new technology to replace everything else, but if you don’t finish this migration, it will be one more technology on the pile. Maybe you’ll do a little bit, and you’ll need to focus on something else, and then someone else will come along and add a new technology to the pile. Or maybe you’ll have a lot of different teams working on different parts of the website, and you’re all using slightly different approaches and different libraries.
This is very common after mergers and takeovers, when the other companies’ systems and codebases get subsumed into the parent companies, and you end up with a lot of different tech stacks. When there are five different ways of doing the same thing in your codebase, how does an engineer know which one is the right one? What is their motivation to update the old and replace it with the new, if there’s still loads of the old stuff in your codebase? If someone is paged for an incident, and this particular part of your website uses a really obscure data fetching library, when the rest of your website uses something else, what are they going to do? Think also about the end user, for those of you who work on client-facing apps, especially in the web world. By adding a new library, are you increasing the bundle size that the user then has to download?
Know the Risks and Plan for Them
You’re also much more likely to get buy-in for your migration if you’ve proven that you’ve given a lot of thought to the safety of the migration, so minimizing disruption to operations and not inconveniencing people too much, and also having an escape plan if things go wrong. Because any migration comes with risk, the important thing is how you deal with that risk, and prepare yourself for what can happen. Type system migrations, fairly low risk as they go, especially as we were moving incrementally. It’s not to say it went perfectly smoothly, but for the next section, I’ve also drawn some inspiration from some other migrations that we’ve done at Monzo that have carried with them a bit more of a higher risk. At Monzo, we have a value that says, be hard on problems, not people. This is my favorite value. Ultimately, we acknowledge that incidents are an inevitable consequence of doing things and moving forward. Except that you probably will have some incidents when you’re migrating things. That’s fine.
The important thing is that you can recover from them quickly and safely, and learn from them and make sure they don’t happen again. We almost certainly had some bugs that arose from migrating bits of our apps to TypeScript, but we were able to quickly roll them back, fix them, and move forward again. It might be appropriate to establish a set of guardrails, which are points at which you will no longer be happy and make the decision to roll your migration back.
For example, if you’re migrating your website to a new platform, and you might want to monitor your performance and load times and say, if load times consistently exceed X, we’ll roll back. You should make sure you’ve got the same metrics across the old and the new systems so that you can compare them. Throw together some Grafana dashboards or something, and make sure that you can get a really good idea of how things are looking across both systems. We recently migrated our customer support call tooling to a new platform. This is where customers call us if they have a problem. It’s a really important system. We had side-by-side dashboards of the old and the new system with things like answer rate, queue length, and the percentage of agents who are on a call. We knew that if the results were vastly different across those dashboards, then something had gone wrong. We also set alerts on those metrics so that we didn’t have to stare at dashboards all day. We worked with the folks in our operations team to figure out what are the numbers that you would expect to see here? What is an unacceptable threshold? That’s the point at which we should roll back.
If you do hit one of those guardrails and you make the decision to roll back, you need to make sure that you’ve got a really well-documented plan to do that. Anyone should be able to follow this plan, not just you, especially if they’re paged in the middle of the night. With the TypeScript migration, we didn’t have a rollback plan because we figured there was no going back. We didn’t foresee a situation where things would be so bad that we had to roll it back. We’d validated pretty early on that there was negligible impact on build times if you had both TypeScript and Flow in the mix. It all just compiles down to JavaScript in the end. Other migrations have a lot more at stake.
My colleague Suhail Patel spoke at StaffPlus conference a few years ago about the various migrations that our platform teams have done. These are migrations touching the thousands of microservices that literally power a bank. Really important. When critical functionality is in the hot path for your migration, you need to make sure you’ve got a very clearly defined escape plan when things go wrong. This might be a full rollback of the whole system or a partial rollback to a state that you know is safe. There’s also going to come a point where it’s not safe anymore to roll back. This hopefully will be closer to the end of the migration. Generally, the big risks in a migration tend to show themselves earlier on in the process anyway. Make sure these points of no return are also really well documented, because it can be really destructive if somebody tries to roll something back after you’ve already gone past that point.
Safe Rollouts, and Celebration of Successes
When you do roll things out, make sure you’re doing that as safely as possible. If you can, test your migration procedures on much lower risk, lower traffic things. The first package that we migrated was actually just like a little handlebars parser that most people don’t even know exists. You can run things in shadow mode, where you’ve got the old and the new system running alongside each other in production. The old system is still handling your production traffic, and your new system is running in a dry run mode, where it’s reporting on the output of each transaction or whatever. Once you’ve verified that the output looks like you want it to, you can then properly switch over to the new system. When we were rolling out our calls migration, we ran a pilot, so we had a small group of agents, and we routed a proportional amount of traffic to the new call system, and so we were able to work really closely with them, literally sitting next to them at times to figure out where the bugs were and get feedback from them as well, and we could iterate really quickly.
Just like growing out a fringe, there’s going to be a point at which you’ve got the old and the new technologies really alongside each other, and it’s this really awkward in-between phase, and you’re going to have to maintain and make changes to both systems. You might have to accept some temporary slowdowns in processes while you’re doing it, such as additional build steps. We had both the Flow and TypeScript compilers running in the build process throughout the migration, as I said. Yes, it made a very little impact, but it did add a few seconds onto every build. Keep on going, and eventually you will get to the point where you can turn off the new system entirely. We did it.
In December 2023, I was running the migration stats script more and more frequently to see that number ticking up, so like 97, 98, 99. It never did hit 100, because I realized my script was also counting the JavaScript config files in the root of the repo. I PR’d a change towards Christmas. There was just the remaining Flow files left in the app. When I came back from Christmas break in January, I decided to remove Babel from the customer service app to see what would happen. I’d expected to find that we’d forgotten to migrate something, or something was misconfigured, but no, it weirdly just worked. That was a very nice way to start the year. The results really were what we’d hoped for. The new compiler was faster in both dev and CI. We were upgrading packages a lot sooner than we’d hoped to. We found, concerningly, quite a lot of type errors that Flow had blissfully ignored, so lots of actual missing parameters and props that we had fixed. As I said before, our code editors were a lot faster, so a lot of us use VS Code, which is built in TypeScript, so it worked really nicely.
When you do finish, make sure you celebrate it as well, because migrations are not an easy task, and it can take a really long time, as it did in our case. Make sure you recognize everybody who saw it through with you. Post messages in public forums. I love to publicly embarrass people who have done great things. This one was in our company-wide engineering announcements channel. Make sure engineering leadership know what you’ve all achieved as well. We got stickers made, because everyone loves a sticker to celebrate and show they took part in something. We also had a little party, and I got some cupcakes made with little TypeScript cake toppers as well.
Is a Migration Right for You?
That is the story of how we actually managed to migrate to TypeScript. How about your story? Next time a migration opportunity presents itself, here are some of the things that you should be thinking about before you take the leap. The first question you should always ask yourself is, do you need to do this migration at all? Think about this. As time goes on, if you don’t do the migration, will you be in the same place or a worse place? If you’re going to be in a worse place, then, yes, you should probably do something about that. If you’re going to be in the same place, you need to think really carefully. The decision to introduce a new technology needs to be very well considered. You need to have a really clear idea of the benefits that you’re expecting to get from it. Because, ultimately, the benefits are all relative to the effort that’s required to actually do the migration. How much do you value that benefit in relation to the number of developer hours it’s actually going to take to get there? Is there a point at which the effort outweighs the value? This gets more of an issue, the larger your codebase is, and the more complex your organization is as well.
For TypeScript, compilation time, great bonus. Ultimately, we knew we couldn’t afford to be stuck on an obsolete type system that fewer and fewer people knew how to use. It did take us a very long time to get there, but it’s going to continue to be worth it every time we’re able to upgrade a package because there are types for it, or that we hire somebody who knows how to use TypeScript.
Consider the Maintenance Cost, and Do it For the Right Reasons
Remember that technology isn’t free once you’ve implemented it. There’s maintenance to think about. Who’s going to look after the new technology? This includes staying on top of vulnerabilities, like being on call, if that’s relevant. Major version upgrades are always a pain. Debugging when things go wrong. Make sure you don’t expect to just hand your new technology over to an infrastructure or platform team and expect them to magically know how to look after it. Does more than one person in your organization know how this technology works? If you left the company, would anybody else be able to pick up where you left off?
Other engineers in the company, are they ready to change their ways of working? Do they reasonably have time to learn how to use this new technology? I keep referring to GraphQL here. It’s a really tricky migration because GraphQL is a massive paradigm shift. Most engineers will be used to writing RESTful APIs. With GraphQL, you’re asking them to suddenly start building everything in a graph. It’s a real paradigm shift. You need to make sure that as well as implementing a new technology, you’re also implementing a new way of working and thinking. Are you even going to be able to hire people for this new technology? If it’s obscure enough, you might even have trouble with that.
If you started a new codebase today and you chose this new technology to build in, you’re going to be going down this exact same road a few years in the future, because there’s always going to be new technologies in the way that all code is legacy as soon as it’s merged into the main branch. You don’t have to do a migration every time a new technology comes along. Think really carefully about your motivations. There’s a massive difference between picking a new tool because it seems cool, and picking it because it’s genuinely going to be better than what you already have. Lots of us will have built horrible workarounds and complex functionality.
Then there’s been a new library that’s come out that’s basically done everything that we wanted to do with that, which has been, great, let’s use that instead. Perfect. Things do become end of life as well. We might put off major version upgrades because we’re intimidated by the amount of work that’s required. It does become a necessity at the point of which things aren’t being updated anymore, or at risk of massive software vulnerabilities not being patched. Things that seem like a good idea at the time might end up being a poor decision. Have you ended up with a technology equivalent of HD DVD in your codebase? HD DVD was the competitor to Blu-ray, like the Betamax of the early 21st century. Blu-ray won the war of the standards and HD DVD disappeared never to be seen again.
If like me, you had an Xbox 360, you had an HD DVD player. Many of us will have bet on the wrong horse when it comes to technology decisions. For us, Flow was our HD DVD. We made a choice in the past, seemed like a good idea at the time. It turns out not to have been the right choice. These things happen sometimes with a library or technology, something will be deprecated in favor of another, and that can leave you at a disadvantage when it comes to things like upgrading things or taking advantage of new advances in technology.
As well as a lot of good reasons, there are a lot of ill-advised reasons to do a migration as well. Sometimes companies migrate to new technology because it just seems like that’s where the industry is going, we should do that too. Don’t assume that just because a well-respected tech company is using a particular technology, that you should as well. When everyone was adopting React, I wonder how many of them actually stopped to think whether they really needed a framework that was built for a Facebook scale application. We use it internally for our customer service app, but it’s massive and complex.
I think it’s actually a pretty good use case. For a static website, like how much of it is just excess JavaScript that you’re sending down the wire? Can you achieve the same effects with a static site generator? Maybe you joined a new company or you came onto a new project where they’re using some technology or library that you’re not familiar with or you don’t like. I was given some good advice by my boss when I joined a new company once. I said I didn’t like a library that they’d chosen. He said, live with it for a few weeks, for a few months, think about it. Is it just different or is it worse? That’s really good advice. Is it just different from what you’re used to?
In my case, I do actually think it was worse and I was right. The principle is a really good one. Is it worse or is it just different from what you’re used to? By not opening your mind to new technologies that are different from the ones you’ve used in the past, are you holding yourself and your work back? Even if it is worse, does changing to this new technology make sense at the point where your company or your project is? If it’s a startup, migrating to a new technology is probably not the best thing for your startup to be doing at this point. Do you have a really good plan for how you’re going to get there? Maybe there’s a new language you’ve been playing with in your spare time and you wish that you could use it all the time. Or maybe there’s massive hype around a new technology and it seems like it would be good for some reason or another.
New Isn’t Always Better, and Boring Isn’t Always Bad
There are so many new libraries and frameworks out there and it is exhausting trying to keep up, and many of these will come and go before you’ve even had a chance to use them. It’s really important to consider whether a new technology was actually better than what you’ve got now. There’s nothing wrong with boring. Boring works. Boring is tried and tested. Boring is a lot easier to maintain. This is an excerpt from an essay by Dan McKinley called “Choose Boring Technology”, and you must read it. It’s at boringtechnology.club. He says, “Boring should not be conflated with bad. There’s technology out there that is both boring and bad, and you should not use any of that. There are many choices of technology that are boring and good or at least good enough. MySQL is boring. Postgres is boring. PHP is boring. Python is boring. Memcached is boring. Cron is boring”. Yes, boring though they are, these technologies are familiar and reliable and people know how to use them. There’s lots of documentation, there’s massive communities, and they’re well maintained. Sure, they’re not new and exciting but they will work and they will do what you need them to do. I hope that these lessons are going to give you a framework to tackle your next migration. Remember when you do do a migration, do it very wisely. Give it lots of thought. Don’t be afraid to be boring. Maybe you don’t actually need to do it at all.
Questions and Answers
Participant 1: I was curious what your opinion on stricter typing during your TypeScript migration was. Maybe you don’t do it as a goal, but for instance like TypeScript strict mode or any types, that kind of stuff, during your migration, did you allow for that or did you immediately counter that? You wanted absolute typing, very strict typing as you went? What’s your opinion on that?
Sophie Koonin: Did we allow any during our migration? Are we going to strict mode or did we just let anything go? We set out with very good intentions. We have got linting rules. We don’t allow any as a type because it’s basically like having no type, but there are also a lot of ts-ignore comments in our codebase that I encourage engineers to clean up as they go. I think we did at some point have to accept that as we migrated things, as we wanted to get things migrated, we did have to start being a bit more liberal with our ignore comments. In all new code especially, we do prefer strict typing basically.
Participant 2: When you were migrating, did you adopt a lift and shift style approach or did you use it as an opportunity to introduce new functionality or features to make the work more interesting?
Sophie Koonin: We try to follow the scout rule of leaving things better than you found it, but this was mainly a lift and shift. There were occasions where I would do a bit of cheeky refactoring because I can’t resist. For the majority of cases, it was pretty much a lift and shift from old to new.
Participant 3: Casting your mind back to the first moments in this migration, whether it was you or someone else, what were they doing when they thought we can no longer withstand Flow, and perhaps more importantly, how did they communicate that at first? Who to? What did they say?
Sophie Koonin: We have quite a strong proposal culture at Monzo where people can write a proposal of something they want to change. We used to call them RFCs, but effectively, you can propose a technical change. You don’t have to be senior to do it. We encourage all engineers to do this. One of my colleagues had written a document that I’m pretty sure was called something like, we should probably migrate to TypeScript, and so that was floating around at the time. There wasn’t some definitive moment where we said, yes, we’re going to do this. I think someone just got fed up and decided, I think I’m going to just see if we can do this. Let’s see how much of an effort this would actually be. The individual migration of one package is actually pretty low effort, and the actual challenge becomes when you need to scale up your efforts to be a whole organization-wide thing.
Participant 4: How did you balance the tasks in the team’s backlog between new features, basically the business as usual and these migration tasks that are usually purely technical?
Sophie Koonin: How do we balance it?
Participant 4: How do you decide?
Sophie Koonin: It’s hard, basically. One of the reasons it took so long is because people do struggle to balance these things. I think perhaps people are less used to having the agency to come along to their weekly planning and say, I’m going to do this this week as well. Obviously, your team’s goals come first always. Only take on as much work in that week as you can reasonably achieve as well as doing the extra maintenance or migration work, then you’re not letting anyone down if you don’t achieve what you set out to.
Participant 5: We’ve broached the idea of TypeScript in our repo a few times and one of the things that gets in the way of that is folks not feeling super familiar with it, perhaps. Just wondering, with the other teams that you were working with, did you ever have to convince them or upskill or anything like that, or was it quite familiar because of Flow being in the repo beforehand?
Sophie Koonin: I do think that not having to work in Flow anymore was a big selling point. I come from a background originally of Java, so I’m used to static typing. We use Go in the backend, which is a lot less like that. The type annotations are a bit more confusing in Flow land than they are in TypeScript. The documentation is just so good. There are so many courses and so many videos and just so much out there for people who want to learn TypeScript that I think the learning curve is a lot shallower than it was for Flow. I do think that it’s an element of like the less worse option.
Participant 6: Was it possible after making the proof of concept to estimate roughly the amount of effort you needed? Could you say to your management, dear management, it will cost so many thousands of hours, or you came to other problems and it was not possible?
Sophie Koonin: I think we should have done that earlier. The point at which I had to go to leadership and say we need help with this, that was a point at which we say, ok, we estimate there’s about this many hours, this much work left. Part of selling it is being able to articulate how much it’s going to cost. Again, there’s that conversation of like, is the effort worth the outcome as well? Breaking down your migration into these milestones, into these kinds of smaller deliverables will help you to estimate as well in a less amorphous way.
Participant 6: Sometimes it’s not possible at the beginning, I think.
Sophie Koonin: Sometimes it’s not possible. I think there is a certain amount of being able to persuade people that it’s worth the investment without having a concrete number. You can’t always apply a concrete number to everything.
Participant 7: How did you approach refactoring a part of the code that was currently being worked on? Was it like rebase hell or did you have a different approach?
Sophie Koonin: There’s a lot of code. I think some of it was less often touched. There wasn’t so much there. Yes, it did become a bit sticky when someone would make a change to something that you were also migrating to TypeScript. Sometimes you just got to get in there and copy and paste some code and just be like, “Please don’t merge your PR. I’m about to merge a TypeScript migration”. Yes, it’s just part and parcel, unfortunately, of doing a big technical migration like this.
Participant 8: In terms of orchestrating this, so some of the things that you mentioned is that you were catching up and updating people on things. Do you recommend that, having a single point of person, or would you say it’s more helpful to have a cohort or a working group where you’re all collectively orchestrating and leading on it and communicating?
Sophie Koonin: I think it depends how your organization works, really. As the web discipline lead, I was well positioned to be the mouthpiece for the discipline and the project that we were doing. I had good relationships with senior leadership. I think it made sense for me to be the go-to person. If you’re an organization that’s a bit bigger, then it probably makes sense for you to have several points of contact.
See more presentations with transcripts
