Showing posts with label architecture. Show all posts
Showing posts with label architecture. Show all posts

Transforming Primary Care: a Case Study in Evolving from Start-Up to Scale-Up

Mike's Notes

A useful shared experience by Leander Vanderbijl at an InfoQ conference. The original InfoQ video presentation includes the slides. and is well worth watching (52:26).

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > InfoQ
  • Home > Handbook > 

Last Updated

29/01/2026

Transforming Primary Care: a Case Study in Evolving from Start-Up to Scale-Up

By: Leander Vanderbijl
InfoQ: 22/10/2025

Leander Vanderbijl is an engineer and architect that has worked across the entire stack and has been working at Kry/Livi for the past number of years. He has developed large enterprise applications, migrated cloud platforms, designed data query frameworks, architected integration systems and built lots and lots of APIs. Having worked in large enterprises, small companies and, most recently, in healthcare, he has built the monolith, torn it apart and then rebuilt it again.

Transcript

Leander Vanderbijl: Have you ever built the perfect application? Beautifully crafted your models so that everything fits the real-world objects that you think you're going to have in your system? Only to wake up one morning and your PM comes to you and says, yes, that's great, but what if it just did this a little bit differently? With that one sentence, your entire premise is thrown out the window. All of your assumptions are proven false and your beautiful system is impossible to work with. What about that perfect architecture you've developed? Very clean, you've got onions and layers and all of that fun stuff, and it works perfectly. You don't know when and you don't know why particularly, but over time you suddenly come back to it and this beautiful application has become a spiderweb's nest of impossible dependencies. I've been there. We've had that problem.

In this talk, we're going to talk about how our company, Kry, has evolved through startup and scaleup phases, and particularly how our systems have evolved while we were trying to find our product market fit, and how our systems became that spaghetti spiderweb's nest of dependencies.

Particularly, we'll look at how we're now applying domain-driven principles to gain back control of our architecture, to try to make sense of the mess that we built. Again, not intentionally did we build that mess. We're also going to look at how we're changing our mindset from looking at the product and taking a feature endpoint. Our frontend needs this so I'm going to build one endpoint for that. Instead of doing that, we're starting to think about, what functionality do we need to provide for any client or for any other part of the system? We're going to look at how we're moving from an organic approach to our software evolution where people just do what they need to, when they need to, to a much more deliberate and targeted approach to our evolution where everybody knows where we're going.

Marshall Goldsmith says, what got you here, won't get you there. Which is a great quote as long as you know where there is. Throughout this talk I'd like you to remember my golden rule, my guiding principle, working software is better than the promise of working software. We're not Google or Microsoft. I can't go and tell a team to rewrite the OS every four years. We have to find some other way of making our systems better in situ.

Roadmap

For the structure of this talk I'm going to borrow heavily from Simon Brown's C4 model. We're going to look at this problem at four different levels. We're going to start at the company profile. We'll talk a little bit about what our company does and what its purpose is. Then we'll move on to go a little bit deeper to the container level and look at our product history and our requirements and how those have evolved over time. Then we'll get a bit deeper still and get to that component layer and look at how our architecture and our services have grown and adapted over time. Lastly, we'll look at domain modeling, sort of a code level. We won't look at code but that code level of the C4 model. We'll look at how our domain modeling has changed over time and how it's evolved when we started to realize that our initial attempts at it weren't fitting the bill.

Tech in Hardware

Did you know that in the UK there are 353 million GP patient appointments every year? There are 28,000 full-time GPs. Is anybody good at math? I do that for you. That average is somewhere in the neighborhood of 30 patients a day, or 3 to 4 patients an hour. At Kry, we've found that by applying technological solutions in a process that we call digital offloading that we can increase this throughput from 3 to 4 meetings an hour to somewhere in the neighborhood of 7 or 8 meetings an hour, so nearly double, and still provide the same levels of clinical excellence. We do this through a series of things.

A large part of that is moving the meetings online, and that has proven that we can improve throughput just by doing that alone. A lot of it is really around all the rest of the provision of services that go around meeting a patient. That's things like being able to get data in a timely and efficient manner to the GP before they look at a patient. That's mastering the workflows after the patient has been seen by the GP. All the data input, we can standardize that. We also can vastly improve the workflows for continuing care, things like prescriptions or providing referrals and things like that. With all of that that's how we can really drastically improve the speed and the quality in which our GPs can manage the patients.

Company Profile (Context)

We were started in 2015 in Sweden. As we grew, we started to expand into other parts of Europe. Primarily, our biggest markets are France and the UK. We've also recently gone into Norway as well. Again, our focus is the digital transformation of primary care.

Product History (Containers)

Let's go into how our products have evolved over time. When we started in 2015 it was simple. All we wanted to do was hold meetings digitally online. The requirements were again pretty straightforward. Obviously, I'm simplifying this a little bit. Really all we had to do was we had to allow clinicians to be able to meet with patients. We had to be able to get reimbursed, that's the important part, from the national healthcare system particularly in Sweden. We had to be able to share data with the national healthcare services. That's both getting data and being able to write data back to them.

As we grew, we added more requirements and more functionality. Things became naturally more complicated. We added prescriptions. When we moved into other parts of Europe we started having to deal with more complexity. We had to deal with regional differences. Things like in the UK the reimbursement model is based on a fee per action, so we get paid for every patient appointment we have. Whereas when we built the system in Sweden the Swedish model is based on capitation, which means that the patient has to be registered with your clinic in order for us to be reimbursed. They were very different models of getting reimbursed. We also had to deal with a lot of different security and regulatory differences.

Things like age of consent, dealing with children and things like that. We started adding new clinical types, so new health services. Things like mental health, nursing services. These services had very different workflows to the original GP workflows. We started opening our own physical branches and we started having to deal with different scheduling models.

Particularly, we started to have to deal with separated schedule books. Each physical practice would have their own separated schedule that had to be maintained and managed differently from all the other schedules. This was something that was a big difference because originally when we were just digitally online, we just had one big pool of doctors who would serve us a queue. Most recently, we've added insurance companies and private companies who want to provide our services to their employees as a benefit. This has added more workflows that we had to manage. With insurance workflows we have to be able to make decisions on the fly as to whether or not we can see that patient. That means we have to be talking in real time to insurance third-party APIs and be able to make complex decisions on the go. We also came across issues with data sharing.

In the UK, for example, if we see a patient through an NHS contract, we're allowed to use NHS APIs and NHS data. If we see that same patient through an insurance workflow, we're not allowed to use any of the APIs. We're not allowed to use that data either. Suddenly, we had to start understanding, who is paying for this appointment, and be able to make decisions rapidly as to what data we could use, what APIs we could use and the like. That's a plotted history of our product.

Architecture and Services (Components)

When we started, life was simple. We made this ourselves, maybe you can tell. Again, life was easy. We had clinicians and we had users, and they could have meetings. We could store their data in what we called, and I still don't understand this joke, the fax machine. It was ok. It worked. In fact, it worked really well. We could scale quite easily with this service in terms of the volume, and life was good. We started having patients and we started increasing the number of patients we were seeing. We started adding more functionality. We added the ability to manage prescriptions. As we grew, sometimes it was quite easy for us to add new services because you could fit it in or you could do things. Sometimes, for example, when we had to add new payment models when we moved into the rest of Europe, things became more complicated. We had made assumptions about our payment systems and our payment models when we started in Sweden.

