The spiral demonstrated the methodology on a clean forward pass, and Brownfield runs it in reverse. Neither is the test a skeptic actually wants. The test is an adversary handing over the scenario engineered to break the model: the feature that seems to span two boundaries at once, the rule that seems to force a rewrite, the data that seems to drift no matter how it is stored. This chapter collects those scenarios — every one drawn from a real argument, most from a single sustained one — and runs the methodology on each in the open, including the places where the honest answer is here is the edge, and here is what you do at it rather than the model absorbs this for free.
The cases share one domain on purpose: the same ticketing platform the spiral carried, now pushed past where a designer would normally stop. Carrying one domain into its hard corners is the same discipline as carrying it up the altitudes — a fresh toy per objection would prove nothing.
The reframe: altitudes, not modules
Every one of these challenges arrives in the same disguise. It names a new feature and asks which module does this go in? — and the question has no answer, because there is no predrawn map of modules to file a feature into. There are telescope altitudes (use case, workflow, subsystem, system), and a unit appears at the altitude where one change driver’s cohesion closes: completeness and purity, nothing scattered out, nothing foreign in. So the question is never does this fit workflow W? It is what change forces this, and only this, to move — and what moves with it? Altitude is discovered from the driver, not assigned from a diagram.
That single move dissolves most of the disguises before any detail. A feature that seems to straddle two boundaries usually serves two drivers, and the right answer is that it is two units, one at each altitude — not one unit in an agonized location. The cases below are that move, performed over and over on harder and harder inputs.
The escalation
A reader escalates the booking example one feature at a time, asking at each step: same workflow? whose is it? The escalation feels like it is breaking the methodology. It is the spiral discovering altitude.
Buy N adjacent seats. The challenge: buying a block of adjacent seats instead of one. Same workflow. The reservation policy — hold, timeout, confirm, pay — is the change driver that grouped buy ticket into its workflow, and buying several seats under that same policy joins the cohesion rather than forming a new one. The only genuinely new thing is data, and it appears exactly where an invariant summons it: if “all N or none” is a real rule, a group-atomicity owner appears at that invariant and nowhere earlier — no graph of Seat and Block drawn in advance, the owner born the moment the all-or-nothing rule is. A new use case under an existing driver joins the existing unit; data grows at the invariant, not before it.
Premium members auto-buy on availability. The challenge: a premium account automatically buys a seat the instant one frees up. Ticketing or premium? It splits; it was never one unit. The buy mechanics stay in booking’s reservation workflow. The auto-buy decision answers to a different driver — premium rules, the existence of premium accounts — and purity forbids it inside booking. So auto-buy is a premium use case that composes booking’s buy capability across the subsystem boundary, triggered by a seat-freed event booking publishes. The question “ticketing or premium?” dissolves, because the answer is “one of each.” The honest edge: if premium buyers must outrank ordinary ones, either booking exposes a neutral priority primitive — premium supplies the value, booking stays pure — or booking’s workflow consciously takes a second driver. The latter is permitted; multi-driver cohesion is real. But it must be named, not stumbled into.
Conference, parallel tracks, change seat mid-event. The challenge: one ticket now covers a multi-track conference, and an attendee changes which track they are sitting in partway through. Change-track is the change seat operation of Spiral 3 — acquire the new, release the old, settle the difference — a Sequencer composing whole use cases under the in-event seat-management driver. The data stays thin: no Conference/Track/Seat object graph up front; change-track owns one field, the current assignment; the timetable is reference data with a seeding operation, an admin import. The honest edge: same-slot exclusivity and track capacity are genuine cross-field invariants. This is the book’s honest limit, the place a small record earns its keep. Name the invariant; do not pretend it away.
Do the three features touch each other’s code or data? The challenge: three features layered on one platform — surely they tangle. They meet at exactly one point, by design. In code: three use cases under three drivers, with nothing shared to collide in. In data: they meet at one field, the seat status enum — free, held, confirmed, cancelled — which is a state machine, and every feature writes it only as a transition. All transitions on a seat serialize at the seat, because the seat is the consistency boundary. Every other field has a single owner. The real interactions surface only at that one field: auto-buy versus group-booking resolves at the held → confirmed guarded write; change-track releasing a seat emits seat-freed, which may trigger an auto-buy — the one legitimate cross-subsystem seam, an event, not a shared call.
The payoff is the contrast. An aggregate hides this N-way interaction inside one mutable Booking object; the telescope funnels it to one visible, named coordination point and routes everything else as single-owner fields and boundary events. None of the four cases breaks the model. Two of them mark honest edges, where you make a deliberate, named call — a multi-driver unit, an earned record — with vocabulary for it, instead of discovering a god-object after the fact.
The conference ticket, and the residue it leaves
Case three left a record that earns its place: the conference ticket. It is worth following, because the moment a record earns its place is the moment a skeptic expects the data to start drifting.
Make the ticket concrete. It is a schedule — a set of (track, time-slot) session reservations, 12:00–13:00 in A, 13:30–14:00 in B, 15:00–16:30 back in A — under one spanning rule: no two of the holder’s reservations overlap in time. That rule is exactly what makes the ticket an earned record: it owns the set of session-reservations and the no-overlap invariant. The honest edge of case three, now concrete.
Cancellation, and the residue. The challenge: cancel the ticket. Its status goes free — but the session rows still point at the old holder. Stale data, the exact drift the skeptic predicted. Two moves answer it. First, availability is derived, not stored: a slot is free if and only if no active reservation overlaps it. There is no is_free boolean to drift; cancel, and the next read re-derives the slot as free. Second, cancel is a transition the ticket owns: one guarded atomic statement flips the ticket to cancelled and releases every session-reservation it holds, together, because the invariant — a ticket is cancelled exactly when its sessions are released — belongs to the cancel operation. Status and periods cannot diverge, because one owner writes both in one move. The cancelled rows that remain are owned residue — audit, refund — not orphans; their state is cancelled, not “still held.” Store each reservation’s own lifecycle, derive availability from it, and never store “free” as a second source of truth. The drift the skeptic predicted requires precisely what the methodology refuses: a denormalized flag updated out of band. A performance counter is allowed, decremented inside the same guarded statement; it is un-owned denormalization that is the bug.
The time-shared seat. Sharpen it again. An attendee holds one home seat on one track for the whole event, but spends chosen windows on another track, and during those windows the home seat is freed for others, reclaimed afterward. Time now lives inside a single seat. Finer residue, the same machinery. The stored fact is a presence interval — (user, track, seat, [from, to]) — not a seat with a status and a pile of exception rows. A schedule is the set of a user’s intervals; the home booking is one interval spanning the event. Moving to another track for a window is one guarded atomic transition the ticket owns: trim the home interval around the window, add an interval on the other track. The home seat “freeing” is the trim — the absence of a covering interval is the seat being free for that window, derived, never stored. Reclaiming is adding a home interval back, guarded against whoever took the freed window. “Free the home seat for others, or keep it held” is a named business policy, one line in the move transition, surfaced rather than buried. And the spanning rule — no two presence intervals on a seat overlap in time — is a Postgres range-exclusion constraint: design-out as a declarative schema rule, the conflict made unconstructible. The residue, derived-availability, and guarded-transition trio, pushed to a time-sliced single resource, still holds with no new primitive.
The central objection: change-locality versus the aggregate
This is the objection the chapter exists to answer, and the sharpest thing the debate produced. The challenge: change the representation. A seat is no longer one seat but one seat | a list of (seat, interval). In entity-first design that change lands inside one entity — the public interface holds still, only the private save-and-return changes, and every caller picks up the new behavior for free. Process-first’s separate use cases seem to force rewriting cancel, return, premium, adjacent-seats: rewrite half the system; the schema has leaked into the code.
The premise is false, and falsifying it is the whole reply. Process-first encapsulates the shared primitive in a value object too — call it Reservation: the union is internal, the interface (release(), overlaps(), total(), freeWindows()) is stable. The representation change lands there, in one value type, exactly as the entity-first story claims for its entity. Cancel rides along through release(); the hold-timer rides along on the held → expired state machine, which is agnostic to how capacity is represented. What actually changes is only genuinely-new behavior — schedule booking with new input, windowed reads, slot-level premium triggers — and that is new in entity-first too. The representation change is a tie: both disciplines insulate their callers behind a stable interface. The “rewrite half the system” charge needs a fat aggregate that never had the value object; against thin-entity design, which keeps representation in a value object and policy in services, the charge evaporates equally for both — because thin-entity design is the process-first split, named differently.
The tie is the concession that sets up the real point. Representation churn is rare; policy churn is constant. Every refund rule, every premium-eligibility change, every adjustment to hold duration is a policy change — and a policy change isolates to one use case in process-first while editing the shared entity in entity-first. The aggregate’s “one place to change” is a dilemma, not a virtue: put the union inside the entity and old bookings are forced through new union code, so a conference bug can break concert sales — risk isolation lost; split into separate entities and you lose the auto-pickup the objection was built on. Process-first adds a use case and leaves the old paths untouched. None of this removes the earned record: an invariant-dense domain still earns one, and the conference ticket earns a small one — the no-overlap rule — with policy kept outside it. The win is not that representation change is cheaper. It is that the axis which actually churns, policy, is where the locality lives.
Variation without branching
The sharpest form of the “process-first explodes” objection is dimensional. Give the booking example three independent axes at once: how seats are selected (one, several, several adjacent), how they are numbered (assigned seats, or a standing section with none), and how tracks work (a single hall, or parallel tracks an attendee moves between). They combine freely — auto-buy five adjacent seats that must be free across different tracks in different windows; change a seat between tracks in a standing hall. The objection: process-first must either write a use case per combination (a combinatorial explosion), or one use case that branches on every axis (a diamond-riddled procedure), or a dispatch layer that just reinvents the entity hierarchy it claims to have dropped.
The objection rests on one buried premise: that process-first cannot dispatch — that a use case is a concrete procedure, so varying its behavior means multiplying it or branching it. Object-oriented design answers dimensional variation with polymorphism: the use case calls a Strategy interface, the vtable selects the concrete strategy, and the body stays linear because the choice is hidden in the dispatch. That instinct is correct, and the mechanism is real. Process-first has the same mechanism; it spells it as data rather than inheritance.
A varying how is a bound policy, not a branch. Seat selection is a function value chosen at the boundary and passed in — pickOne, pickN, pickAdjacent(n). The buy use case calls select(request) and never learns which was bound; adding “adjacent” adds a function, not a use case and not an if. Numbering is a policy on the Section value object: a numbered section derives availability per seat, a standing one by capacity, and the use case calls section.hold(request) uniformly, the section’s type deciding how. Track structure is the presence-interval residue of the time-shared-seat case, change-track its guarded trim-and-add. The use-case body is a straight Sequencer — validate, select, hold, pay, confirm — with no decision diamond in it, for the exact reason the object-oriented version has none: the variation was resolved into a bound value before the body ran, not branched inside it.
This is the policy axis of the four-way split, pointed at variation. The aggregate fuses representation and policy, so a new variant edits the shared object; process-first keeps policy its own value, so a new variant is bound where it is needed and nowhere else. The “two-level structure” the objection concedes would be needed is neither absent nor a smuggled aggregate — it is the use case plus the policies it composes, already in the book twice: as the four-way split, and as absorption, the parent that owns a spanning invariant when two axes genuinely couple while the parts keep their own logic. Nothing fuses data with behavior; nothing multiplies; nothing branches.
Map the toolbox and nothing is lost: a Strategy interface with its subclasses is a function-typed step or a policy method on a value object; polymorphic dispatch is a sum-type match resolved once at the boundary; a Template Method is a Sequencer with steps passed in. Where object-oriented design reaches for Strategy, Template Method, or polymorphism to keep a procedure linear across a dimension, process-first binds a policy value to the same end. The dispatch is the same dispatch; only the spelling differs, data for inheritance.
The honest limit is narrow. Dispatch is not free in either discipline — the policies are real code, and a strategy-dense domain carries a real table of them whichever way it is built. Process-first does not abolish that table; it keeps it out of the use-case body and off any shared object, so a new strategy lands in one bound value instead of a subclass every caller inherits. And where two axes are coupled by an invariant that spans them, the coupling earns an owner by absorption, at the one place the invariant lives — a cost named and located, not a branch smeared through every use case.
Objections answered
The edge cases are technical. The objections that recur in argument are not; they are about what kind of thing the methodology is. Three are worth answering directly, because each rests on a misreading the worked cases above already refute.
The inner saga. A frequent move is to ask for a Rosetta table mapping each domain-driven-design construct to its process-first equivalent. The book declines, because there is no clean bijection — an aggregate is not an earned record, the aggregate fuses policy the record keeps out — and a table would launder that difference into false equivalence. The useful artifact is a diagnostic instead. When a saga appears inside a single service or bounded context — orchestration and compensation among objects that were supposed to be one consistency boundary — that inner saga is the tell that the aggregates were cut by noun rather than by transaction: objects grouped because they are the same thing instead of because they must commit together. Process-first reads the inner saga as a boundary in the wrong place and re-cuts by the transaction, which is to say by the change driver.
“Process-first is bottom-up, so it has no vision.” The allegory offered is a pyramid: grand design at the top, stones at the bottom, process-first accused of starting from the stones with no view of the whole. The accusation imports a direction the methodology never declared. First, process-first fixes no direction — fixing one would be self-limiting; you may start from the vision and descend, from a use case, or work both ends toward the middle. Second, the puzzle picture requires the helicopter view: seeing at every moment what is placed, what is sorted, and what is still in the box is only possible from above, so the global view is the method’s precondition and one of its deliverables — the change-driver catalogue — not something reached only at the end. Third, vision is not method: a pyramid’s vision says nothing about how to cut a stone, and a pyramid is built bottom-up from its base. Vision on top, construction from below — that is process-first’s exact shape, and mistaking “draw the capstone” for “start building” is the error, not the correction. Fourth, top-down-from-vision has its own failure mode: the vision underdetermines the structure, so descending straight into a model is Big Design Up Front, a vision-derived model the real processes will not fit. Fifth, the synergy a grand model is supposed to add is not lost; it is change-driver cohesion, and the grand strategic vision is the enterprise altitude, which the methodology takes honestly as an input rather than faking as a model it does not own.
“Domain-driven design already handles the enterprise level.” The strategic half of domain-driven design — the socio-technical framing, value streams, context boundaries drawn to organizational reality — is real work the methodology does not duplicate. It is also a different layer. Process-first is a code-structure method at the tactical layer; it consumes the strategic frame rather than competing with it, taking the boundaries the enterprise already decreed and deriving the same cuts from how the system actually changes. The honest position is the one the debate itself reached: the strategic level handles what is above the system, the tactical level handles the system’s structure, and the interlocutor who began by excluding every concrete tactical item from “the enterprise level” ended by conceding that level has limits of its own. The two are not rivals at one altitude. They are two altitudes, and naming which one you are working at is the first move of working at it.
None of the cases needed a primitive the spiral had not already introduced. The seat status state machine, the owned transition, derived availability, the value object, the earned record, the named edge — the same small vocabulary absorbed every challenge, and where it reached a real limit it named the limit and handed back a deliberate choice with words for it. That is the test a methodology is supposed to pass: not that nothing is hard, but that the hard things land in the open, at one coordination point, with a name.
This chapter advances the cohesion and honest-scope threads: the cohesion test run against adversarial inputs, and the methodology’s edges named rather than hidden.