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.”

Before you read this

This article is mostly about how programs are written and structured. If IT vocabulary is unfamiliar, reading the primer "Programs and APIs" first makes it far easier to follow. You can also look anything up in the glossary as you read.

What is domain logic in the first place

Domain logic is “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 two main styles — Transaction Script vs Domain Model

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

3 Ways to Express Domain Logic Choose by complexity & team maturity. In practice, it's Transaction Script vs DDD Transaction Script Procedural: 1 request = 1 function getUser → checkStock → calcPrice saveOrder → sendMail → return Strengths Simple and fast to start coding Low learning curve Weaknesses Logic scatters and duplicates easily Becomes chaotic as business grows CRUD-centric, for MVPs with 3 or fewer people Table Module Logic grouped per table OrderTable.calcTotal() UserTable.validate() Positioning Middle ground for legacy renovation Rarely used for new projects Almost never chosen in practice Domain Model (DDD) Express business concepts as classes Order.place() → OrderPlacedEvent Money.add() → express constraints via types Strengths Business rules consolidated in model Resilient to complex business changes Weaknesses High learning curve Overengineering for small scale For complex domains with 50+ business rules Simple ← Business Complexity → Complex Start with Transaction Script. Migrate to DDD as complexity grows — the pragmatic approach
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 — the strength of being simple

Transaction Script vs Domain Model

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) — giving order to complexity

Domain Model represents business concepts as classes and aggregates logic there. DDD 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’s main 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 objects are where most of the benefit shows up first.

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.

The aggregate is the unit that holds consistency.

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 is the layer above all of those tactics.

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.”

The anemic domain model — the most common 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.

How to choose — decide it on business complexity

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.

Where the logic belongs follows one 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.

By case, the style lands like this.

CRUD-centric work with simple business rules. Transaction Script. Forcing DDD just adds classes without raising business value.

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

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

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

Three scenarios

If you are building solo or at a startup

Write it as a Transaction Script and move fast. At the MVP stage the business rules are still being discovered, so building a domain model ahead of time means rebuilding the model every time the hypothesis changes. That said, introducing value objects (Money, Email and the like) early makes the later migration considerably easier.

Personal / Startup: Ship in One Month Is Correcten.senkohome.com/arch-intro-case-startup/

If you are a small or mid-size SaaS

Around the point where the business rules pass fifty, it becomes time to start a staged move to a domain model — a lightweight DDD. There is no need for a full rewrite: moving the areas that change most, such as pricing calculation or stock allocation, onto a domain model first is the realistic path.

Small-Mid SaaS - Lean on Managed and Run with Few Peopleen.senkohome.com/arch-intro-case-saas/

If you are a large enterprise

For a genuinely complex domain of more than two hundred rules — insurance, finance, logistics — the investment in full DDD, aggregates and domain events included, pays back. When improving a legacy system, migrate the bottleneck areas onto a domain model bit by bit, and avoid a big-bang rewrite at all costs.

Large-Enterprise Core: Design That Holds Up for Yearsen.senkohome.com/arch-intro-case-enterprise/

AI decision axes — Business concepts promoted to types reach the AI

DDD terminology organization helps convey domain knowledge to AI

When ubiquitous language (the shared terminology between developers and business stakeholders) is reflected in type names and method names in code, AI can write code with an understanding of domain intent. If “order,” “shipment,” and “billing” are type-defined in code as Order, Shipment, and Invoice, AI can accurately grasp the relationships between them.

Excessive abstraction in CRUD apps hampers AI

Applying DDD’s full arsenal (Aggregate, Repository, Domain Event, etc.) to a CRUD app with few business rules means AI needs to understand unnecessary abstraction layers when you ask it for fixes — actually reducing efficiency. A structure matching business complexity is more efficient for AI utilization.

Pitfalls and forbidden moves

Here are the six most dangerous of the typical ways a project that adopted DDD goes wrong.

Forbidden moveWhy it is bad → what to do instead
An anemic domain modelthe shape of DDD with none of the benefit → let the logic live on entities and value objects
Primitive obsessionthe types cannot protect the business rules → promote an amount to Money and an address to Email
Designing aggregates largelock contention, degraded performance and tests that are hard to write → keep them small and reference across aggregates by ID only
Copying the tactical patterns while ignoring strategic DDDthe same “customer” means a different thing in each department and it collapses → cut the contexts with bounded contexts
Modelling from the code alone, without talking to domain expertsthe business vocabulary and the code names drift apart → build the ubiquitous language
Applying DDD to every domain from the starteven the CRUD screens get a four-layer structure and the classes explode → use it only at the complex boundaries

That DDD still feels hard more than twenty years after Eric Evans’s original book is because the essence of it is “learning the business rather than the technology.” Time spent talking to domain experts matters more than time spent memorising patterns.

Author’s note — “just a Transaction Script split across two files”

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.