Code arrives faster now. Not a little faster — a different order of magnitude. A feature that used to be a week of typing is an afternoon, and the pull request that lands on Monday is longer than what the same person shipped in a month two years ago.
Everyone noticed the speed. What I think we've been slower to notice is what the speed broke. It isn't review, exactly. Reviewers still read diffs, and they still catch things. What broke is the quiet side effect review used to have: at the end of a normal week, somebody on the team understood the shape of the system. Not because anyone wrote it down. Because the code went past their eyes slowly enough to leave an impression.
That impression is gone. The diff still tells you what changed. Nothing tells you what the system now is.
The thing that stopped scaling isn't reading
It's tempting to treat this as a review-throughput problem — more reviewers, smaller PRs, better tooling. But the shortage isn't attention. It's that a diff is the wrong shape for the question people actually need answered.
A diff is a list of edits, ordered by file. The architecture is a set of relationships, ordered by nothing. When a change adds an event, routes it through a policy, and quietly does not connect it to anything a user can see, the diff shows you three files that each look completely reasonable. The defect isn't in any of them. It's in the space between them, and the space between files is exactly what a diff has no way to render.
That was always true. It just didn't hurt much when the system grew at the speed of human typing, because the mental map decayed slower than the code changed. Now it's the other way around. The map is out of date before you finish reading it.
Diagrams were the old answer, and they rot
The traditional fix is a picture. Draw the architecture, put it in the wiki, keep it current.
Nobody keeps it current. This is so universal that we've stopped treating it as a failure and started treating it as weather. The reason isn't laziness — it's that a diagram is a rendering, and updating a rendering is manual work with no consequence attached to skipping it. Nothing breaks when the picture is wrong. It just becomes quietly misleading, which is worse than absent.
Event modeling gets something important right here: it fixes the shape of a flow. Actor issues a command, command produces an event, event feeds a read-model, read-model shows up on a screen. That vocabulary is a real contribution, because it makes "is this flow complete?" a question with an answer instead of a matter of taste.
Where it stops short, in my experience, is storage. A swimlane board is a view, not a data structure. It reads beautifully for one flow and fragments the moment cross-cutting concerns show up, because you can't query a picture and you can't diff it usefully either.
So invert it. Make the graph the storage and the picture one projection of it, rebuilt on demand around whatever you happen to be asking about.
What a machine-readable model buys you
Once the model is a data structure sitting in the repo next to the code — versioned, diffable, boring — two things become possible that a picture can never do. They're the two things the speed took away.
A human can ask for a summary instead of a map. Not the whole architecture; nobody can read the whole architecture, and a whole-graph rendering dies somewhere around forty nodes exactly like a growing swimlane board does. A slice: this one event, and everything structurally adjacent to it.
A machine can be told what "incomplete" means. Not "does this look right," which is judgment and doesn't scale, but "which gaps are structurally impossible to leave open," which is a query.
That second one is the part I care about most, and it's worth being precise about why. In How to Rescue Software You're Afraid to Touch I argued that a test suite is what turns AI from a plausible-code generator into something that converges on correct code, because it gives the model a ground truth outside its own confidence. That's still true — for behavior. Tests tell you the code does what it did yesterday. They have nothing to say about whether the system you're assembling makes sense as a whole, because every test in the following example passes.
One missing line
Here's a small ordering system. A command produces three events:
produces:
pay-order: [payment-taken, payment-refused, payment-settled]
projects-to:
payment-taken: [order-status]
payment-settled: [order-status]Read that as prose: paying an order can succeed, can be refused, and eventually settles. Two of those outcomes feed the read-model behind the order-tracking screen.
The third doesn't. payment-refused is produced by working code, belongs to the payment aggregate, is covered by tests, and is consumed by nobody. The customer's payment fails and the tracking screen shows them nothing at all — not an error, not a retry prompt, just an order that never advances.
Now notice what kind of defect that is. There is no wrong line anywhere. The bug is a line that isn't there, in a file that isn't in the diff, and code review has no mechanism for noticing absence. The payment service is right. The projection is right. Every test on both sides is green. This is the class of failure that survives review indefinitely, and it gets more common, not less, as the volume of generated code goes up — because plausible code is very good at being locally correct.
Ask the graph, though, and it's not subtle:
✗ orders.payment-refused
no read-model or policy consumes this event [event-no-consumer]
→ Add a projects-to edge to a read-model, a triggers edge to a policy,
or set data.terminal to a reason.
1 error(s), 0 warning(s) across 21 nodesThat's the whole idea. Not "the architecture looks off." A named node, a named rule, and the specific edges that would close it.
Gaps an agent can act on
The finding above is written for a machine as much as for me, and that's deliberate. It names the node, names the rule, and lists the exact moves that resolve it — which makes the loop trivial: ask for the most pressing gaps, fill exactly one, ask again. An agent can run that loop without inventing anything, because at no point is it asked for an opinion about whether the architecture is good. It's asked whether a specific relationship exists.
The design constraint that makes this work is the one that's easy to skip: a rule you can't tell it's wrong becomes noise. Some events genuinely have no consumer. So the third option in that suggestion isn't an ignore flag, it's terminal: <reason> — silencing the finding costs you a sentence explaining why. Write terminal: nothing reacts yet, refunds are manual for now and the rule goes quiet forever, and the next person to open the file finds the decision instead of the silence.
That's not a small detail. It's the difference between a linter people disable in week two and one they keep, and it has a nice second-order effect: the reasons accumulate into the documentation nobody would have written on purpose.
What keeps the model honest
The obvious objection to all of this is the one I raised against diagrams: a file in the repo rots exactly like a picture in a wiki, because nothing breaks when it's wrong. A graph you can query is still a graph nobody updated.
So make something break. Nodes carry pointers into real source, and verify checks that those pointers still resolve — rename the file and the model says so, instead of quietly describing code that no longer exists. That's the whole difference between this and the wiki diagram, and it's one command in CI.
The version of it I didn't expect to care about reaches across repositories. A command declares the refusals it can answer with; an actor declares where its caller lives. Those two facts sit in different codebases, in different languages, and no linter can see both at once:
report-party: { type: command, rejects: [INVALID_PARTY_DATA, RATE_LIMITED] }verify searches the caller's source for each code. One that appears nowhere is reported — which catches the most boring outage there is: a service gains a rejection, every test on both sides stays green, and the caller treats the new code as unknown. It drops the message, or retries something that will never succeed.
The matching is a substring search across whatever language the caller happens to be written in, which is deliberate. A code mentioned only in a comment counts as handled, and one assembled at runtime isn't found at all. Owning a parser per language to find a constant that is missing entirely isn't worth it, and a false pass is cheaper than a check nobody runs.
And a summary a human can hold
The other half is the one the speed made urgent. When you land in a flow you didn't write — increasingly, one you didn't type either — you don't want the architecture. You want this corner of it:
slice: Payment Refused
actor —
screen —
command Pay Order
event Payment Refused
aggregate Payment
read-model —
policy —
shown on —Ten seconds, and you know both what the flow is and where it stops. The dashes are the finding. The same model answers "what does this aggregate's life look like from first event to last" and "if I change this command, what's downstream of it" — a blast radius you'd otherwise estimate by grepping and hoping.
Same data structure, different projections. The human gets a summary small enough to hold in their head. The agent gets a gap precise enough to close. Neither is reading the whole thing, because nobody can, and pretending otherwise is how we got here.
The artifact that survives the speed
I built this because I needed it, and it's a real tool now rather than a sketch: eventgraph — a landing page with a live viewer and the full rule catalogue, MIT-licensed, npx eventgraph-cli init to start. There's a CLI, an MCP server so agents can query the model directly, and a scaffolder that reads routes and table declarations out of an existing codebase so transcribing an app by hand isn't the price of entry. It deliberately refuses to guess commands, events and policies — a graph that invented them would look finished while being wrong, which is worse than one that's obviously partial.
But the tool is downstream of the point, and the point is this. We spent years making the code faster to produce and left the understanding of it in the one place that doesn't scale: people's heads, refreshed by reading diffs. That worked while the code moved at typing speed. It doesn't now.
The way out isn't reading faster. It's keeping a model of the system in the repo, in a form a human can get a summary out of and a machine can find holes in — where "the map is out of date" stops being weather and becomes a finding with a node name attached.