Suddenly, when those models changed, we had to work out how to fit them in. Sometimes we could fit that in just beside the code we'd already written. Other times that would be too difficult and so we'd have to create a new service. The same thing happened across the board when we added mental health services, for example. Mental health services are a very different workflow. They don't necessarily revolve around one meeting with a patient like we had with a GP. You might have a dozen meetings or more meetings, all part of one service as you were. We had to build another service to manage that, and the trend continued. When we moved into physical, we started dealing with queues and scheduling. We already could do queues and scheduling in our meeting service, but we couldn't separate them nicely so we had to refactor parts of that. We had to build a service to go with it to manage some of that complexity.

It was at this time that we really started to see that maybe what we had initially thought, wasn't as perfect as we had initially thought. Things were starting to change. When we started having physical meetings we realized, yes, we've got patients as well. When we started, users were patients and patients were users, and it was simple. Who's a user in a physical sense? We also at this stage started being able to see children. Somebody worked out that children might have parents, we didn't really think about that.

Then, some really clever guy, real person, realized that actually a child might have two parents and they might have two separate accounts. How do we deal with those relationships? We made this work but it wasn't pretty. Remember, this is all through our startup phase. We didn't have the luxury of stopping, pausing everything, and refactoring a bunch of code, refactoring some of our main assumptions. Our primary goal was to get customers in the door. Our primary goal was to try to get profitable, and so we were doing everything we could to make that happen. I must stress this, that wasn't the wrong decision. We absolutely had to do that. If we hadn't have done that, I probably wouldn't be here today. These are decisions that are made because of the situation that you're in, and that's fine.

At some stage, you are going to have to deal with the technical debt you've built. Our insurance workflows were so drastically different to what we had built before that we actually built pretty much a stand-beside service to handle a good chunk of our insurance workflow. We started having duplicate code because we couldn't make it work otherwise. We started complicating the systems that we had.

This was a very organic approach. When we started, we didn't really know where we were going to go. People just did what they needed to do. At the beginning, again, remember this was easy. Our dependencies were very straightforward. You could see which service needed to talk to which service and how things should go. Over time, as we added more services and we added more complexity, those dependencies grew. We didn't have any rules about who could talk to what or when.

Basically, if you needed it, you could build it. We started to realize that our services were no longer really fit for purpose. Many of them grew so bloated through just constant addition of different functionality that they hardly resembled the purpose for which they'd been started in the first place. We had also the opposite problem where people couldn't be bothered or didn't want to wreck what was already there, so they'd create a standalone service often for a very tiny bit of functionality. What was even better was that people didn't necessarily talk as well as perhaps we should have, and so we started to duplicate. Sometimes we had multiple services doing very similar things, but one very similar thing. Again, this is natural. You've got a company that's growing very quickly and we're trying to get as many customers through the door. We lived with it and it worked. We did get customers through the door. Our architecture grew very flat.

Everything could talk to everything else and there were no rules around it. We knew that our services were no longer becoming a cohesive API that anybody could use. Instead, they just became really a big collection of endpoints. Things were starting to become problematic. It was difficult for us to extend. It was difficult for us to change anything. We spent probably two years knowing that this was a problem. It's very easy to identify that you've got a problem. It's a lot harder to actually work out how to fix it. We tried all sorts of things. We went to the bosses and said, can we just stop doing everything for a couple of years and rewrite this? You can guess what their response was. We thought, what if we just decided to do a version 2 of our APIs? Maybe we can just fix the problems with that API.

That failed too, not because they didn't do good work but because we didn't really know what we were building. We were just trying to fix our perceived problems with a very small part of the API without taking the entire picture into view. Remember my golden rule, working systems are better than the promise of working systems. We knew we weren't going to start from scratch, and we knew we had to do it in situ. We realized, the most important thing we had to do was we had to agree on where we were going. We had to stop all pulling in different directions and work out what it was that we wanted to do, and then move in that direction all together. It didn't matter if people took little deviations here and there, as long as they weren't going backwards or southwest.

This is where domain-driven design came to the rescue. We had all of these services and all of these systems that we knew about and we listed them. There's some of them here. This is where we took the decision to flip what we were doing from looking at the product down and look from the functionality up instead. We'd gotten to a stage now that we were mature enough that we did start to understand our systems reasonably well or at least what the functionality needed to be. We didn't have to kowtow to the product team and build a brand-new thing every time because by this stage we'd built most of the functionality. We could start thinking about it in the opposite way. We sat down and we started to think about, what is it that we actually do? What's our core business? We didn't do this in a big team. We tried to do it in big teams. We tried to crowdsource this, and we just had too much whataboutery. There was just too much, what about this edge case? That's not going to fit that. It became counterproductive.

In the end, we had to make a decision. We had to try to decide where to go. We just made a small team and said, we're going to make this broad and we're going to start somewhere so that we can get going in one direction. We sat together and we said, what is it that we do? We do healthcare. That hasn't really gotten us that far, has it? At least we know we don't do bananas. This is what we have now, one big domain of healthcare. We knew we had to dig deeper, and this is where we actually got really pleasantly surprised because actually when we looked at our services it wasn't that hard to start grouping things in big buckets.

In fact, we went right back to the beginning and said, what were we trying to do? We were trying to have meetings online. We looked and we saw we had meetings, we had users, we had a queue service, schedule. That's a bucket. That's really easy. I don't need a PhD in rocket science to work that one out. We put that into a bucket and that became our first domain. Again, things seemed very simple. We looked at what was left, and again we started to realize that actually some of this was quite straightforward. We had patients. We had prescriptions. We had different service types. We were storing their data. We had ways to do referrals. We had all of these services that were really around providing that medical service to a patient. That became a domain called medical. We had two great pillars there ready to go, and that felt really good.

Then we went, we're forgetting probably the most important part of this, how do we get paid? We realized that the payment systems and our reimbursement models and all of that stuff, our contracts, that those things were definitely a domain on their own because they changed for all sorts of reasons, a new government, a new payment model. That became a third domain where we could isolate all of that, let's call it crazy, on its own.

We had three brilliant pillars and that felt right, but we still had a bunch of services left over. We had no idea where these would fit. We had a bunch of services around messaging. Messaging was used by parts of systems in all of those domains. It was used to communicate with patients. It was used to communicate with our contract with the NHS or with GP surgeries. It was used between clinicians to communicate. Where does that go? We also had a bunch of services around EMRs, medical record systems. That's where your patient data is stored. We had a bunch of services that were connecting to different medical record systems.

Again, these were used by all of the domains. What were we going to do with those? We didn't really know. We didn't want to have a handful of separate domains standing on their own, so we did what any sane person would do and we fudged it. We created a domain that isn't a domain, really. We created something that we just called support systems, and whatever didn't really fit or was used by multiple domains we put into that. We were left with a very simple model of what we did. This reduced our cognitive load when we had to start dealing with new functionality or new systems. It meant that our developers didn't have to worry about 50 services when they were adding functionality, but they could just deal with the one or two that were relevant to their domain.

We got here. We know what we're going to do. How do we go from there? Domain-driven design to me is a journey, it is not a destination. I never expect us to get to what that domain looked like. We might but I doubt it. As we go, as we develop, as we work towards that domain, things will inevitably change. Requirements change. Our understanding of the problem changes. We have to expect that that's going to happen. The beauty of that was that it wasn't super specific. It wasn't like anything was going to trip us up, but it all gave us a unified idea of where we wanted to go. It was something that everybody could get behind. What got you here, won't get you there. At least we now know where there is. Let's take a look at a few of the approaches that we used, not to get us to domain driven, but to move us in that direction.

