Architecture: Write It Down Before Rewriting
The Problem
Part of the "How to bring order to a project" series - the architecture track. Same context: a growing gamedev project, about twenty services on different stacks, an architecture that "grew historically." Nobody designed it badly - nobody designed it at all: services appeared as needs arose, boundaries were drawn by circumstance, decisions lived in chats and heads. That's a normal phase - up to a point.
The signals that the point has arrived:
- incidents increasingly happen at the seams between services, not inside them;
- every change starts with an argument about "whose area is this anyway";
- estimates keep growing, because "we need to be careful there, we don't know what we might break";
- the only answer to "why is it built this way?" is "historically."
The track's main thesis: order in architecture starts not with rewriting, but with writing down. Half of the "architecture problems" dissolve once the system is honestly described as it is. The other half turns from "scary to touch" into tasks with a price and a plan.
Step 1. Service Passports
The first tool is boring to the point of indecency: every service gets a passport. One page:
# <service>
Why it exists: one sentence.
Area of responsibility: what it does.
Boundaries: what it does NOT do, even when "it would be convenient here."
Stack, port, status (production / deprecated / experiment).
Owner: a name, not "the team in general."
Operations: how it deploys, where the configs are, where it logs.
Three rules that keep passports alive instead of becoming a wiki graveyard:
- The owner writes it, not the architect. A passport is "I'm responsible for this and here's how it works," not "management told me to document it."
- Half an hour, no more. A passport is not design documentation. If it takes a day, you're writing something else.
- Updated on touch. Changed the boundaries or the deploy method - update the passport in the same merge request. Dedicated "documentation days" don't exist; they don't work.
Filling in the passports is diagnostics in itself: you keep catching moments of "wait, why is this even here?" - and those are the right questions, there was just nobody to ask them before. A couple of our services honestly moved to deprecated status during passportization - it turned out habit was the only thing keeping them alive.
Step 2. The System Map
The passports add up to a map - two artifacts:
- The registry - a table of all services: type, stack, status, owner. Boring, but this is where you see that there aren't "about fifteen" services, but exactly nineteen, three of which are alive only in legends.
- The call graph - in text, per service: whom it calls, over which protocol, and why. For example:
shop → accounts service HTTP REST ownership check at purchase
shop → game API HTTP REST progress levels (callback)
shop ← payment webhooks HTTPS events from payment providers
No pretty arrows at the start - a table is enough. And it has a property a slide-deck diagram never will: it lives in git next to the code, so it can be reviewed and diffed. Changed an integration - the map's row changes in the same merge request; a mismatch is caught in review, not a year later.
The rule is the same as in the state snapshot: trust reality, not documentation. A map that shows the desired state is worse than no map - people make wrong decisions with full confidence.
Step 3. Boundaries: Where a Service Ends
The most expensive architecture bugs live not inside services but between them - where two services consider the same thing "theirs."
The signs of broken boundaries are recognizable:
- two services write to the same database table;
- the same business logic is duplicated in two places "for speed";
- a service reaches into a neighbor's internals past its API, "because it's simpler";
- the first half hour of every incident goes into figuring out whose area it is.
Our classic was the payment flow: historically payments started in the accounts service, then the shop grew its own purchases and webhooks - and the logic sprawled across two homes. Every payment incident opened with archaeology: which of the two services was supposed to handle this event? Duplicated handlers masked the problem right until the day they both handled the same event.
The cure is a boundaries document. For every service, explicitly:
- inside: what the service is solely responsible for;
- outside: what's adjacent but belongs to someone else (and to whom exactly);
- interface: how others interact with it - and that's the only door.
The order of the fix matters: document first, code second. First the team agrees on paper where payment events live (in our case - webhooks in the shop, accounts only read status via API), and only then the code is brought to the agreement. The reverse doesn't work: code without an agreement sprawls back within a quarter.
The goal isn't beauty but two practical properties: no overlaps (every entity has one owner) and growth control (a new feature first gets an address - which service it goes to and why - instead of settling wherever).
Step 4. Decisions on Paper
Architecture "in heads" dies when people leave. Two document genres stuck with us, and the difference between them matters:
- RFC - before a change. Half a page, by template: Problem (what hurts, in numbers or incidents), Options (2-3, including "do nothing"), Decision (which one and why), Cost (work, risks, what about rollback). The lifecycle is simple: Draft → Review → Approved/Rejected → Implemented. A new RFC is automatically announced in the team channel - the review is open, and objections at the Draft stage cost one comment instead of a production rework. A live example: the proposal to replace direct calls between game services with a message bus went through three review iterations - and the final design (with a journal and delivery guarantees) had almost nothing in common with the first draft. All those iterations happened before the first line of code.
- ADRs and research - around a decision. A short record: what was decided, why, what was considered and rejected. Plus research for specific risks - "how to back up the analytics database," "the plan for moving data off the game host." The difference from an RFC: nothing is being proposed here; knowledge is being recorded - so the same research doesn't get redone a year later.
And one rule on top of both genres: before any architecture work - check the index first. Don't reinvent what's already decided; don't start a migration if there's an open document about it.
Step 5. Tech Debt with a Price Tag
Everyone has a tech debt list; it only works with two extra columns:
| Debt | What it blocks | Cost to fix | |
|---|---|---|---|
| Shop and accounts share payment logic | every payment incident ×2 in time; new providers go into two places | ~3 weeks: boundaries + moving the webhooks | |
| No backups for the analytics DB | losing all analytics on a disk failure | research done, ~1 week to implement |
"Blocks" is the main column. Debt that blocks nothing is not debt - it's a trait of the system; cross it out and stop carrying it. Debt that makes every feature in a service cost ×2 is the first candidate for work - and now that's visible not only to the tech lead, but to the product manager who sets priorities.
The list's sources are the state snapshot and incident reviews. Debt is paid down two ways: small - by the touch rule (touching a service - leave it cleaner), large - through an RFC, like any architecture change. Once a quarter the list is revised: some items closed by touch, some stopped blocking, some grew into an RFC.
How Not to Slide into Bureaucracy
The honest risk of this track: getting carried away and demanding an RFC for renaming a variable. The antidote is explicit thresholds. For us, an RFC is mandatory when a change:
- moves a boundary between services (or creates a new service);
- touches data: a shared DB schema, a migration, a storage change;
- touches money: payment flows, billing;
- is irreversible or expensive to roll back.
Everything else is a regular merge request with a regular review.
And the second antidote: all documents live next to the code or in one wiki with a single index, get updated on touch, and fit on a page. A document nobody can find or finish reading - that's what bureaucracy is.
What It Buys You: Evolution Without Revolution
With the map, the boundaries, and the decision process in place, big rebuilds stop being leaps of faith. From the same project's practice - three rebuilds, all through RFCs and along the boundaries:
- The most loaded service was rewritten in another language - entirely behind its own boundary. The API contract was pinned in the passport and the boundaries document, so "rewrite the internals" became a local task: the other seventeen services noticed nothing. Without a pinned contract this would have been "rewriting the system."
- The shop was extracted from the "main API" that did everything - first the boundaries document got the line "purchases, cases, storefront - a separate service," then the code moved along that boundary. Extraction by document rather than intuition is also a ready checklist: what moves, what stays, where the interface is.
- Data was separated from compute onto different hosts - following a research document with a migration plan, in stages, with rollback points. Not "overnight on Saturday," but a boring sequence of steps, each of which could be stopped.
None of these rebuilds was a "big rewrite." Each was a local operation with a clear price and a rollback. That's the point of the track: architectural order is not a pretty diagram on the wall - it's cheap changes. The diagram is a by-product.
The Track's Checklist
- ⬜ A passport for every service: why, area, boundaries, owner, operations
- ⬜ Owners write the passports; updates on touch, in the same merge request
- ⬜ Service registry + call graph in text, in git; verified against reality
- ⬜ Boundaries document: inside / outside / interface; one owner per entity
- ⬜ Fixing boundaries: agreement on paper first, code second
- ⬜ RFC before a change (problem → options → decision → cost); open review in the channel
- ⬜ Explicit RFC thresholds: boundaries, data, money, irreversibility - the rest is a regular MR
- ⬜ ADRs and research: record knowledge, "index first"
- ⬜ Tech debt with "what it blocks" and "cost" columns; quarterly revision
- ⬜ Small debt - by touch; large - through an RFC
The other tracks of the series are on the map. Originally published at mind.mansur.expert.
Comments
No comments yet. Start the discussion.