Auditing a Scala Service Against Chad Fowler's Four Regenerative Constraints
I walked a Scala order-processing service from my notes through Chad Fowler's four regenerative constraints. Two passed for free, two would force a real redesign. Here is what I learned about where "loosely coupled module" ends and "regenerative component" begins, and which parts of the redesign I would actually pay for.
In a fragments post on March 16, 2026, Martin Fowler highlighted Chad Fowler's "Compile to Architecture" essay, which argues that when code is cheap to generate, the constraint shifts from writing software to safely replacing it. Replaceability turns out to require four specific architectural properties. I read the piece, then opened the notes I keep on a Scala order-processing service I have been studying and walked it through each constraint. The result was uneven: two of the four held up without effort; the other two would force a redesign I am not sure I would actually pay for.
This post is a record of that audit, and what it taught me about where "loosely coupled module" ends and "regenerative component" begins.
The four constraints, in plain terms
Chad Fowler's essay proposes that an architecture earns the word "regenerative" when it imposes four rules on its components.
Communication patterns are few and uniform. A system might use RPC, events, actor messages, command buses, or a mutation log. Pick a small set. Replacing a component then does not require rediscovering which transport it uses or what state assumptions live inside the call.
Each dataset has exactly one writer. Chad Fowler's phrase is "exclusive mutation authority for each dataset to a single component." If two components mutate the same table or stream, neither can be replaced safely — the invisible coupling lives in the writes, not the reads.
Evaluation surfaces are independent of implementation. Contract tests, domain invariants, event consistency rules, property tests at the boundary. Behavior must be checkable without booting the whole system. Without that surface, replacement turns into guesswork.
Components are sized at their natural grain. Too large, regeneration gets risky. Too small, coordination overhead dominates. The grain comes from the intersection of the earlier two constraints — a unit that owns mutation for some data and can be verified independently is roughly the right size.
These look like restatements of familiar modularity advice. They are not. The bar is sharper. "Loose coupling" tolerates two components reading the same table; exclusive mutation authority does not. "High cohesion" tolerates a domain module that handles its own background jobs; the natural-grain rule asks whether those jobs share a verifiable surface with the synchronous path.
The service I audited
The service is a Scala 2.13 backend running on Akka 2.6 and Postgres, with Kafka as the asynchronous bus. It accepts orders over HTTP, runs each order through pricing and validation, reserves inventory, charges through an external payments API, and emits an event stream. Tests sit in three layers — unit tests for pure domain logic, integration tests for the data layer, contract tests for the HTTP and event surfaces.
Before getting into what passed and what failed, the diagram below maps each component to the constraint it satisfies or violates — green seams hold up under audit, red seams do not.
I will not walk through every module. The interesting parts are where the audit broke.
What passed for free
The communication-pattern check held up. The service has exactly three modes: HTTP requests at the edge, Kafka events between bounded contexts, and Akka messages for short-lived per-order coordination. There is no fourth path — no scheduled scripts hitting other services' endpoints, no shared filesystem, no service-to-service direct database reads. When I traced a single order from HTTP request to settlement, every hop landed in one of those three categories. That predictability is what makes the first constraint pass: a replacement implementation only has to honor three contracts, not three plus a long tail of "and also."
Evaluation surfaces also passed, with one caveat. The HTTP and Kafka boundaries each have contract tests that run in milliseconds without spinning up the rest of the service. The domain layer has property-based tests for invariants like "an order's total equals the sum of line items minus discounts." I can verify a rewritten pricing engine without touching Postgres. The caveat: the Akka coordination layer has no independent surface. Its behavior is exercised through end-to-end paths and inspected with logs. Replacing it would mean replaying production traffic and squinting at logs, which is not regenerative — it is debugging.
What didn't
Exclusive mutation authority is where the audit got uncomfortable. On paper, each module owns its tables. In practice, three places write to the order_status column: the order module's HTTP handler, the payments module after a refund, and a nightly reconciliation job that lives in the same artifact but runs from a different entry point. None of these writes go through a shared function. Each one has its own SQL.
This is the exact failure mode Chad Fowler warns about — both the order module and the payments module work in production today, but neither can be replaced without auditing the other's writes and the reconciliation job's behavior. The fix is not subtle. Either every status mutation gets routed through a single API on the order module — which means moving the refund code path and the reconciliation logic into it — or status becomes a smaller component that everyone calls. Both options touch a lot of code.
Natural grain failed for a related reason. The order module is roughly 12,000 lines of Scala wrapping pricing, validation, status transitions, idempotency keys, the reconciliation job, and a metrics exporter. Each of those has a different rate of change and a different blast radius. When the reconciliation logic broke last quarter, the fix touched files that had nothing to do with reconciliation, because the module's tests boot the whole thing. Independent evaluation is impossible by construction. Splitting the module along data-ownership lines — orders, status, reconciliation — would give three smaller components, each with its own contract tests. It would also force a commitment to inter-component contracts that today are implicit method calls in the same JVM.
When the redesign is worth it
Chad Fowler's framing is explicitly about regeneration by tools — a future where a coding agent rewrites a component overnight. That bar is high enough that any redesign satisfying it also pays off for human maintainers, but the cost-benefit changes. Walking the redesign on paper, the savings split into two buckets.
Worth doing: the exclusive-mutation fix. Even with no agent in sight, three writers to one column is the kind of debt that produces "fix in one place, break in two" incidents. Thoughtworks Tech Radar Vol 34 names the same dynamic as codebase cognitive debt — a reinforcing loop where small changes start triggering unexpected failures. Centralizing status mutation behind a single API removes a class of bugs and unblocks any future replacement.
Worth questioning: the full grain split. Breaking the order module into three regenerable components costs new contracts, new wire formats, new build targets, and a permanent increase in cross-module call complexity. For a system that humans will keep maintaining for three more years, the simpler win is to add architectural fitness functions — small ArchUnit-style assertions that fail the build if a forbidden dependency reappears — rather than to physically split the artifact. Tech Radar's "Architecture drift reduction with LLMs" blip describes the same instinct: enforce the constraint at the seams without paying the full distribution cost.
The two-of-four pass rate is, in retrospect, exactly what I would expect from a service that grew under "low coupling, high cohesion" guidance. Communication uniformity and evaluation surfaces are what disciplined codebases already build. Exclusive mutation authority and right-sized grain are the parts that quietly slip when a deadline lands.
Takeaways
- Audit each boundary against four questions, in order: who writes this dataset, how does this component talk to the rest, how is its behavior verified, and is its size consistent with the answers above.
- Treat shared writes as the highest-priority debt — they block both human and agent replacement.
- Add an architectural fitness function for every constraint that has been declared non-negotiable. ArchUnit, Spring Modulith, or a custom rule set is enough.
- Resist splitting modules just to satisfy the grain rule unless the data-ownership fix forces it. Distribution has its own cost.
Reach for these constraints when the service is large enough that the mental model has stopped being trustworthy, when AI-assisted contributors have started landing changes faster than reviewers can audit, or when a planned rewrite is on the calendar. Skip them when the service is small, when one or two people own it, and when the next replacement is a year away — the modular monolith is still a fine answer at that scale.
For further reading, Chad Fowler's original essay names the constraints; Martin Fowler's fragments post locates them inside the broader supervisory-engineering shift; Thoughtworks Tech Radar Vol 34 names the gravity those constraints are pulling against.
Still here? You might enjoy this.
Nothing close enough — try a different angle?
Related Posts
Code Graphs for Coding Agents: The Delivery Shape Matters More Than the Algorithm
I spent a weekend pointing a coding agent at a 480k-line Go monorepo and watching it grep-loop through 38 tool calls on one question. AST-derived code graphs fix that, but the delivery shape — local stdio MCP, remote service, or skill — changes the economics more than the graph algorithm does. Here is where I would put one in 2026, with a minimal Go indexer I can drop next to the agent.
Event-Log-as-Source-of-Truth Turns Schema Evolution Into a Forever Problem
When the log is the source of truth, every schema change is permanent. A Kotlin/Avro walkthrough of the rename that passed the Schema Registry check and silently corrupted every old event, plus the Protobuf and Avro invariants I now keep pinned above my desk.
Cell-Based Architecture Isn't Free: What Slack, DoorDash, and Roblox Actually Paid For It
Cell-based architecture contains blast radius, but it is not free. A look at what Slack, DoorDash, and Roblox actually paid for cells in production — and a checklist for the cheaper fault-isolation patterns most teams should reach for first.