The first one I call the take that approach. The take that approach is where we have a bunch of singers who are all right on their own, but we put them together and they become greater than the sum of their parts. You remember I was talking about those medical record systems. We had a good dozen or so different services that managed our connections to medical record systems. They were used in different ways by different parts of the code at different times. We started to have a couple of duplications. We'd have code in two different areas calling the same EMR in a different way because they needed a slightly different outcome or a slightly different result. All of the different connections to those EMRs were implemented totally differently. All of the code that used those systems couldn't just talk to a different EMR if we wanted to change, they had to rewrite or reorder how they were using them.

One thing about all of these EMRs is that they weren't consistent at all. The way that you had to get data from each of them was different. They all had different models for the data. They all had different ways of connecting to them. For some of the systems, or a lot of the systems even, you could connect over HTTP. There are some systems that you can only connect to locally through a TCP port. We actually had to have a VPN into some of these systems. That complexity, coupled with the fact that we were just duplicating all of this logic all over the place, seemed ripe for simplification.

Again, nothing fantastic here, but this was perfect for an adapter proxy pattern. We created a single proxy at the top where all of our clients could connect to if they wanted medical record data. They could connect to a single consistent API and get a single set of domain models back. If they wanted to connect to a different medical record system, they'd just call the same one and just say, I want it from a different system. In one swoop, it standardized all of this complexity. We hid all of that complexity in the EMR connectors. The EMR connectors would be responsible for translating from the medical record system to our domain models, and we'd have one connector for every external EMR. It became very easy for us to now just talk to a different medical record system, because all we had to do was add another connector. We didn't have to rewrite all of the code from top to bottom. That was the take that approach.

The next approach is the opposite of the take that approach, the Robbie Williams. The Robbie Williams approach is where you take the one singer in the band who's actually any good, and you get rid of the rest. I apologize to all of you take that fans. We had a REST Entity called care facility, and there were over 40 endpoints dealing with a care facility. Thirty-one, I think, when we counted last, of those endpoints, were just one variation or another of getting a care facility. You can see we got creative with the naming. We've got with a hyphen, without a hyphen. We've got a plural. We've put physical in there. We had a second version. Again, this is obvious, but we came to this and went, that's a bit mad, isn't it? Most of these endpoints, the only difference between them was they would call by a different column, they'd filter by a different column. We had all these endpoints. We had two versions of our APIs.

In behind all of that, in our code, we had 15 different service classes, all basically just querying a care facility. We had five different data access objects, all for a care facility. We realized we could just take this and make this simple. We could just have one query endpoint. The way we did this, we'd already tried this, and you can see that with the V2. We didn't want to go down that road again. What we decided to do was that we'd start off with our brand-new single query endpoint, we'd go through each of those 31 endpoints one at a time, and we'd make sure that we could match the functionality in our new query endpoint. We'd go from top to bottom on our systems and get all the calling code, point it to the new endpoint, and then we deleted the old endpoint, so that every time we went through a new endpoint, we knew we'd gotten rid of it. It was never going to stay around anymore. That allowed us to take our 40 endpoints and get one endpoint, one version, one service, and one data access object.

Domain Modelling (Code)

Let's move on to our domain modeling. This was what we started with. Again, it was simple. We designed this ourselves, and it matched what we knew about the world when we started. We had users who could have a meeting. We had clinicians who could join that meeting. We knew who was paying for it. We would store some data. The way that we were able to really dramatically speed up the way that clinicians would handle our meetings was that we standardized using forms, and that made it really quick and easy for clinicians to go through a meeting, fill out the information they had, and move on. It was also great for us to be able to review those meetings to ensure that we were still providing the same levels of clinical excellence because we were standardized. The forms were hugely important for us, but when we stored the data, we stored the data in the format of that form because that's what made sense at the time.

As we grew, that started to show its age. Forms would change over time. If we want to look at an old meeting that was written in an old form, and we've got the data, how do we render that form again? We had to be able to match those things, and that became difficult, especially with more iterations. We had to support UX that was old and things like that. Because the data wasn't stored semantically, we had no way of being able to ask the kinds of questions you would expect to be able to ask of a system like this. How many people have we seen for COVID-19? We could answer those questions, but it was really difficult.

One of the other aspects we had was that you can see that everything was revolving around the meeting object. The meeting was really our central pillar for understanding our system. As we started adding new workflows, that assumption started to become invalid. When we had mental health services, we had to be able to manage a service over multiple meetings, and so things were starting to get strained. We talked about users being patients, and patients being users, and them not being the same thing. Again, we realized that our model was starting to strain at the seams. It was starting to become difficult to use. It was starting to show some of the frailties.

We had a look, and this was different from our approach to services. We decided here, this is a healthcare model. This is a popular industry in the world, I think. Maybe somebody has done this before. Turns out they have. FHIR stands for Fast Healthcare Interoperability Resources. It is a full schema and model for healthcare objects. It's used around the globe. We were like, "I don't know why we didn't find this a couple of years ago, but excellent. We've got it now. This will solve all of our problems". No, that was bad timing. It's great. It is fantastic. It's a beautiful set of resources and models that have been really well thought out. Anything you could possibly want in the healthcare space is catered for.

The fact that they've been able to manage all of the complexity that occurs around the world with a model like this is amazing. Some of the things you might notice here is that in order to support all of that variability, they've had to make it fairly abstract. Things like the patient name isn't a name, it's a human name. Inside the human name, object, or a bunch of smaller objects. If you look in the contact relationship, you've got a codable concept. You'll see that codable concept in the communication language. You'll see it in the marital status. Codable concept is basically a namespace key-value pair. That's fantastic because it allows us to model all of these things or allows them to model all these things without being too specific. Can you imagine your frontend developer who wants to get the first name on a screen shouting at you when he's like, what do you mean I have to go through eight different objects to get the first name?

It was great for telling us how things should look, but it wasn't necessarily great for every implementation that we would need. When we took this model into our medical domain, it was like the lights came on. It made sense because everything was specific to the much more narrow domain in which we were working. We had patients. We don't have users. We had appointments instead of meetings. We had clinicians. Clinicians are clinicians. Instead of this random set of forms and data that we had, we had observations which were strongly typed, so we could now start asking questions like, how many people have we seen for COVID-19? What was fantastic about doing this exercise was that when we started looking at the other domains that we had, it started to make a lot of sense. We could see where the objects in the different domains met. We could see how they related to each other. We could also see why they weren't the same thing anymore. This was a massive step forward for us.

Let's move to my last approach that we used around how we dealt with domain models. That's the approach formerly known as Prince. This is where we take a system that works and that is useful and that we quite like, but we realized that there was actually another service, a hidden service that we hadn't identified at first that would better suit part of our requirements. That we'd had one service that was actually trying to do multiple things, and it did both of them badly. It would be better to leave the service as-is for the things that it was useful for, but to build a new service for the things that it didn't do well.

