· 7 min read · updated
Alternatives to Domain-Driven Design, and When to Use Them
Series: Domain-Driven Design (DDD) Series · part 11
- 1. What Domain-Driven Design Is (DDD series part 1)
- 2. Benefits of using DDD (DDD series part 2)
- 3. Who should consider using DDD? (DDD series part 3)
- 4. Domain Model (DDD series part 4)
- 5. Bounded Context (DDD series part 5)
- 6. Value object (DDD series part 6)
- 7. Entity (DDD series part 7)
- 8. Services (DDD series part 8)
- 9. Mastering Transactions: The Power of Aggregates in DDD
- 10. Domain Events in DDD (DDD series part 10)
- 11. Alternatives to Domain-Driven Design, and When to Use Them
I have spent ten parts of this series arguing for Domain-Driven Design, so this part owes you the other half. Most of the projects I have worked on did not need it. A few of them adopted it anyway, and the result was a codebase with aggregates, repositories and value objects wrapped around business rules that amounted to "this field cannot be empty". Nobody was better off. The invariants were not worth the ceremony, and the ceremony was the only thing anyone remembered.
DDD is not a default with exceptions. It is one option among several, and the others have names, histories and legitimate uses rather than being what you settle for when the team is not good enough. Treating the alternatives as failure modes is how teams end up paying for a model they never needed.
This part covers the signals that say DDD is the wrong purchase, the four alternatives worth knowing by name and what each one is genuinely good at, why microservices belong nowhere on that list, what tactical-only DDD gets you and what it quietly gives up, and the one question I ask before agreeing to model anything.

