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.
| 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 — the strength of being simple
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) — 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.
| 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’s main 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 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.
| 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.”
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 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.
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.
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.
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.
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 move | Why it is bad → what to do instead |
|---|---|
| An anemic domain model | the shape of DDD with none of the benefit → let the logic live on entities and value objects |
| Primitive obsession | the types cannot protect the business rules → promote an amount to Money and an address to Email |
| Designing aggregates large | lock 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 DDD | the 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 experts | the business vocabulary and the code names drift apart → build the ubiquitous language |
| Applying DDD to every domain from the start | even 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
Related Articles
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.
Also popular with readers
📚 Series: Architecture Crash Course for the Generative-AI Era (33/95)