skip to content

Domain Logic Patterns

Three ways to hold business logic: Transaction Script for straightforward procedures, Domain Model for rich behavior, Table Module in between. You will learn how complexity drives the choice, and where the anemic-domain-model criticism actually applies.

part ofSoftware design & architectureoverview, primer and where to startread it →
on this pageshow

questions

5

What is the Transaction Script pattern for organizing business logic, and what does a single script typically do?

level: juniorimportance: must knowfreq 55%

answer

  1. one procedure per use case
  2. no shared object model
  3. Gateway/DAO for DB access
  4. duplication + long-method risk
  5. fine for simple/CRUD-shaped logic

basics

~20 s

A Transaction Script is one procedure that handles a single business request end to end: it reads input, runs the rules step by step, talks to the database, and returns a result. No shared object model, just a top-to-bottom recipe per action.

solid answer

~30 s

Transaction Script organizes domain logic as one procedure per business transaction (e.g. placeOrder, transferFunds). Each script directly orchestrates validation, calculation, and persistence for that use case, usually calling the database through a Gateway/DAO. There's no shared object model, so logic isn't reused via polymorphism, though duplication can be reduced with shared subroutines. It's simple to build, test, and reason about for straightforward CRUD-heavy or calculation-heavy work, but tends to duplicate logic and grow into long, tangled methods as business rules multiply and start interacting with each other.

go deeper

for a junior

Should describe Transaction Script as one procedure per business action, be able to say it doesn't use a shared object model, and note that database access happens inline, typically via a gateway or DAO.

for a middle

Should additionally explain how to reduce duplication with shared subroutines and identify the signal that scripts are becoming unwieldy long methods.

for a senior

Should discuss real trade-offs — testing implications, when duplication becomes a genuine maintenance liability — and place Transaction Script within a broader system that may use other patterns elsewhere.

for a principal

Should defend Transaction Script as a legitimate long-term architectural choice for genuinely simple domains, articulate the cost of abandoning it prematurely, and connect the decision to team velocity and system-wide consistency.