This is what we did for our medical data. We took the FHIR model and we built a new data service from it using FHIR, not directly, but as an inspiration. We did think about using it directly, but then we got to that, how are we going to tell the frontend developer that he's got to learn FHIR and learn these complex models? We didn't. We used it for what it was good for. We took the best bits, particularly the objects and the relationships between the objects, and then we modeled the properties as made sense for us and for our systems. Now we can very easily query our data. We've got a given name there, so our frontend developer is happy.

This drastically improved our understanding of the system because we no longer were tightly coupling our patient data to how it was inputted. It allowed us to be able to make changes in the input forms without having to worry about the backend. Things become simple in small doses. There are three approaches, the take that, where we can get a bunch of things that seem similar, maybe they're not rockstars in their own, but we can put them together and they'll be greater than the sum of their parts. We can have the Robbie Williams approach, where we take the one good thing that you have and then delete all the rest. We have the approach formerly known as Prince, where we rebrand or rework some of the functionality that we've discovered we needed at the time, but we still can use the old stuff because the old stuff is still good.

Summary

Remember, you are never going to be in a situation where things don't change. People are always going to be coming to you and asking you to make new things, to build new things, to change how they work. You have to lean into it and you have to accept it. We've used domain-driven design very successfully to reorganize how we think about our systems, even the ones that are already running. Lastly, I'd really recommend that you organize around the functionality that you have, not around what the product says they need, because, remember, domain-driven design is a journey, not a destination.

Questions and Answers

Dr. Kreindler: How do you think larger health systems can get to 11 out of 10 in terms of their whole architectural strategy? Because right now it feels like most of us are at 1 or 2 out of 10 in large health systems.

Leander Vanderbijl: There's a lot at play. There's a lot of perceived complexity, I think, in the healthcare industry. I think a lot of that complexity isn't necessarily complexity, but it's just different ways of working. Trying to convince people to move towards a standardized approach to things, which isn't necessarily a be-all and end-all in and of itself, but has proven very successful for us, can be difficult. We go to practices and we say, if you do things this way, we can really improve your throughput, we can really improve your services.

A lot of people go, yes, but I can't do it that way because we use a fax machine or whatever. A lot of that is changing people's minds. There's a lot of vested interests in terms of how systems work, and maybe not even necessarily vested interests, but medical data is very protected, rightly so. That makes it very difficult if you want to start doing novel things with it. It makes it very hard to get around some of those hurdles and to be able to convince large organizations like the NHS or other things to be able to allow you to get into data, to do things that maybe are slightly off the beaten track.

Dr. Kreindler: You talked about domain-driven design. By that, I'm assuming you're talking about the different components of the business, are you, or domain as in the healthcare domain?

Leander Vanderbijl: It's more in a software perspective. It's how we group our functionality together to be able to make it easier to understand and to develop.

Participant 1: With the increasing role of tech companies in the government sphere, do you think we'd ever get to a point where governments themselves are agreeing on data models or other interfaces, like the FHIR one, that could make just business on the international scale easier?

Leander Vanderbijl: Yes. In fact, the NHS has adopted this wholesale. In fact, their latest set of GP Connect APIs are all FHIR-based and they are brilliant. There's a lot that people complain about the NHS, but their GP Connect APIs are absolutely brilliant. They're very well thought out, they're very well modeled. If you can get access to them, they're amazing. It is difficult because they're under a lot of pressure. There is a lot of governance you need to go through to be able to get access to them, but they are fantastic. It would be nice if the private companies that had medical record systems would support FHIR as beautifully as the NHS does, and that's actually been almost more of a problem than the national healthcare systems.

Participant 2: Domain-driven design is obviously very good for informing your software, but to what extent, when you've drawn your models, you've mapped out domain, you understand what your software currently does. Can those models maybe inform back on the product side of things, go, actually, what you're suggesting here, we're moving into a new domain? Are we losing the purity of the focus? Are we maybe changing direction a bit? Can it shake back and affect the product as much as it affects the software that comes out of it?

Leander Vanderbijl: Absolutely, it can. The way you model things can absolutely impact how well you can do certain things. We had the instance with the way that our data was stored, we couldn't answer a lot of the questions that we wanted to ask. Once we started storing it in a model that made sense, in a domain that made sense, we suddenly could ask those questions, but we could also ask a lot more that came up, because they were like, "You can do this. Yes, I hadn't thought about that kind of question". Absolutely, it's a two-way street there. Definitely.

Participant 3: To paraphrase, "We drive down costs and increase efficiency by standardizing the business workflow of medical care, domain-driven design being a great way of standardizing IT systems". I totally agree. I'm a big fan. Have you thought about the next step, so, for example, 4C analysis in the business workflow to further drive optimization? Given the data you now have or the personnel you have, how you could use something more like 4C to drive the actual outcomes in the medical business, because it's a different approach to try to drive different medical outcomes through process change.

Leander Vanderbijl: Absolutely. The trick there is around governance. How long can you keep data like this? Are you allowed to use it for other purposes, like training data models or that sort of thing? That's a bit of a minefield at the moment. I think there are still a lot of people trying to work out the ins and outs of that. We are starting to do things like automated triage, so being able to say, given a set of complaints, do they need to be seen tomorrow, yesterday, or whatever?

Participant 3: After domain-driven design, have you thought about what would be the next thing that you would implement, like a 4C analysis or something else?

Leander Vanderbijl: No is probably the shortest answer. Again, this is a journey. What we want to do is see where it takes us, really, and try not to be too prescriptive of where we're going to go. Once we start getting the initial domains starting to make sense, then we have the time to start looking deeper and fixing some of the other problems. At the first hurdles, we're really fixing the top layer of problems. We do need to go deeper into some of the way that some of our other code is written and some of the problems that are there. That's natural.

Modular software design

Mike's Notes

A good, clear explanation of modular system design by Chris Loy.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library >
  • Home > Handbook > 

Last Updated

25/01/2026

Modular software design

By: Chris Loy
Chris Loy: 24/01/2025

Hey, I'm Chris. Nice to meet you!

I spend my professional life writing code, training machine learning models, building software products and running tech teams. I'm a startup founder with a successful exit, and in my career I've been CTO, a developer and a data scientist. I've worked at companies big and small, mentored junior engineers and data scientists from across Europe, and served as an advisor for some companies. Throughout my career I have been lucky enough to work with and hire some amazingly smart people.

This is my website, where I write about tech, data and AI, as well as some of my other interests and hobbies. Those include music and photography, and I also use this site to document the books I read..

Whenever people ask me about my philosophy for building software, top of my list is always to say that I am a proponent of modular design. To explain what I mean by that, it is first necessary to understand a few of the fundamental theories that underpin how I view software engineering as a discipline, and then we can explore how those come together in a philosophy of modularity.

Systems

Software engineering is a discipline for the design and implementation of complex systems of interacting elements. If a program is intended to serve a single purpose by completing just one task, that task can be broken down into smaller tasks, which can themselves be broken down further and further.

The result of conceptually atomising monolithic operations is a network of interdependent operations that comprise the wider system. The boundaries of this system are wide and varied - how data is written and read from hardware devices, communication of information between geographically separated networks, even the knowledge and opinions of humans interacting with websites or smartphone apps. Whatever we are building, our control extends only partway through the network.

Systems thinking is the embracing of the vastness and complexity of all the moving parts that influence the operation of software. The challenges we face therefore come from the limits of our own capacity for comprehension.

Abstraction

To address this complexity, the primary philosophical tool in the software engineer’s belt is abstraction. To abstract something is to hide its complexity by defining only the way that it interacts with the world around it, and to deliberately behave in a way that ignores the inner workings of the thing.