Every option on this page is a correct answer to some project. The mistake is having only one.
The signals that say no#
Three conditions push a project away from DDD, and any one of them is usually enough.
The domain has no invariants worth protecting. If every rule in the system is a field validation, there is no consistency boundary to draw, and the aggregate you build will exist to hold a setter. Ask someone to describe a rule that spans two pieces of data. If the room goes quiet, you have a data entry application, and that is a perfectly good thing to have.
Nobody can correct you. DDD runs on a ubiquitous language, and a ubiquitous language requires a domain expert who will tell you that you used the wrong word. Without that person the model becomes the developers' guess, written in developer vocabulary and defended in code review. That is worse than an honest CRUD layer, because it looks like it was designed.
The system will be rewritten before it is maintained. Internal tools with a known two-year life, migration scripts, prototypes that exist to answer a question. DDD is an investment that pays back over years of change. A project that will not see those years cannot collect.
Team inexperience is the reason people usually cite, and I think it is the weakest one. Teams learn. A domain with nothing to model does not become interesting because the team got better.
Transaction script#
The oldest option and still the correct one more often than its reputation suggests. One procedure per use case, doing the work from top to bottom: read the input, run the rules, write the result.
final class RegisterAttendee
{
public function __invoke(int $eventId, string $email): void
{
$event = $this->events->find($eventId);
if ($event->seatsTaken >= $event->capacity) {
throw new EventFull();
}
$this->attendees->insert($eventId, $email);
$this->events->incrementSeats($eventId);
}
}The whole rule is visible in one screen, which is the property people undervalue. There is no indirection to trace, no mapping layer, and a new developer reads it correctly on the first attempt.
It degrades in a specific and predictable way. When the same rule appears in a second script it gets copied, and the two copies drift. That duplication is the signal to move, and noticing it early is most of the skill.
Active record#
The model object knows how to save itself. Eloquent, Rails, most ORMs when you use them the way the framework intends.
Active record gets dismissed by architecture-minded developers, and the dismissal is usually a category error: it is not a broken version of a domain model, it is a different trade. You give up the ability to test business rules without a database, and you get an enormous reduction in the code between an HTTP request and a row. For a system whose rules are thin and whose schema is stable, that trade is straightforwardly good.
It stops working when the rules stop matching the table shape. The moment a single business concept spans four tables and two of them have conditional writes, the model that maps one class to one table is fighting you.
Table module#
One class per table, holding the logic for all rows of that table rather than one instance per row. Underused outside the .NET world, and a good fit for reporting-shaped and batch-shaped work where you operate on sets rather than individual entities.
If your business logic keeps wanting to say "for every overdue invoice in this account", a table module says that directly and a domain model makes you loop.
Tactical DDD without the strategic half#
The most common real-world position, and the one worth being honest about. You adopt value objects, entities, repositories and aggregates, and you skip context mapping, event storming and the organisational work of agreeing boundaries.
This buys real things. Value objects remove a class of bug that primitives cannot. Aggregates give you a transaction boundary you can defend. Repositories keep the ORM out of the rules.
It also gives up the part that produces most of DDD's value on large systems. Without bounded
contexts you have one model that every team edits, which means the word Customer means four things
and each meaning has a stakeholder. The tactical patterns will not save you from that, and no amount
of good aggregate design substitutes for a boundary that was never drawn.
Useful? Yes. Domain-Driven Design? Only half of it, and the cheaper half.
Microservices are not an alternative#
Earlier versions of this article listed microservices alongside CRUD and layered architecture. That was wrong and worth correcting rather than quietly editing out.
DDD is a way to model a domain. Microservices are a way to deploy software. They are orthogonal: a monolith can be rigorously domain-driven, and a fleet of forty services can be forty transaction scripts with HTTP between them. Choosing microservices answers a question about team autonomy, scaling and deployment risk. It answers nothing about how your business rules are expressed.
The two get conflated because bounded contexts make excellent service boundaries, which is true and is a statement about DDD helping you deploy, not about deployment replacing modelling.
A comparison you can act on#
| Approach | Good when | Fails when |
|---|---|---|
| Transaction script | Few use cases, rules visible in one place | The same rule appears in a third script |
| Active record | Rules match the table shape, schema is stable | One concept spans several tables with conditional writes |
| Table module | Set-shaped and batch-shaped logic | Per-entity invariants start to matter |
| Tactical DDD | Real invariants, one team, one model | Several teams need the same word to mean different things |
| Full DDD | Complex rules, an expert who corrects you, years of change ahead | Any of those three is missing |
The table has no default row on purpose. Reading it top to bottom and stopping at the first row that fits is a better decision procedure than starting at the bottom and justifying your way up.
One rule#
Before agreeing to model anything, I ask for one business rule that cannot be expressed as a constraint on a single field, and I ask a non-developer to say it out loud.
If they can, and the rule surprises me, the domain is worth modelling and the rest of this series applies. If the answer is a list of required fields, build the CRUD application, ship it, and spend the saved months on something the business actually asked for.
The scope of that rule is narrow and deliberate. It tells you whether a rich model earns its place. It says nothing about whether your architecture should be hexagonal, whether you need CQRS, or how your services should be deployed. Those are separate purchases with separate bills, and I have written about hexagonal architecture and DDD together for the first of them.
Previous DDD parts#
Unlocking the Power of Domain Events in DDD (DDD series part 10)
Mastering Transactions: The Power of Aggregates in DDD
Value object (DDD series part 6)
Bounded Context (DDD series part 5)
Domain Model (DDD series part 4)
Who should consider using DDD? (DDD series part 3)
Benefits of using DDD (DDD series part 2)
Unlocking the Power of Domain Driven Design in Software Development (DDD series part 1)
$ related posts
18 min readNEW
The Application Layer, the Transaction, and a Working API
Application layer in Symfony 8: use cases on a Messenger command bus, the transaction boundary in config, optimistic locking and RFC 9457 errors. Part 2.
DDD, CQRS and Hexagonal Architecture in Symfony 8 · part 2#ddd#hexagonal-architecture#symfony#php#cqrs#messenger#software-architecture15 min readNEW
The Domain Model, and the Only Rule That Matters
DDD domain model in PHP 8.5 and Symfony 8: keep the container out of the domain, and choose Doctrine ORM or DBAL with the costs on the table. Series part 1.
DDD, CQRS and Hexagonal Architecture in Symfony 8 · part 1#ddd#hexagonal-architecture#symfony#php#doctrine#software-architecture3 min read
Event Sourcing: State as a History of Events
Storing every change as an event instead of overwriting state: how the history is replayed, where event sourcing fits, and the practices it demands.