Why we deleted our micro-frontends
Codat's frontend used to be a distributed system: a handful of separately built, separately deployed applications stitched together at runtime behind a shell. We've merged all of it back into one application in one repository. It took about four months. Customers never noticed.
Splitting it up was a defensible decision when it was made, in a company that looked different from this one. We paid for it for a long time afterwards. This is what the bill came to.
What we thought we were buying
The case for splitting a frontend is always an organisational one. Teams get blocked on each other, nobody ships without pain, so you draw lines through the product, hand each team a piece, and describe the result as autonomy.
What we got was narrower than that. Our teams could deploy without asking anyone, which is worth having. What they couldn't do was see each other. And the coordination we'd tried to remove didn't disappear, it moved somewhere more expensive. A merge conflict is cheap, because the two people who caused it fix it in ten minutes. A disagreement about the shape of an event payload, between two teams who own two repositories, takes a Slack thread, a ticket and a fortnight. We swapped a compiler error for a meeting.

The bill
The biggest line item was dependencies. When each application builds on its own you don't get a dependency tree, you get a matrix: two versions of the framework, four different pins of the design system. A design system that exists at four versions at once is four things sharing a name. The browser pays as well, downloading the same framework code once per slice.

You could watch the language change under you as you moved between repos: a different TypeScript in each, a router a few versions adrift, one slice still running a React that had been end-of-life for almost five years. Nobody decided any of that. It's just what a codebase does when no single place has to agree with itself. The user paid for the split too, not only us: a page load could fan out into seven separate fetches for code, and any one of them failing was a blank screen, because there was no compiler in between to have caught it first.

One tree fixed that. An upgrade or a CVE patch is now a single PR, verified by a single CI run and applied everywhere. The volume of dependency PRs fell by roughly an order of magnitude, though the number matters less than the fact that upgrades stopped being optional. There's nowhere for a stale version to hide.
The second line item was debt. Splitting a system doesn't reduce technical debt, it moves it somewhere you can't see. A repo nobody reads, owned by a team that reorganised twice since, where it sits quite happily until it becomes an incident. In one repository you can grep for it. Codemods work again. So do atomic changes: you can alter a shared component and every caller of it in a single commit, with CI telling you immediately whether you were wrong, instead of publishing a version and waiting months for the last consumer to adopt it.
Our boundaries also got types. Custom events and conventions about what goes in the URL were contracts checked at runtime, which is a polite way of saying our users checked them for us. Merging the repositories turned them into compile-time contracts, and the first week surfaced an embarrassing number of bugs that had been sitting there for ages.
Then there's the overhead. Every micro-frontend carries a repository, a pipeline, a bundler config that rots, a test setup with its own flakiness, a deployment target, an observability config, a dependency tree, and a local dev story that somebody has to keep alive. Multiply that by however many boundaries you drew, and you've built a platform team's worth of undifferentiated work and handed it out, unfunded, to your product engineers. In one repo it gets done once, properly, by whoever cares most about it.
Consistency, and silos
We used to chase consistency with process: a component library, guidelines, a review forum, a fair amount of evangelism. All of that is a sign the architecture is fighting you. You only need governance when the code can't enforce the thing you want.
Now the consistent option is the lazy one. There's one button. You use it because it's there, and writing a second one would be more work, and someone would ask you why in review. The seams a user could see went too. No more full-page reload when you crossed an invisible internal border, no more half-second of the wrong font.
While each team owned a repository, the repository was the edge of what anyone was curious about. Engineers didn't read code they couldn't run, and reviews stopped at the border. We solved the same problem multiple times, without anyone noticing. Now people wander. They read adjacent code because it turns up in the same search, and they'll open a PR against something they don't own rather than file a ticket about it. Ownership is still real. It's enforced by review requirements rather than by a repository boundary nobody could see past, and engineers can move between areas of the product without hitting an onboarding cliff, which fixed a staffing problem we'd been trying to solve with org charts.
New starters clone one repo and run one command. It used to take them days to find everything.
What we should have done
We wanted modularity and we bought distribution, and those aren't the same purchase. Modularity is the good part: boundaries you can enforce, code that can't reach where it shouldn't, ownership that means something, changes that don't ripple outwards. All of it is available inside a single build, with tooling that's cheap and dull. Lint rules that enforce module boundaries. Tests that assert which way dependencies are allowed to point. Ownership at the folder level. Feature flags, so that releasing something and deploying it stop being the same event.
Distribution adds version boundaries and deployment boundaries on top of that, and unless you need them, they're cost. Deployment independence solves a deployment problem. If your deploys are slow or frightening, fix the deploys. Trunk-based development and a fast pipeline will have you shipping several times a day without splitting a single bundle.

We'd still split under some circumstances. If CI can't keep up however you cache it. As a strangler pattern to get off a legacy stack, treated as a transition and not a destination. If an acquisition leaves you with two products that share a domain and nothing else. None of that was true of us, and I doubt it's true of most teams having this argument.
Why this is on a careers page
Taking apart an architecture you built is uncomfortable. It means writing down, where your colleagues will read it, that something you made carefully is now in the way.
Nobody had to defend the original decision in order to protect their standing. It was made with the information available at the time, and everyone treated it that way. The case for reversing it got written up and argued over, then tested with a spike before anyone committed. The engineers who'd built within the old architecture led the merge, partly because they knew where the bodies were buried and partly because being wrong in public isn't a career event here.
We build financial infrastructure. Our customers put financial products on top of our platform, and they need it dependable and boringly correct. That rewards engineers who care more about being right in eighteen months than about winning today's argument, and who'll look at their own old decisions without much sentiment and ask whether they're still earning their keep.
If that's how you'd want to work, you may be a good fit at Codat, and we'd love to have a chat. See our open engineering roles.
