What I Learned From Splitting One Big App Into Smaller Ones
A while back, our app had grown into one large program that did everything — orders, payments, user accounts, all of it, living in one codebase. Every time someone touched the payments part, they risked breaking something in the orders part by accident. Deploys got scary. So we did what a lot of teams do at that point: we split it into smaller, separate services that each did one job and talked to each other over the network.
I want to be honest about something, because most articles about this topic aren't: splitting the app didn't remove our problems. It just moved them somewhere else, and introduced a few new ones we hadn't planned for.
The problem doesn't disappear, it relocates#
Before the split, if the orders code needed something from the payments code, it was just a function call — instant, and either it worked or it threw an error you could see immediately.
After the split, that same request has to travel across the network to a different service, wait for a response, and handle the case where that other service is slow, or down, or half-working. We didn't get rid of complexity. We traded "one big confusing codebase" for "several smaller codebases plus a bunch of new ways for them to fail while talking to each other."
That's not a reason to avoid splitting things up — sometimes it's genuinely the right call. It's just a reason to go in with your eyes open.
Three things that actually helped#
1. Each service keeps its own data, no exceptions. Early on, someone on the team had the orders service read directly from the payments database, just for one quick report. It seemed harmless. Three months later, changing anything in the payments database meant checking whether it would quietly break that one report nobody remembered existed. Now the rule is simple: if you need data from another service, you ask that service for it — you don't go looking in its storage yourself.
2. Every request gets a tracking number. When a request comes in, we now attach a unique ID to it right at the start, and that same ID gets passed along to every service it touches and printed in every log line. Before we did this, debugging a slow checkout meant opening logs from five different services and trying to line them up by guessing the timestamps. Now I just search for one ID and see the whole story, in order, across every service it passed through.
3. "Is it healthy?" needs to mean something. We used to have each service report "I'm fine!" as long as the program was running at all — even if it couldn't actually reach its database. That's like a person saying they're fine while unable to stand up. Now, a service only reports "I'm fine" if it's actually able to do its job, not just if the lights are on.
If you're about to do this yourself#
My honest suggestion: do the boring stuff first. Get your tracking IDs and your logging sorted out, get your health checks telling the truth, before you split anything up. It's tempting to jump straight to drawing the new architecture, because that's the fun part. But the boring plumbing is what actually saves you the night something breaks — and something will break, that's not pessimism, that's just what happens once a single request depends on five things instead of one.
Get new posts as a 30-second summary
no spam, no schedule, just a short cheatsheet-style recap with a link to the full post whenever I actually publish something