This is the fundamental thought process in software development, first articulated by Dijkstra in his essay “The Humble Programmer”. The central thesis of that essay, written in the 1970s before the invention of the microprocessor, was that as programmers we are developing systems too complex to hold fully within our mind all at once. At the time, this was a unique and forward-thinking insight - today it is obvious.

Abstraction teaches us that software engineering is a philosophical discipline - one in which our goal is to enhance our ability to reason about complex systems by effectively abstracting away complexities that lie outside our immediate sphere of interest. Components that are behind or within the components we directly interact with are ignored - only the immediate effect on us is relevant.

Encapsulation

To make this kind of abstraction possible, it is necessary that components within the system encapsulate their complexity such that others can abstract it away when interacting with them.

This concept is often talked about within the domain of object-oriented design - a discipline that is perhaps less fashionable than it once was, but still underpins much of the fundamental way in which software is best built. The core idea is that a component should encapsulate its functionality and only present what it does to the outside world, not how it does it. It is not necessary to understand what happens to the engine of a car when you depress the accelerator pedal - only to know that it goes faster.

In normal language, we would call the surface of encapsulated functionality an interface, but our industry loves three letter acronyms, and so we universally tend to use the clunky term Application Programming Interface for this, or API for short.

Recursion

Recursion is a simple-to-explain, difficult-to-grasp context from mathematics that is all over software design. The simple explanation is that it is just the concept of a system in which components can be self-referential within their category. To give an example, I have two parents, and each of them had two parents, and so on. The “and so on” in this sentence is the recursion. This one very simple relationship belies a hugely complex tree of connections - my family tree, in this case.

Through this lens, there are no limits to encapsulation, where each layer of abstraction can be nested within arbitrary other layers of abstraction. Components encapsulate components, recursively. A component has many components within it, each of which has many more. And so on.

The ability to zoom into the right layer of abstraction within this tree of complexity is how we accomplish the task of writing software. By well defining interfaces around components, by abstracting the details of how they work and instead just understanding what they do, and by navigating through a recursive tree of components, we are able to reason about the behaviour of our system at all levels of abstraction.

Modularity

What then, is modular software design? Well, it is an acceptance of the above philosophy, and an embracing of the implications, specifically in thinking about how systems evolve over time. For me, this fundamentally comes down to designing systems such that you maximise your ability to effect change through adding, moving or replacing components, rather than modifying existing ones. This motivation comes from the hard-earned experience that modifying working software is hard, but replacing or adding software can be easy if your setup is optimised for it.

By thinking of systems as networks of interconnected and nested components, and adopting a strategy of ensuring all of those components have well defined interfaces at each layer of abstraction, we build software that is flexible, resilient and easy to reason about.

Top mistakes from Neo Kim

Mike's Notes

This is a compilation of posts taken from different issues of Neo Kim's excellent System Design Newsletter. It's worth subscribing to.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > System Design Newsletter
  • Home > Handbook > 

Last Updated

11/01/2026

Top mistakes from Neo Kim

By: Neo Kim
System Design: 24/12/2025


39 mistakes YOU make in scaling a system  (21/12/2025)

Here are the biggest mistakes I see 99% of YOU making in scaling a system:

  1. Scaling vertically instead of horizontally (and hitting hard limits)
  2. Adding “microservices” too early (plus unnecessary complexity)
  3. Ignoring load balancing
  4. Not using caching at all… and increasing system load linearly with traffic
  5. Caching ‘everything’ blindly (causing stale data, memory pressure, complexity)
  6. Forgetting CDNs for static assets
  7. Keeping the server STATEFUL… (and limiting horizontal scalability + recovery)
  8. Scaling compute before data (databases are usually the first bottleneck)
  9. Treating the database as “infinite” storage
  10. Not using read replicas
  11. Sharding BEFORE understanding access patterns
  12. Never indexing ‘critical’ queries
  13. Allowing SLOW queries reach production… and amplify under load
  14. Blocking requests with “synchronous” processing
  15. Not using QUEUES for background jobs
  16. Ignoring retries and back-off… transient failures are typical in distributed systems
  17. Not setting “timeouts” (and causing thread exhaustion + cascading failures)
  18. Forgetting RATE LIMITS
  19. Letting failures ‘cascade’ by not using circuit breakers
  20. Ignoring backpressure
  21. Deploying ONLY to a single zone/region (and failing during zone/region outages)
  22. No global traffic routing
  23. Manual scaling instead of auto scaling (and moving slowly on traffic spikes)
  24. Shipping without ‘load testing’
  25. Never doing “capacity planning”
  26. Storing big files in databases (instead of object storage)
  27. Sending uncompressed payloads
  28. Making “excessive“ network calls
  29. Not BATCHING for writes
  30. No ‘service discovery’
  31. No failover strategy
  32. No graceful degradation… and disrupting core functionality under load
  33. Retries without ‘idempotency’ (and causing data corruption)
  34. No observability,,, you cannot scale what you cannot measure
  35. No monitoring alerts
  36. No “tracing” across services in a distributed system
  37. Scaling features instead of fixing BOTTLENECKS
  38. ‘Blindly’ copying big tech architectures
  39. Believing scale is about tools,,, not tradeoffs

23 latency mistakes YOU make when building distributed systems (24/12/2025)

Here are the biggest latency mistakes I see 99% of YOU making:

  1. Not indexing CRITICAL database queries (and causing full table scans + slow reads at scale)
  2. Hitting the database ‘repeatedly’ instead of caching hot data
  3. Not using a CDN for static assets and cacheable responses
  4. Deploying everything in a single region,,, and ignoring geographic latency
  5. Designing architectures with too many network hops… and unnecessary microservice chains
  6. Not using load balancers (and overloading individual servers)
  7. Scaling vertically instead of horizontally under CONCURRENT load
  8. Running “inefficient” code and algorithms on the critical path
  9. Executing ‘independent’ work sequentially instead of batching + parallelizing it
  10. Blocking requests with synchronous heavy processing (instead of async queues)
  11. Sending large payloads (instead of efficient compression & serialization)
  12. Sticking to HTTP/1.1 and missing out on multiplexing benefits from HTTP/2 & HTTP/3
  13. Opening new connections per request… instead of connection pooling
  14. Blocking threads with locks or synchronous I/O (and destroying parallelism)
  15. Running systems at “full capacity” with no headroom for traffic bursts
  16. Not measuring latency percentiles or profiling bottlenecks
  17. Allowing cold start affect user-facing requests
  18. Separating data and compute unnecessarily (and forcing extra network calls)
  19. Letting SLOW external dependencies sit on the critical path
  20. Not using ‘timeouts or circuit breakers’ for downstream calls
  21. Relying on slow disks or poorly tuned infrastructure for latency-sensitive paths
  22. Choosing runtimes & languages without tuning for latency
  23. Believing low latency comes from a single optimization instead of disciplined system design & tradeoffs

Latency is rarely caused by one big mistake… It’s the accumulation of many small, avoidable decisions in architecture, code, and execution.

C4 for documenting architecture

Mike's Notes

Pipi is highly complex, with a novel architecture that resembles a moving biological cell, so diagramming to aid understanding will be very challenging.

The C4 model by Simon Brown may be a valuable tool for Pipi to automatically document itself because it follows simple rules and can zoom in and out. Many other tools will also be needed. Some may have to be invented.

