Teardown: finding boundaries before splitting services
Reading a student records system through finding boundaries before splitting services, starting with the schema rather than the docs.
We inherit systems more often than we start them. This is how we read one: what we look at first, and what each thing tells us about decisions made by people we'll never meet.
A service boundary in the wrong place costs more than no boundary at all. Find the seams in the domain while everything is still one deployable, and only then decide whether any of them are worth a network hop.
SEAMS ARE FOUND, NOT CHOSEN
The seams in a system are wherever two parts of the business genuinely change on different schedules. You can't pick those from an architecture diagram. You find them by watching which files move together in six months of commits, and by noticing which conversations never involve the same two people.
Draw the boundary somewhere else and you've created a distributed transaction where a function call would have done. Every feature after that pays a tax: two deploys, a migration on each side, and a failure mode where half the change is live.
MODULES FIRST, NETWORK LATER
Enforce the boundary inside one codebase first. Separate modules, no shared tables, communication through an explicit interface. That gets you almost all the design benefit and none of the operational cost, and it's cheap to move if you drew the line in the wrong place.
If the boundary survives a year of features without anyone needing to reach across it, it's real, and you can consider a network. Most don't survive, which is exactly the information you wanted before committing to a deployment topology.
The exception handlers were the most honest documentation in the repo. Every branch that swallowed an error marked a spot where registrars had once been paged and somebody had made it stop.
WHAT ACTUALLY JUSTIFIES A SPLIT
There are honest reasons: independent scaling of something genuinely heavy, a compliance boundary that has to be physical, a team that needs to ship without coordinating. Notice that two of the three are organisational. That's normal and worth saying out loud.
"Microservices" as a goal isn't a reason, and neither is a diagram that looks tidier. If you can't name which of the three you're buying, you're paying the cost for nothing.
The schema told the real story. Three nullable columns added in one migration, all named after an enrolment record field, all populated for precisely the weeks when a withdrawal was backdated after the census had been filed was an open ticket.
WHERE IT GOES WRONG
- Splitting for scale on a component that turns out to be one percent of load.
- Distributed transactions across a seam that a function call handled fine last year.
- A boundary drawn from the org chart, requiring a two-service deploy for every ordinary feature.
- Two services sharing a database table, which is one service with extra latency.
Enforce the boundary as a module first. Earn the network hop later.
THE VERDICT
Better than its reputation. Most of what looked like bad engineering turned out to be a sensible answer to a constraint that had since been lifted, and nobody had gone back to check.