Why strangler applications are not completely mad
I want to talk to you about a journey we are currently on to migrate our main platform from it’s initial architecture to a more flexible front end architecture that will scale with us.
Rather than telling you about our long term architecture, I want to tell you about where we are right now, what lead us there and what that experience has been like for us.
HMH is a learning company. Our aim is to provide integrated digital education solutions to improve the learning outcomes of all our students. As part of that goal, we provide a learning platform called ‘Ed: your friend in learning’.
We initially released Ed for the start of the school year in 2017 and it has been well received.
So what’s the problem?
We were asked to spin up a new platform in summer 2016, we had the usual — a very hard deadline to get something out for a sales pilot, not enough people and way too much to do.
We started this project by re-using a code base from a similar project that gave us a good set of utils, this sped us up quite nicely and we were able to hit the ground running. We managed to get that first pass out the door for the sales pilot in November 2016.
One slight hiccup — we had written this in Angular 1 and Google had announced Angular 2 in September 2016.
So, our brand new shiny platform instantly needed a refactor. 😬
The pilots went well but Ed was nowhere close to being an MVP and it had to go live for back to school 2017. We needed to scale up from being able to find our content to be able to use it meaningfully in a classroom setting complete with rostering, assignment and reporting capabilities.
This meant we didn’t have a whole lot of time on our hands for a refactor — we had to bite the bullet and take on tech debt for a little while.
But we had a decision to make — what would be our migration path?
Plan A
Take one team of engineers and have them rebuild the whole thing from scratch, in React this time. The big bang approach if you will — rebuild the whole thing and then cut over to the new version all in one go.
Not going to lie — that didn’t go as well as we had hoped.
While it was a little quicker to rebuild as we had the services in place and some people in the team had domain knowledge from building it the first time — we found there was not much code re-usability.
Trying to translate from Angular directives, controllers and services to React and Redux was just not something that we found worked easily. We did find that we could re-use CSS quite well but most of the JS had to simply be re-written.
We also had multiple teams who were still rapidly building new features in Angular 1. So for those of us on the team, we would be playing a catch-up game to a moving target until we could cut over.
We also made life a little hard for ourselves by building in React native at the same time¹.
So what did we do?
Thankfully we figured out that Plan A was going to go the way of all plans pretty quickly. We hadn’t invested too much time yet.
So, we sat down with some fairly senior leadership and had a chat about our options.
We knew a couple of things:
- We liked React and we wanted to stick with it
- We were thinking that monolithic applications — like Ed was at this stage in either format — probably wouldn’t work for us long term given our business needs. We had an idea of what we wanted to do here but we were not sure how we could do it. We needed a transitionary architecture to get us there.
- Deadlines had reared their heads — we knew we needed all our people focused on one code base and delivering immediate value to our customers.
We knocked out a couple of POCs and super quick spikes to see if we could keep two separate apps. We looked at a couple of different ways of routing between apps but were running into technical obstacles or bad user experience with what we tried.
Strangler Pattern
That’s where the strangler pattern came in. I had come across an article from Martin Fowler which described an application architecture based on the strangle vine.
The idea is rather than re-writing an app from scratch you try and eat away at the edges writing new features and refactoring small chunks into your new architecture/framework/language until you have replaced all of the old application.
It’s a pattern that has been successfully used to convert monolithic service applications to microservice APIs one endpoint or service at a time.
Here we wanted to replicate that pattern in a UI application. To write all our new code in React and to find a way to seamlessly use that React code in our existing Angular application.
How?
Practically it turned out not to be that hard.
We re-purposed the React components we had already written by packaging them as a library using Webpack and including it as a dependency in the Angular application.
We then used the ngReact directive to include our React components in Angular code.
We wrote any new feature we needed as its own app in the library and any small re-usable component went into a shared component space.
The tricky part was figuring out some patterns to connect the two.
The most basic react components just needed some inputs from the Angular application, such as environment or authentication information, this is easily done by passing in an object from the parent app to the React component through ngReact.
We also had some trickier cases.
Take the case in the gif above as a for instance — we have a page which shows search results with a number of resources from one of our Biology programs any of which can be assigned to a class by the teacher.
The page itself is Angular (highlighted in green), it displays resource cards and a filter panel both of which are built in React(highlighted in blue) and the cards open an Angular assignment modal!
All done with callbacks to an Angular service that opens a sibling Angular component on the main page! Simples.
The people element
As with many things, it’s not just the technical solution you need to figure out you also need your team on board and aligned.
Buy-in is key. You are asking your developers to willingly take on a complex architecture with slightly annoying dev experience. They need to understand why and they need to understand fully what you are trying to do so they can code in a future-facing way.
I think that was one of the key things that made this work for us. Our product team were great and had conveyed an excellent understanding to everyone what exactly we needed to be done for the back to school deadline — so we knew the scope. We sat down our whole engineering group went through all of the spikes we had done, explained the pros and cons of all of them and got an agreement that this made sense for now.
We were able to get engineers to understand there would be an end to this, that this was a transitionary architecture. Whether you transition from one monolith to another or a different architecture — it can work either way — I would suggest you have a think about where you might be going so you can structure your code as best you can.
You need onboarding documentation more than ever — we have recorded onboarding sessions, a developers guide and some follow along with tutorials. Be honest when you are hiring! You want to know what kind of architecture you are going to be working on.
The more complex your architecture the more you have to work on your communication. We have 6 UI teams working on this — and we are expanding — so we have pretty well laid out and hard-won ways of working that we revisit very regularly.
You can’t neglect your old codebase — you need to keep an eye on tech debt and dependencies. This has been tricky for us and has bit us. We had an issue where our build and deploy times ballooned from 5 minutes to 50!
So how has it been going?
Not too bad.
We have delivered quite a number of features over the last year all in React and we are slowly replacing the Angular code.
We have narrowed down our future architecture to micro-frontends, using Single SPA pattern, and we are in a good position to switch to that with the newer code. The older code will work too but with just a bit more work that we are already undertaking.
Our build frequency has increased from every few weeks to multiple times a week.
Some of the interactions between the two code bases were tricky at first, but we established some patterns pretty quickly and now it just means we can follow what others have done.
On the other hand, you do have two code bases in different frameworks/libraries to contend with. It requires a bit of a mind shift working on one and then the other — you have two different ways of coding and two different ways of testing.
The dev experience is also a little slower — we have scripts that mean everything is automatically updated for you and refreshed but that does mean there are two watches and two babel runs every time you make a change.
[1] This was really interesting — I promise to write a blog article about that someday!