Application Architecture

Domain Logic — Transaction Script vs DDD

Domain Logic — Transaction Script vs DDD

About this article

This article is the second deep dive in the “Application Architecture” category of the Architecture Crash Course for the Generative-AI Era series, covering domain logic.

Domain logic carries “business-specific rules, judgments, calculations,” and its design quality determines the app’s long-term competitive edge. The article covers the two big styles (Transaction Script vs Domain Model / DDD), DDD tactical / strategic patterns, the anti-pattern of anemic domain models, and the AI-era value of “promoting business concepts to types.”

What is domain logic in the first place

Domain logic is, in a nutshell, “the business rules, judgments, and calculations unique to your app, expressed in code.”

Imagine a tax accountant’s work. The ledger format (UI) and the safe (DB) are generic — anyone can buy them. But the expert knowledge to judge “is this expense deductible?” or “is the tax rate 8% or 10%?” belongs specifically to the accountant. Software is the same: screen rendering and DB storage are generic mechanisms, but rules like “10% discount on orders over $50” or “anonymize accounts 30 days after cancellation” are unique to that app. How you design this layer determines the app’s long-term quality.

Why domain-logic design matters

What happens if domain-logic design is left vague? When business rules leak into UI or DB, the same rule scatters across multiple places, and changing one without the other creates contradictions — a common accident. When this layer is chaotic, every business change distorts the code.

App value lives in domain logic. The UI layer (screen rendering, input validation) and the infrastructure layer (DB, external APIs) are generic, but the domain layer alone is the app’s unique competitive edge.

The three main styles

Martin Fowler’s organized domain-logic representation methods split into three. Project complexity and team maturity determine which to adopt.

flowchart LR
    Q{Business complexity}
    TS["Transaction Script<br/>Procedural<br/>Business = 1 function"]
    TM["Table Module<br/>(seldom used)<br/>DB table = logic unit"]
    DM["Domain Model (DDD)<br/>Business concepts as objects"]
    Q -->|Low / CRUD-centric| TS
    Q -->|Legacy| TM
    Q -->|High / complex business rules| DM
    TS -.- L1[Fast to start<br/>Grows into anemic model<br/>Service layer bloats]
    DM -.- L2[Promote business concepts to types<br/>High initial cost<br/>Strong long-term]
    classDef q fill:#fef3c7,stroke:#d97706;
    classDef ts fill:#dbeafe,stroke:#2563eb;
    classDef tm fill:#f1f5f9,stroke:#64748b;
    classDef dm fill:#fae8ff,stroke:#a21caf;
    class Q q;
    class TS ts;
    class TM tm;
    class DM dm;
StyleTrait
Transaction ScriptProcedural; one business operation = one function
Table ModuleLogic aggregated per DB table
Domain Model (DDD)Business concepts represented as objects

Table Module is rarely used; the practical choice is mostly “Transaction Script vs Domain Model.”

Transaction Script

Transaction Script writes processing per request procedurally. Common in MVC framework Service layers; simplest and fastest to start.

function registerOrder(req) {
  const user = getUser(req.userId)
  if (!user.isActive) throw new Error()
  const stock = getStock(req.productId)
  if (stock < 1) throw new Error()
  const price = calcPrice(...)
  saveOrder(...)
  sendMail(...)
}
StrengthsWeaknesses
Simple, fast to startLogic scatters everywhere
Low learning costSimilar processing duplicates
Fits small projectsBecomes chaos as business rules grow

For CRUD-centric simple business, Transaction Script is enough. No need for DDD from the start.

Domain Model (DDD)

Domain Model represents business concepts as classes and aggregates logic there. DDD (Domain-Driven Design) is the systematized form of this approach, showing power on complex business domains.

class Order {
  place() {
    if (!this.user.isActive) throw new InactiveUserError()
    if (this.items.isEmpty()) throw new EmptyOrderError()
    this.status = OrderStatus.Placed
    return new OrderPlacedEvent(this.id)
  }
}

Logic concentrates inside the Order class, so “what does it mean to place an order” is understood by reading one place. Tests are easy too, and order can be maintained as business rules grow.

StrengthsWeaknesses
Logic concentrated, easy to testHigh initial design cost
Code follows business changesHigh learning cost (whole team)
Strong on large complex domainsExcessive at small scale

DDD tactical patterns