## What a script actually does **Transaction Script** is the simplest of the three classic domain-logic patterns (alongside **Table Module** and **Domain Model**). Mechanically, you write one procedure per business transaction — a use case like `placeOrder`, `calculateShipping`, or `transferFunds` — and that single method does everything: 1. it validates the incoming request; 2. pulls whatever rows it needs from the database (often via a thin Gateway or DAO that just runs SQL); 3. applies the business rules inline as ordinary conditionals and calculations; 4. and writes the results back to the database, usually inside one transaction boundary matching the method's name. There is no object graph representing the domain; data mostly moves through as primitives, DTOs, or raw database rows. Multiple scripts are commonly grouped into a Service class per subject area (e.g. `OrderService`, `PaymentService`) purely for organizational convenience — that grouping does not imply a shared behavioral model, since each method inside is still self-contained. ## Why the pattern exists The pattern exists because a large fraction of real business logic is genuinely simple: - a handful of validations, - a calculation or two, - a database read and write. Building a full object model with associations, identity, and polymorphism for that kind of logic adds real cost — mapping layers, more classes and interfaces, more indirection to read through — for no behavioral payoff. Transaction Script maps directly onto how most engineers already think procedurally, has almost no learning curve, and maps almost one-to-one onto database operations, which makes it fast to write and fast for a new team member to trace end to end. ## The trade-off — duplication versus simplicity The trade-off is duplication versus simplicity. **On the plus side:** - each script is easy to understand in isolation, - there is minimal object-relational mapping overhead since you're usually working close to raw rows or DTOs, - and performance is often good because there's little indirection between the request and the database call. **On the minus side**, similar business steps get repeated across scripts unless someone deliberately factors them into shared subroutines, and even with subroutines the discipline to keep reusing them (instead of re-implementing 'close enough' logic) tends to erode over time. Testing at the level of a whole script often forces either a real database or heavy mocking of the gateway, since business logic and persistence aren't cleanly separated. As the number of business rules grows and the rules start interacting — pricing that depends on customer tier, order type, and an active promotion, for instance — scripts accumulate deeply nested conditionals and become 'long methods' that are expensive and risky to change. ## The failure mode in production In production, the classic failure mode is **duplication drift**: a new business rule (say, a new discount type or a new tax jurisdiction) has to be added to every script that touches pricing, and inevitably one or two scripts get missed or implemented slightly differently, producing inconsistent behavior — a customer gets charged the old tax rate through the batch-invoice script while the checkout script applies the new one correctly. A related symptom is business logic leaking sideways into UI controllers or database stored procedures because there's no natural single home for a piece of shared computation, so the same rule ends up implemented in two or three places with subtly different edge-case handling, which shows up as production bugs that are hard to trace because 'the logic' isn't in one place. Over time, complexity concentrates in a handful of scripts that become the ones nobody wants to touch: - high defect rate, - slow code review, - and a de facto bus-factor risk since only one or two engineers fully understand them. ## Where it shows up in practice In practice, Transaction Script is how a great deal of real transaction-processing software is actually built: - simple CRUD SaaS features, - batch billing/invoicing jobs (a `processMonthlyInvoice` procedure), - request handlers in early-stage APIs, - and simpler ledger/funds-transfer processing. Many 'fat controller, thin model' Rails or Django codebases are effectively Transaction Script wrapped in an MVC skin even though they look object-oriented from the outside, because the controller action (or a thin service it calls) is doing all the orchestration procedurally rather than delegating to behavior-rich domain objects. Recognizing that is useful: the pattern isn't a mistake in those codebases, it's an appropriate match to genuinely simple logic — the mistake would be sticking with it once the rules interact enough that duplication and long-method risk start dominating, which is the signal to consider Domain Model instead.

  • How do you reduce duplication between Transaction Scripts without moving to a full Domain Model?
    Extract the shared step (a calculation, a validation rule) into a reusable subroutine or a small stateless helper function that every script calls, rather than reimplementing it inline in each one. This keeps the procedural style but gives the rule a single source of truth, which is usually enough until the rules start genuinely interacting with each other.
  • How do you unit test a Transaction Script that talks directly to the database?
    Introduce a thin Gateway or Repository interface behind the direct database calls and substitute a fake or in-memory implementation in tests, so the script's logic can be exercised without a real database. This doesn't require adopting a domain model, just isolating the I/O boundary.
  • When does duplication across scripts become a real cost rather than premature optimization?
    When the same rule has to change for a business reason and someone can point to more than one place it lives — that's the moment drift becomes likely. Before that, duplicating a two-line check across scripts is often cheaper than the abstraction it would take to share it.

Like a fast-food order line: one worker follows a fixed checklist front to back for each order, rather than consulting a shared recipe book of ingredient objects that know how to combine themselves.

saying these in an interview costs you the question

  • claims procedural organization is inherently bad regardless of context
  • doesn't mention the duplication/long-method risk as scripts multiply
  • thinks scripts can't call shared subroutines to reduce repetition
  • conflates the pattern with stored procedures only
  • says it never fits real production systems

context

open as a page

How does the Domain Model pattern differ from Transaction Script in organizing business logic, and what should actually drive the choice between them?

level: middleimportance: must knowfreq 65%

basics

~20 s

Domain Model puts business rules inside objects that represent real concepts, like an Order that knows how to calculate its own total, while Transaction Script keeps rules in separate step-by-step procedures. Pick Domain Model when rules are complex and interact a lot; pick Transaction Script when they're simple.

open as a page

What is an Anemic Domain Model, why did Martin Fowler call it an anti-pattern, and what does the trade-off actually look like in practice?

level: seniorimportance: must knowfreq 60%

basics

~20 s

An anemic domain model is when your 'domain' objects are just data bags with getters and setters and no real logic, while all the actual business rules live in separate service classes. It looks object-oriented but behaves like plain procedural code.

open as a page

What is the Table Module pattern for organizing domain logic, and in what situations does it fit better than Transaction Script or Domain Model?

level: middleimportance: should knowfreq 30%

basics

~20 s

Table Module has one class per database table, not per row, and it holds the business logic for that table. You pass it a set of rows, like a whole table's worth of data, instead of creating a separate object for each individual record.

open as a page

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%

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.

open as a page