Chris Richardson's diagrams look useful as well.

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library >
  • Home > Handbook > 

Last Updated

23/12/2025

C4 for documenting architecture

By: Simon Brown
C4: 23/12/2025

I'm the author of Software Architecture for Developers; a developer-friendly guide to software architecture, technical leadership and the balance with agility. I'm also the creator of the C4 software architecture model and the founder of Structurizr, a collection of tooling to help software teams visualise, document and explore their software architecture.

Ask somebody in the building industry to visually communicate the architecture of a building and you’ll be presented with site plans, floor plans, elevation views, cross-section views and detail drawings. In contrast, ask a software developer to communicate the software architecture of a software system using diagrams and you’ll likely get a confused mess of boxes and lines … inconsistent notation (colour coding, shapes, line styles, etc), ambiguous naming, unlabelled relationships, generic terminology, missing technology choices, mixed abstractions, etc.

As an industry, we do have the Unified Modeling Language (UML), ArchiMate and SysML, but asking whether these provide an effective way to communicate software architecture is often irrelevant because many teams have already thrown them out in favour of much simpler “boxes and lines” diagrams. Abandoning these modelling languages is one thing but, perhaps in the race for agility, many software development teams have lost the ability to communicate visually.

Maps of your code

The C4 model was created as a way to help software development teams describe and communicate software architecture, both during up-front design sessions and when retrospectively documenting an existing codebase. It’s a way to create “maps of your code”, at various levels of detail, in the same way you would use something like Google Maps to zoom in and out of an area you are interested in.

System Context

A system context diagram provides a starting point, showing how the software system in scope fits into the world around it.

Container Diagram

A container diagram zooms into the software system in scope, showing the applications and data stores inside it.

Component Diagram

A component diagram zooms into an individual container, showing the components inside it.

Code Diagram

A code diagram (e.g. UML class) can be used to zoom into an individual component, showing how that component is implemented at the code level.

Uses and benefits

Good software architecture diagrams assist with communication inside and outside of software development/product teams, efficient onboarding of new staff, architecture reviews/evaluations, risk identification (e.g. risk-storming), threat modelling, etc. The goal of the C4 model is to raise the level of maturity associated with software architecture diagrams.

Visualising software architecture with the C4 model - Simon Brown, Agile on the Beach 2019

Just magic - working on Globe for Magiciens de la Terre

Mike's Notes

I was talking with Chuheng in an online meeting yesterday about the Pipi origin story.

I ended up explaining how I work things out visually, like an artist, not like an engineer.

Working for NZ Sculptor Neil Dawson was one of those formative experiences that taught me to work consciously like an artist. Daydreaming and intuition with deadlines, not plans. Just making it up on the day, letting go of the handrails. Initially, I found it difficult; I had been a skilled tradie in heavy engineering, used to fabricating big, complex, expensive structures perfectly.

I discovered, in the course of the Globe project described below, how to consciously create a rich model in my head, work from it, and keep doing so. I still have that model and can fly around in it whenever I want to. The same goes for the thousands of other models floating around in my brain since age 4.

So what has this got to do with Pipi? The truth is, I built up all the internal layers in Pipi one on top of another, like making an oil painting, a film, or a magnificent, tasty dish. A dash of this, a smidgen of that. It has become a Complex Adaptive System. I can see the whole, but the swirling parts connect, and I have no idea how to describe why it works, because its properties emerge from the interactions of the parts. There are hundreds of agents, each with multiple copies, and they self-assemble in different ways at different times. I can't even draw it as it adapts and grows. I suspect I just got lucky. Anyway, it works.

That might be why new, unexpected properties of Pipi keep getting discovered.

That is also why Pipi must generate its own documentation, because I can't anymore.

And that is why Pipi 9, working with a human, will generate Pipi 10 when it is ready and needs to.

Resources

References

  • Barr, Jim; Barr, Mary (1989). Neil Dawson: Site Works 1981–1989. Wellington, New Zealand: National Art Gallery

Repository

  • Home > Ajabbi Research > Library >
  • Home > Handbook > 

Last Updated

03/02/2026

Just magic - working on Globe for Magiciens de la Terre

By: Mike Peters
Redworks: 19/08/2010

Mike is the inventor and architect of Pipi and the founder of Ajabbi.

From late 1987, I had the wonderful opportunity to work for Neil Dawson, a Christchurch-based sculptor who exhibits worldwide.

I was employed as a sculptor's assistant. I got to act as his hands, and it was just fantastic. Every morning when I came in to start work, there would be a pile of A4-sized drawings he had done the night before—any of which could have become a work of art. Occasionally, I was given a drawing and told to go off and make it, or several identical copies. I then realised what "Michelangelo being a factory" meant.

Neil was a really nice chap to work for, very talented, and he taught me a lot by personal example.

From memory, work started on Globe about mid-1988. It was to be hung above the plaza outside the Centre Georges Pompidou in Paris as part of the exhibition Magiciens de la Terre.

First, a small Marquette was made out of beaten metal mesh and presented with location photos.

Bruce Edgar, Neil's technical wiz, found a way to make this impossible object possible through investigations and material trials. Richard Reddaway made a 60cm sphere for a working drawing.

spent two weeks at the University of Canterbury Geography Department watching all of the satellite imagery available. I remember being mesmerised by time-lapse movies lasting months, showing swirling patterns of clouds moving across the planet. There were also books with photos taken by astronauts. It was just stunning!

In an interview, I said, 

"We originally started off projecting photos onto the surface of the sphere and then accurately trying to trace them on. Every little dot and speck, and then going through and editing that. But it was too mechanical, so now it is being done in quite a sort of painterly way - I just try to put in the general sweeps and swirls and then put lots of resolution in. I noticed that all the scientists have got books and books and books on dissecting the weather, but they have no beautiful pictures of what it actually looks like. It's really very interpretive - a funny situation, really - sort of like a ghost painter. I'm painting the world as I see it and Neil comes along and edits it. It's his choice."


The model was then photographed and projected onto a series of hexagon plates made of foam fibreglass composite, eventually bolted together to make a 4.5-meter hollow sphere suspended 25 meters above the Pompidou Plaza.

I traced photos and then cut them out with a router. I can still remember the fibreglass dust and the breathing gear.

I think six people were involved in making Globe. It was carefully assembled and hung from the rafters in Neil's studio. Neil spray-painted it—he did an amazing job—just perfect.

Then Neil and Bruce went to Paris to put it up.

The whole thing was a great experience. After the exhibition, it went to a gallery in New Plymouth and to an outdoor gallery in Australia, where it was eventually destroyed in a storm.

Looking back, Neil showed incredible guts tackling this project. Everyone worked very hard, and technology was at its limit. It was an honour to be part of it.


Micro-Frontends: A Sociotechnical Journey Toward a Modern Frontend Architecture

Mike's Notes

A good way to think of this. It's added clarity to Pipi UI's architecture, so I need to make a tiny change to the CMS Engine, etc. Thanks, Luca.

The CMS Engine datamodel > page_section needed an additional column for the workflow process run by a specific team, agent or system.

30 minutes thinking, 1 minute coding. Done 😁

Now

Page > Module > Component

Resources

References

  • Reference

Repository

  • Home > Ajabbi Research > Library > Subscriptions > InfoQ Weekly Digest
  • Home > Handbook > 