DDD organizes tactical patterns for building the domain model. Knowing them lets you express complex business logic in organized structure.

PatternRole
EntityIdentified by ID, internal state mutable
Value ObjectIdentified by value, immutable (e.g. Money, Email)
AggregateCluster of entities with consistency boundary
RepositoryWindow for persisting aggregates
Domain ServiceLogic spanning multiple aggregates
Domain EventRepresents a business occurrence

Patterns are “introduced when needed”; no need to use them all from the start.

Value Object benefits

Value Object represents “the domain in meaningful types” rather than primitives. Expressing “money” as a Money class instead of number, “email” as an Email class instead of string, promotes business concepts into the type system.

❌ sendMoney(amount: number, currency: string)
   → Argument-order mistakes go undetected

✅ sendMoney(amount: Money)
   → Money holds amount + currency together

❌ if (email.includes('@'))
   → Validation needed everywhere

✅ Email.parse(str)
   → Invalid values rejected at creation; safe afterwards

Designs that pass primitives around are “Primitive Obsession” — the named anti-pattern.

Money and Email-style Value Objects are valuable regardless of scale. Worth being aware of Primitive Obsession from the start.

Aggregate

Aggregate clusters multiple Entities and Value Objects as “the unit maintaining consistency.” Strong consistency is maintained inside aggregates; aggregate-to-aggregate is eventually consistent — design basics.

[Order Aggregate]
  ├─ Order (aggregate root)
  ├─ OrderItem[]
  └─ ShippingAddress

Updates crossing aggregates forbidden in principle
 → If affecting other aggregates, notify via DomainEvent

Design principles:

  • Keep aggregates small (large aggregates breed contention and lock issues).
  • External references are by ID only (avoid direct object references).
  • Aggregate updates only via aggregate root.

Whether you can draw aggregate boundaries appropriately is DDD’s hardest spot. Failure here breaks everything.

Strategic DDD

DDD has strategic DDD more important than tactical patterns. It focuses on “discovering business boundaries before writing code,” often slighted but where the real value lives.

ConceptSubstance
Ubiquitous LanguageUse the same words in business and development
Bounded ContextThe range where the meaning of words holds
Context MapRelationship diagram across multiple contexts
Event StormingDiscovery method visualizing business with sticky notes

Even with the same word “customer,” sales department might mean “prospect” while accounting means “billing target.” Strategic DDD’s substance: don’t unify them; “treat each context as a different model.”

Anemic Domain Model (anti-pattern)

The failure pattern of mimicking DDD form to produce “classes holding only data + logic all in service layer” is called “anemic domain model.” Looks DDD-ish on the surface; substance is no different from Transaction Script.

❌ class Order {
     id, status, items    // Data only
   }

   class OrderService {
     static place(order) {
       // All logic here
       if (!order.user.isActive) throw ...
       if (order.items.length === 0) throw ...
       order.status = 'placed'
       ...
     }
   }

This loses DDD’s substance: “business logic concentrates in business objects.” It’s just “complex Transaction Script.” It’s also the biggest reason DDD’s learning curve feels steep.

95% of “we adopted DDD is anemic models. Question “where does the logic live” rather than form.

Selection by case

CRUD-centric / simple business

Transaction Script. Forcing DDD just adds classes without raising business value.

Complex logic / business changing frequently

Domain Model (DDD). Insurance, finance, healthcare, e-commerce, logistics — domains rich in business rules pay back the investment.

Startup MVP

Transaction Script -> DDD after growth. Aiming at perfect design from the start exhausts you. Grow gradually as needed.

Improving legacy systems

Phased DDD-ification. Don’t rewrite everything at once; gradually migrate bottleneck business areas to domain models.

Logic-placement principle

Always place business rules in the domain layer. Common across whichever style you adopt. Business rules leaking into UI / infrastructure mean modifying multiple places on changes, breeding contradictions.

❌ Tax calculation in controller
❌ Discount in frontend (also requires recomputation in backend)
❌ Business rules in DB stored procedures

✅ Aggregate business rules in domain-layer Value Objects / Entities

UI-side recomputing the same calculation “for display” is fine, but the rule’s owner is always the domain layer.

Business-complexity × style ladder

Note: industry rates as of April 2026. Periodic refresh required.

DDD from day one” is excess; “perpetual Transaction Script” breaks. Grow gradually matched to business complexity is the realistic answer.

