skip to content

As a system's business logic grows more complex over time, how would you decide whether — and how — to migrate domain logic from a Transaction Script style toward a Domain Model, and what does that migration actually cost?

level: principalimportance: should knowfreq 40%

answer

  1. signals: duplication, drift, growing type-branching
  2. type-checking switch statements ask for polymorphism
  3. migrate incrementally: characterization tests + Strangler-style extraction
  4. coexistence of patterns per subdomain is normal, not a compromise
  5. cost = ORM/mapping + learning curve + boundary-drawing risk

basics

~20 s

Watch for pain signals — like duplicated rules and tangled functions — before deciding to switch styles, then move gradually, pulling shared rules into small rich objects piece by piece, instead of doing a risky big rewrite of everything at once.

solid answer

~40 s

The decision should be driven by concrete complexity signals, not fashion: business rules drifting out of sync across scripts, long conditional-heavy methods, bugs from a rule applied inconsistently, or a growing need for polymorphic variation across multiple types. Given those signals, migrate incrementally: identify the specific rule cluster causing the pain, extract it into a small domain object used by the existing scripts, verify behavior with characterization tests, and repeat — rather than attempting a big-bang rewrite. The real cost is the object-relational mapping investment and the team's learning curve, plus the risk of over-applying the pattern to logic that never needed it; a good architect keeps both styles coexisting permanently, using Domain Model only where the complexity genuinely earns it.

go deeper

for a junior

Should understand at a basic level that logic can move from one style to another over time as complexity grows, without needing to know migration mechanics.

for a middle

Should name at least one concrete signal — duplication or a growing type-based conditional — that indicates it's time to consider extracting a domain object.

for a senior

Should describe an incremental migration approach involving characterization tests and Strangler-style extraction, and articulate the risks of a big-bang rewrite.

for a principal

Should set migration strategy, sequencing, and risk controls across a whole system, decide which subdomains deserve investment, and defend a coexistence architecture rather than forcing uniformity.

## The signals that justify a migration The decision to migrate should rest on concrete, observable signals rather than architectural fashion. **Four signals matter in practice:** 1. repeated duplication of the same business rule across more than one script, with any observed divergence between the copies being a strong red flag; 2. growing method length and cyclomatic complexity in the scripts touching a given area; 3. a rising rate of incidents where a rule was changed in one place but not another that also needed it; 4. and, most tellingly, a conditional chain branching on a type or category field that keeps growing as new variants are added — the classic case being an if/else or switch on a 'payment type' or 'account type' field, duplicated in more than one script. That last signal is the textbook invitation to **replace conditional logic with polymorphism**, a piece of general object-oriented wisdom that applies directly here. ## Migrating incrementally instead of by rewrite Once those signals appear, the mechanics of migration matter as much as the decision itself. A big-bang rewrite of the whole logic layer, run in parallel with the live system, is a well-known failure pattern: it takes months, the two systems inevitably drift on business rules during that window because nobody can keep every rule change synchronized across both, and the project risks being abandoned or shipped with regressions because the old tangled logic was never pinned down before the rewrite began. The safer approach borrows the **Strangler Fig** idea and applies it at the domain-logic level rather than the service level: 1. pick the single rule cluster causing the most pain; 2. write characterization tests around its current behavior first — tests that pin down what the system actually does today, quirks and all, rather than what it 'should' do — so the extraction step can be verified as behavior-preserving; 3. then extract a small object owning that rule; 4. have the existing Transaction Scripts delegate to it instead of implementing the logic inline; 5. verify against the characterization tests, and move on to the next cluster. Over time this produces a partial, targeted Domain Model around the subdomains that genuinely earned the investment, while simple CRUD areas remain untouched Transaction Scripts — an architecture Fowler explicitly endorses: it is entirely possible, and often correct, to use Transaction Script for some parts of a system's logic and Domain Model for others. ## Why this is really a calibration problem Why this matters is really a calibration problem. Waiting until scripts are completely unmanageable before reacting, and over-engineering greenfield code with a rich model nobody needs yet, are both costly failure modes at opposite ends of the same mistake: investing out of proportion to actual, observed complexity rather than fashion or fear. This mirrors evolutionary-architecture and YAGNI thinking more broadly — decisions are best made at the last responsible moment with real evidence in hand, and reversibility, via characterization tests plus incremental extraction, matters more than getting a hypothetical 'final' design exactly right on day one. ## What the migration costs The migration itself has real, named costs on both sides. **The benefit:** future rule changes localize to one object instead of touching N scripts, which reduces the defect rate caused by drift and enables genuine reuse and polymorphism going forward. **The cost:** - introducing mapping complexity — an ORM or repository layer for the newly extracted objects, since previously the scripts talked to the database directly through a simple Gateway; - plus a real learning-curve tax for engineers unfamiliar with rich OO domain modeling, including how to draw aggregate boundaries and enforce invariants without accidentally recreating an anemic model during the extraction; - there's also a short-term velocity hit while characterization tests and extraction happen alongside ongoing feature work; - and a genuine risk of doing the migration badly: extracting objects that still don't own their invariants, or drawing an aggregate boundary that's too large (a new god-object) or too small (chatty, inefficient cross-object calls). Because of this, the migration itself benefits from domain expertise — techniques like event-storming or close collaboration with domain experts to establish a shared vocabulary help find the right boundaries rather than guessing. ## A concrete scenario A concrete scenario makes this tangible: a payments platform starts with a simple `processTransaction()` Transaction Script handling card payments. Over several years, different teams bolt on branches for ACH transfers, wire transfers, multi-currency FX conversion, fraud-hold rules, and partial refunds, all inside the same script or its close relatives. A senior or staff engineer recognizing the drift-and-god-method signals would carve out a `PaymentMethod` hierarchy — `CardPayment`, `AchPayment`, `WireTransfer` subclasses, each owning its own validation and settlement behavior — behind characterization tests, migrating one payment type at a time, while deliberately leaving genuinely simple reporting queries as untouched Transaction Scripts. That's the coexistence architecture Fowler describes in practice, not a wholesale rewrite chasing an ideal 'correct' design.

  • What's a concrete code smell that signals 'extract this into a domain object now'?
    A growing switch or if-else chain branching on a type field across multiple Transaction Scripts, especially when the same type field is switched on in more than one script, is the classic invitation to replace conditional logic with subclass polymorphism rather than keep expanding the branch.
  • Why use characterization tests specifically, rather than jumping straight to writing tests for 'correct' behavior, during this kind of migration?
    Legacy Transaction Script logic often has undocumented quirks and edge cases that real users depend on. Characterization tests pin down what the system actually does today first, so extraction can be verified as behavior-preserving, and any intentional behavior change can then be made explicitly and separately, rather than sliding in accidentally during the refactor.
  • Should the whole application eventually converge on one Domain Model?
    No — Fowler explicitly endorses mixing patterns per subdomain based on actual observed complexity. Forcing every corner of the application into a rich model purely for stylistic consistency reintroduces the exact over-engineering cost the incremental, evidence-driven migration approach was meant to avoid.

Like renovating a house room by room while people still live in it — you don't demolish the whole house to fix a leaky bathroom; you isolate the room, verify the fix, move to the next, and some rooms, the simple ones, may never need renovation at all.

saying these in an interview costs you the question

  • recommends a full parallel rewrite as the default migration approach
  • can't name a concrete signal that should trigger a migration decision
  • doesn't mention tests or behavior-preservation during extraction
  • assumes the whole application must converge on one uniform style
  • ignores the ORM/mapping cost the migration itself introduces

context