Last Updated

28/11/2025

Micro-Frontends: A Sociotechnical Journey Toward a Modern Frontend Architecture

By: Luca Mezzalira
InfoQ: 24/11/2025

Principal Serverless Specialist Solutions Architect @AWS.

Luca Mezzalira is a Principal Solutions Architect and the author of Building Micro-Frontends. He helps organizations design evolutionary architectures and leads modernization programs for global engineering teams. Luca is also the host of the Micro-Frontends in the Trenches podcast, where he explores the future of distributed frontend systems getting deeper into heuristics, pitfalls, and lessons learned from teams that turned this architectural evolution into a competitive advantage. The podcast can be listened to on Spotify, Apple Podcasts or YouTube

  • Micro-frontends are different from components. Components are abstraction mechanisms designed for standardisation and reuse, while micro-frontends optimise for autonomy and flow.
  • Micro-frontends are not a technical pattern, but a sociotechnical shift mirroring Conway’s law.
  • The micro-frontends migration is not a binary decision but a continuum. The migration should start where autonomy brings the most value. The architecture needs to be aligned with the team structure.
  • Embrace duplication when it accelerates flow and favours iterative delivery over big rewrites. 
  • The first micro-frontend should go end-to-end: from design and development through deployment and observability. That vertical slice will surface every challenge you’ll face later - routing, shared dependencies, authentication, monitoring - on a scale that’s still manageable. 

For years, distributed systems have defined how we think about backend architecture. We’ve learned to break apart monoliths into independently deployable services, embracing autonomy, faster feedback, and continuous change. But on the frontend, many organisations are still trapped in the same cycle we escaped on the backend: large codebases that slow teams down, coupled deployments that introduce risk, and interfaces so entangled that any change becomes an exercise in fear management.

The rise of micro-frontends is not simply a reaction to this pain; it’s part of a deeper sociotechnical evolution. The same forces that once drove backend modularisation are now reshaping the frontend. As organisations demand faster delivery, greater autonomy, and continuous modernisation, our frontend architectures must evolve in step with our teams.

The distributed frontend era is here, but it’s not defined by new frameworks or fancy tooling. It’s defined by the way we align people, processes, and architecture around a shared goal: delivering value faster without losing control.

In this article, you’ll learn when micro-frontends make sense, how to evolve existing systems safely, and how to handle cross-cutting concerns like routing, state, and user experience without disrupting delivery. The goal is to show how micro-frontends represent not a trend, but a natural sociotechnical evolution.


Multiple teams working together on the same UI leveraging the independent nature of micro-frontends

Rethinking Micro-Frontends

This article focuses on architectural principles rather than any specific framework or vendor solution.

Micro-frontends are often introduced as a technical pattern - a way to break a large frontend into smaller, independently deployable pieces. But that framing misses the point. Micro-frontends are not a new stack; they are a new way of structuring work. They represent a sociotechnical shift - one that mirrors Conway’s Law, which tells us that system design reflects communication structures.

When teams are forced to coordinate through a single release train, decision-making slows. When every change requires syncing across multiple domains, creativity fades. The result is not just technical debt but organisational inertia. Micro-frontends reverse that dynamic. They allow teams to own slices of the product end-to-end - domain, design, delivery - without waiting for centralised approval.

It’s also essential to distinguish micro-frontends from components. Components are a software abstraction mechanism designed to standardise and reuse behaviours or interfaces within a single application or shared ecosystem. They optimise consistency and maintainability. Micro-frontends, instead, maximise independence and flow. They exist to reduce cognitive load, enable teams to make decisions autonomously, and accelerate delivery by removing cross-team dependencies. As a result, micro-frontends operate at a much coarser granularity than components. Where components encapsulate behaviours, micro-frontends encapsulate responsibility, for example, a complete vertical slice of the system owned and operated by a single team.

That autonomy doesn’t come for free. Distributed systems always bring complexity. But when applied thoughtfully, micro-frontends unlock a kind of evolutionary architecture that’s been missing from the client side: the ability to change direction safely and iteratively, at the speed of the business.

When Micro-Frontends Make Sense

Not every system needs to be decomposed. A small product built by a single team can thrive perfectly well with a modular monolith. The signal that you may need micro-frontends is rarely technical - it’s organisational.

If your release cadence slows down as your team grows, if changes in one area routinely break others, if onboarding new engineers feels like navigating an archaeological dig through years of intertwined code - these are the symptoms of a system that has outgrown its structure.

Micro-frontends address this by restoring local autonomy. Each team can ship independently, evolve its stack, and respond to customer needs without waiting for the rest of the organisation to move. The return on investment becomes visible early because, unlike a complete rewrite, migration to micro-frontends is incremental. Each small step delivers value on its own, reducing risk and building confidence.

At one media company I worked with, the shift from a shared frontend to domain-owned micro-frontends reduced coordination effort per release by more than half. Deployment frequency rose tenfold within months. That momentum created the trust needed to continue the migration safely.

But micro-frontends are not a silver bullet. For small teams or products with limited complexity, the overhead might outweigh the benefits. The goal is not to adopt a pattern for its own sake but to solve concrete problems: delivery bottlenecks, scaling limits, and the inability to modernise safely.

Evolving Existing Systems

The hardest part of adopting micro-frontends is not starting fresh; it’s evolving what already exists. Most organisations operate in brownfield environments - large, mature systems that can’t be simply replaced. The challenge is to introduce modularity without breaking everything that works today.

The first principle is to think iteratively. A successful migration doesn’t require shutting down the old world to build the new one. Instead, it uses the old system as scaffolding while gradually introducing new components. Each iteration becomes a slight, measurable improvement - an opportunity to validate assumptions, build new capabilities, and reduce uncertainty.

An iterative approach delivers faster ROI and lowers risk. It allows the business to continue operating while modernisation happens in parallel. It also keeps teams motivated, as each release produces visible progress rather than waiting months for the "big reveal".

From Big Bang to Incremental Change

In backend modernisation, the strangler fig pattern has long been used to gradually replace monolithic systems. The same principle applies beautifully to the frontend. Instead of mixing old and new code in the same pages, route traffic at the edge to decide which version of the application should serve a given request.

By placing routing logic at the CDN or edge layer, you can divert specific paths - say, /checkout or /dashboard - to a new micro-frontend while leaving the rest of the site untouched. If something goes wrong, rollback is instant: just change a routing rule. There’s no need to redeploy or revert code.


Micro-frontends iterative migration with edge-compute

This pattern also unlocks powerful release strategies. You can test new experiences with canary deployments, feature flags, or country-based rollouts, and collect honest feedback before a full release. This iterative rhythm builds trust between technical and business stakeholders. Each deployment becomes both a delivery and a learning opportunity.

The key is to resist the temptation to blend old and new UI within the same page. Mixing rendering systems multiplies complexity and breaks the isolation that makes micro-frontends valuable in the first place. Clear, page-level or route-level boundaries keep the migration safe and reversible.

Planning the Migration

Every migration is a balance between impact and safety. The first module you choose sets the tone for everything that follows. It should be meaningful enough to prove value but isolated enough to minimise risk.

In practice, this often means starting with a new feature or a module already scheduled for major refactoring. That way, the migration effort aligns naturally with business goals - you’re not just modernising for its own sake, you’re enabling new capabilities.