Business complexityRule countRecommended styleTactical patterns to adopt
Simple CRUDup to 10Transaction ScriptNone (plain service layer)
Moderate10-50Transaction Script + Value ObjectValue Object only
Complex50-200Domain Model (DDD-light)Entity / Value Object / Repository
Very complex200+Full DDDEntity / VO / Aggregate / Domain Service / Domain Event

The judgment guideline is “business-rule change frequency.” Domains where rules change weekly+ (insurance, finance, logistics, e-commerce) pay back DDD investment. Conversely, CRUD-centric admin screens stay fine on Transaction Script through 5+ years. Martin Fowler’s 2003 organization of the three styles is still useful.

DDD only matches business complexity. Too-early adoption produces only class explosion.

Domain-design traps

Common DDD-project failures. “Lining up patterns in form alone produces only anemic models” — the shared failure.

Forbidden moveWhy
Anemic domain model (Entities only data, logic in service layer)DDD form only. Complex Transaction Script — zero benefit
Primitive Obsession (money as number, email as string)Types can’t protect business. Use Money / Email Value Objects
Large aggregatesLock contention, performance drop, hard testing. Keep small
Direct object references between aggregatesBoundaries break. Inter-aggregate ID-only is the rule
Ignoring strategic DDD (Ubiquitous Language / Bounded Context)Same “customer” is different concepts across departments. Without context cuts, breakdown
Modeling business from code without business expertsBusiness words and code names diverge — the “User / Member / Account / Customer” problem
Apply DDD to all domains from startEven CRUD screens get 4-layer structure; classes explode
Domain Service abuseLogic that belongs in Entities leaks out. Entity first, then Domain Service
Publishing Domain Events directly to event busDoesn’t align with transaction boundaries. Use Outbox to harmonize
Dismissing DDD as “religion”DDD reliably pays back investment in domains with complex business rules and long-term operation. It’s a practical tool, not a sect
Choosing style without assessing business complexityDDD for CRUD is excessive; Transaction Script for complex business breaks down. Business-rule change frequency is the judgment axis

20+ years after Eric Evans’s 2003 “Domain-Driven Design,” DDD still feels hard because the substance is “learning business more than tech.” Time talking to business experts beats memorizing patterns.

DDD’s substance is the philosophy of growing the model in business language; mimicking patterns while skipping strategic DDD fails.

AI decision axes

AI-era favorableAI-era unfavorable
Value Object / Entity expressing business concepts as typesReusing primitive types (string, number)
Aggregates making consistency boundaries explicitImplicit transaction boundaries
Ubiquitous Language matching codeBusiness words and code names mismatched
Domain logic concentrated in domain layerLogic scattered across UI / Service / DB
  1. Choose by business complexity (CRUD = Transaction Script, complex = DDD).
  2. Start small and grow (DDD-from-start everywhere is over-engineering).
  3. Promote business concepts to types (Value Object / Aggregate / Ubiquitous Language).
  4. Avoid anemic models (logic in domain layer).

”Just Transaction Script split into two files” (industry case)

In an e-commerce project, the Order class held only id, status, and items, while “place order,” “cancel,” “refund” all sat as static methods on OrderService. In review, “this isn’t DDD — it’s just Transaction Script split into two files” got pointed out.

A common path for engineers learning DDD is writing an Order class where you can’t say order.place(), getting reviewed with “this Order is just a data class; business isn’t being expressed.” Many projects say “we adopted DDD” but are actually anemic models.

Lining up patterns by form alone, if logic doesn’t live in domain objects, it isn’t DDD. Code where order.place() can’t be written is still a data container, not an object.

What you must decide — what’s your project’s answer?

Articulate your project’s answer in 1-2 sentences for each:

  • Domain-logic style (Transaction Script / DDD / hybrid)
  • Value Object adoption scope
  • Aggregate boundaries (Bounded Context design)
  • Domain Event / event-driven adoption
  • Which DDD tactical patterns to adopt
  • Ubiquitous Language documentation and update rules

Summary

This article covered domain logic — Transaction Script vs DDD, Value Object, aggregates, strategic DDD.

Pick a style matching business complexity; promote business concepts to types. The 2026 realistic answer for domain-logic design including AI era.

The next article covers naming and code conventions (naming principles, linter / formatter, PR review, CODEOWNERS).

Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book

I hope you’ll read the next article as well.