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;
| Style | Trait |
|---|---|
| Transaction Script | Procedural; one business operation = one function |
| Table Module | Logic 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(...)
}
| Strengths | Weaknesses |
|---|---|
| Simple, fast to start | Logic scatters everywhere |
| Low learning cost | Similar processing duplicates |
| Fits small projects | Becomes 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.
| Strengths | Weaknesses |
|---|---|
| Logic concentrated, easy to test | High initial design cost |
| Code follows business changes | High learning cost (whole team) |
| Strong on large complex domains | Excessive 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.
| Pattern | Role |
|---|---|
| Entity | Identified by ID, internal state mutable |
| Value Object | Identified by value, immutable (e.g. Money, Email) |
| Aggregate | Cluster of entities with consistency boundary |
| Repository | Window for persisting aggregates |
| Domain Service | Logic spanning multiple aggregates |
| Domain Event | Represents 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.
| Concept | Substance |
|---|---|
| Ubiquitous Language | Use the same words in business and development |
| Bounded Context | The range where the meaning of words holds |
| Context Map | Relationship diagram across multiple contexts |
| Event Storming | Discovery 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 complexity | Rule count | Recommended style | Tactical patterns to adopt |
|---|---|---|---|
| Simple CRUD | up to 10 | Transaction Script | None (plain service layer) |
| Moderate | 10-50 | Transaction Script + Value Object | Value Object only |
| Complex | 50-200 | Domain Model (DDD-light) | Entity / Value Object / Repository |
| Very complex | 200+ | Full DDD | Entity / 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 move | Why |
|---|---|
| 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 aggregates | Lock contention, performance drop, hard testing. Keep small |
| Direct object references between aggregates | Boundaries 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 experts | Business words and code names diverge — the “User / Member / Account / Customer” problem |
| Apply DDD to all domains from start | Even CRUD screens get 4-layer structure; classes explode |
| Domain Service abuse | Logic that belongs in Entities leaks out. Entity first, then Domain Service |
| Publishing Domain Events directly to event bus | Doesn’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 complexity | DDD 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 favorable | AI-era unfavorable |
|---|---|
| Value Object / Entity expressing business concepts as types | Reusing primitive types (string, number) |
| Aggregates making consistency boundaries explicit | Implicit transaction boundaries |
| Ubiquitous Language matching code | Business words and code names mismatched |
| Domain logic concentrated in domain layer | Logic scattered across UI / Service / DB |
- Choose by business complexity (CRUD = Transaction Script, complex = DDD).
- Start small and grow (DDD-from-start everywhere is over-engineering).
- Promote business concepts to types (Value Object / Aggregate / Ubiquitous Language).
- 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.
📚 Series: Architecture Crash Course for the Generative-AI Era (27/89)