Your first micro-frontend should go end-to-end: from design and development through deployment and observability. That vertical slice will surface every challenge you’ll face later - routing, shared dependencies, authentication, monitoring - on a scale that’s still manageable. The lessons learned there will inform every subsequent migration step.

Think of it as your pilot. If it works, it becomes a reusable template; if it doesn’t, you’ve lost little and gained invaluable insight. Treat migration not as a project but as an ongoing evolutionary process. Each success builds momentum. Each mistake refines your heuristics.

Designing for Modularity

When starting a greenfield project, micro-frontends should align business domains, not technical layers. Instead of organising code by framework or feature type, design independent product capabilities that map to real user needs - catalogue, checkout, profile, analytics.

This domain-driven alignment enables micro-frontends to scale. Each module becomes a boundary between both code and communication. Teams own their space end to end, choosing their technologies, deployment pipelines, and release rhythms. Over time, this reduces coupling not only between systems but between people.

That autonomy requires a contract of trust. Shared guidelines - like how routing works, how design tokens are managed, or how observability is implemented - create coherence without reintroducing central control. The goal is a federation of teams, not anarchy of frameworks.

Evolutionary architecture is not about predicting the future; it’s about being ready for it. Designing change means optimising reversibility. Every decision - tooling, boundary, dependency - should be easy to revisit. The systems that last are those that can adapt, not those that were perfect on day one.

Handling Cross-Cutting Concerns

As with any distributed system, the most complex parts are the seams: routing, authentication, shared state, and user experience. These are the invisible threads that make a product feel cohesive.

Routing is the backbone of your migration. Centralising it at the edge keeps logic out of your application code and simplifies rollback. Using absolute URLs for navigation between systems ensures clarity and predictability. If something breaks, users never land in an undefined state - they’re simply redirected to the stable version.

In practice, this approach turns the edge into a single source of truth for traffic control during the migration period. Instead of embedding conditional logic inside every frontend, edge functions can decide, in milliseconds, whether a request should go to the legacy monolith or to a specific micro-frontend. This also enables progressive rollout strategies, such as canary releases or blue-green deployments, without touching your frontend code.

One of the most valuable advantages is the ability to roll back instantly without polluting your legacy codebase or new micro-frontends with temporary routing hacks. If an issue appears, you simply switch the routing rule at the edge, and all traffic flows back to the stable version. There’s no redeployment, no manual intervention inside the applications, and no need to maintain hybrid rendering layers that mix old and new UI code. The separation between systems remains clean and reversible, which is critical for long-running migrations.

Centralised routing also reduces cognitive load for teams and improves platform stability. Developers no longer need to maintain throwaway routing logic in multiple repositories or synchronise URL patterns between systems. It also simplifies observability, since all incoming requests pass through a single control point that can emit consistent metrics and logs.

Authentication can often be handled more simply than teams expect. As long as all micro-frontends share the same subdomain as the legacy app, they can access the same cookies and session data.

Implementing refreshed token logic in both worlds keeps sessions alive without complex cross-app communication.

State management in micro-frontends should remain local to each application to preserve independence and avoid cross-team coupling. When multiple micro-frontends need to communicate, prefer loosely coupled events that broadcast intent and data where necessary over enforcing shared runtime dependencies. This reinforces architectural boundaries while enabling collaboration where required.

Some configurations, such as authentication tokens, locale, or feature flags, can be shared across systems using stable mechanisms, such as cookies or local storage, or by injecting a lightweight context into each micro-frontend. The key is to keep this shared layer minimal and predictable.

Embracing Duplication (Wisely)

Duplication in distributed systems should never be accidental; it should be intentional. The goal is not to eliminate all repetition but to make conscious decisions about where duplication helps teams move faster and where it introduces unnecessary overhead.

Good heuristics lie at the intersection of complexity and rate of change. For example, a design system evolves quickly during its early phases but stabilises over time. Once mature, its release cycle slows down - nobody changes the design system every day. Similarly, a shared logging library across multiple micro-frontends has low volatility and clear behaviour, so centralising it makes sense. On the other hand, consider a complex video player in a streaming platform. Its heuristics may vary by browser or device to optimise buffering, latency, or startup times. The complexity is high and the rate of change is frequent, so duplication would only multiply maintenance pain without tangible benefits.

Conversely, if you’re dealing with a simple component - say, a basic form of field or small utility - that doesn’t change often and requires minimal effort to implement, feel free to duplicate it. Abstraction can always come later once patterns are proven, and the need for consistency is evident.

Every shared abstraction introduces governance, versioning, and ownership responsibilities. Leaving ownership inside the team responsible for a micro-frontend simplifies the process and preserves autonomy. The best abstractions are those that emerge naturally, not those imposed prematurely. Intentional duplication buys you speed and flexibility; thoughtful consolidation gives you long-term coherence. Balancing the two is what makes distributed architectures evolve gracefully.

The Fastest Path to Modernisation

One of the most persistent myths about micro-frontends is that you need microservices to support them. You don’t. The frontend and backend evolve at different speeds, and the frontend almost always moves faster.

Micro-frontends focus on how teams build and deliver interfaces, not on how data is served. As long as you have stable API contracts, your frontend can modernise independently of your backend. You can keep your monolithic APIs while introducing modular frontends on top of them. The stateless nature of frontends makes them ideal for incremental modernisation.

Backend migrations are often lengthy because data has gravity. Schema changes, replication strategies, and legacy dependencies can stretch timelines for months or years. Frontend migrations, by contrast, can deliver visible value in weeks. You can start improving performance, maintainability, and user experience immediately, without waiting for backend modernisation to catch up.

At a retail company I advised, the frontend migration to micro-frontends was completed in roughly 14 months. The backend modernisation took twice as long, but the organisation saw immediate value early on through faster releases and a better user experience, even while the backend was still monolithic.

The frontend can lead the way, serving as a proving ground for distributed practices and a catalyst for broader organisational change.

Modernisation at Human Speed

Modernisation is not a single event; it’s a journey. The temptation to start over is strong, especially when legacy systems feel immovable. But the "big bang" approach rarely succeeds. It freezes business progress, drains morale, and often ends in partial rewrites that never reach production.

Micro-frontends offer a different path, one that aligns with how organisations truly evolve. They let you move at human speed: fast enough to show progress, slow enough to stay safe. They encourage experimentation, continuous learning, and local ownership.

Every migration is a balancing act between ideal architecture and practical delivery. The teams that succeed are those that accept imperfections as part of the process. They know that good architecture is not about purity; it’s about flow.

If there’s a guiding principle to remember, it’s this: evolutionary beats revolutionary. Iterate, learn, and adapt. The systems you build today will change tomorrow, and that’s a feature, not a flaw.

For teams exploring this path, the distributed frontend era is not about frameworks or bundlers. It’s about designing systems and organisations that can evolve continuously, safely, and with purpose.

Conclusion

Micro-frontends are more than a technical pattern; they reflect how modern organisations build software. They embody the shift from centralised control to distributed ownership, from significant releases to continuous flow, from architectural rigidity to evolutionary change.

By approaching migration iteratively - starting small, learning fast, and keeping business goals at the centre - you can modernise your frontend without halting innovation. Whether your backend is still monolithic or already distributed doesn’t matter. What matters is your ability to evolve safely and deliver value continuously.

Architecture, at its best, mirrors the way people collaborate. When teams are empowered, systems follow. Micro-frontends are simply the architectural expression of that truth.