An environment for Domain-Driven Design
Domain-Driven Design makes one big claim: that a model built with the business, and written into the code rather than into a document beside it, keeps earning its place as the business changes. A good model lasts.
It is the method's most important claim and the one nobody can show you, because almost every article about DDD is written from a project that is eighteen months old.
I can put a number against it. The entity model I am going to describe has been in production in a foreign-exchange trading business for twenty-five years, and it is running today. I wrote it from scratch starting in 2000; it went live in 2001 with two tiers and five hundred accounts, and it grew into a globally distributed platform carrying millions of accounts. In those years the storage engine was replaced, the architecture generation was replaced twice, the regulatory surface changed, and the company changed its brand. The way entities are structured and positioned did not change.
Two things that number is not. It is not this code: the framework in this article is a new implementation of that model, written recently in Java, Spring Boot and Angular. And it is not a customer story — that business does not run this software. What has the twenty-five-year record is the idea, and the record is my own work, which is the only reason I can write about it at all.
So this article is about what that model turned into, measured honestly against DDD — including the parts where it does not measure well.
The short version:
DDD tells you what to build. Esquire is where you build it.
Not a DDD framework. An environment for one.
What DDD actually asks of you
Briefly, for readers who do not practise it, because the rest of this depends on it.
DDD's premise is that in software with complicated business rules, the hard part is not the technology — it is understanding the business. So you build a model of the business together with the people who know it, you name everything in one shared language, and you write that model straight into the code. The names in the code are the names the business uses. When the business renames something, you rename it in the code that day.
It has two halves. The strategic half is about boundaries and language — where one model ends and another begins, and how the two talk. The tactical half is the pattern catalogue: entities, value objects, aggregates, repositories, factories, domain services, domain events. The tactical half is the famous one, and a project can follow every pattern in it and still have no model at all. That is the usual way DDD fails.
And it forces real limits, which is the part usually left out of articles like this one:
- No single company-wide data model. Each context keeps its own "Customer", and the duplication is deliberate.
- The aggregate is a consistency boundary, and the rules are hard: reference other aggregates by id only, change one aggregate per transaction, and anything that crosses aggregates is eventually consistent.
- Aggregates stay small, or they become a lock everybody waits on.
- The model must not know about storage — no ORM detail, no SQL inside the business rules.
- Behaviour lives with the data. Push it all into services and you get the anemic model DDD names as the failure.
- Language discipline is permanent.
- It is expensive, and it only pays on the core domain. Applying the whole catalogue to CRUD is the most common way DDD is misused.
- It needs a business expert available continuously. Without one you get folklore.
Item 7 is where this article starts.
One: it is the part of DDD you are told not to build
Evans' advice about your system's outskirts is unambiguous: find the core domain, put your best people on it, and get the generic subdomains as cheaply as you can. A generic subdomain is a part every business needs and no business competes on.
Look at what that covers in a real backoffice system. Identity and sign-in. Permissions. The audit trail. Storage. The messaging between services. Metrics, logs and traces. Every one of those is a generic subdomain in DDD's own vocabulary, and every one of them is where projects actually spend their first year.
Esquire is those, already built and already joined to each other: a Keycloak-backed identity flow with the user directory kept in step, permissions resolved from the organisation tree, an audit trail on every write, a messaging bus with pluggable transports, and an observability stack that can be switched on for a while and switched off again.
So the framework's motto — nothing else is left to write — is a DDD statement that avoids the vocabulary: only the core domain is left to write.
That is the first move, and on its own it is only half true. A framework that filled the generic subdomains and nothing else would be a collection of libraries with a shared build.
Two: and it sets how an entity is declared
The second move is the one that makes it a framework rather than a library set, and it is the part DDD has no name for.
In Esquire an entity declares itself, once, across five different things at the same time:
| An entity declares | Which means |
|---|---|
| its data | the fields it holds, per kind |
| its permissions | who may see it, who may change it, who may change it only on their own record |
| its behaviour | which commands it accepts, and which handler runs for each event |
| its validation | what a value must satisfy — required, range, allowed values |
| its look and feel | the tab it appears on, its order, its label, its hint, how a code is shown as words |
One statement. The server obeys it, the REST contract exposes it, and the browser receives it and builds the dialog from it — the browser holds no second copy of the rule.
DDD has no pattern that spans those five. Its nearest ones each take a single column: Specification is the validation alone, Published Language is the contract alone, Entity is the data alone. And presentation is outside DDD's scope on purpose. So this is not a mapping exercise where the framework gets a DDD label pinned to each part; it is a concept the method does not carry.
Two consequences are worth more than the description.
The ubiquitous language gets a machine-readable home. In most projects the shared language lives on a wiki page that is stale within a month. Here it is the running configuration: the field's business label, its hint, its allowed values with their business names, the rule it must satisfy. It cannot go stale, because changing it changes the screen. And it is single-sourced across the whole stack, so the language cannot drift between the server and the browser — which is the usual place a ubiquitous language quietly breaks in half.
And it is a third answer to the rich-versus-anemic argument. Fowler's split is between rules living on objects and rules living in services. Moving the description of an entity into data is neither: the rule is stated once, in a place both sides of the wire read. That is not better than rich types — see the honest half below, where a compiler cannot check a declaration — but it is a different axis, and the two combine rather than compete.
Three: it forces exactly one thing
This is the sentence I would most like a DDD reader to take away.
Esquire does not dictate your domain model. Entity structure, aggregation, your types, whether a rule lives on an object or in a procedure — all of it stays yours.
The single thing it forces is that entities hang on the user access tree: every entity has a position, and that position is a stored path.
One constraint, and here is what it buys.
"Every list must be limited to what this user may see" is an invariant DDD can state and cannot enforce. It depends on every developer remembering it in every query, forever — and a query missing the filter looks exactly like a correct query. Nothing goes red. Not the compiler, not the linter, not code review, because there is nothing there to review.
In Esquire visibility is resolved from the caller's position in the tree, on every read, because the position is a column on the row and the caller carries theirs. There is no filter to forget, because there is no filter to add.
For a domain team that removes an entire class of rule from the model. Authorization stops being something the domain has to say, and becomes a property of where a thing sits.
I did not arrive at that opinion on my own. Reading half a dozen open-source projects that scope authorization by hierarchy, the model turned out to be broadly the same everywhere — and the projects split cleanly into those where the scope can be forgotten and those where it cannot. The ones that made it structural all give the same reason, in their own words.
Four: and it does not hand you the tactical patterns
The declaration is data, and the accounting workflow that ships with the framework is procedural. If you want rich types with the rules on them, write them behind the declaration. The framework does not do it for you and does not stand in the way.
That is the whole of the deal: an environment supplies the surroundings, never the domain code.
What else falls out, once the environment is there
These are not claims about DDD — they are the parts that happen to line up with it, and every row was checked against the code.
| DDD | Esquire |
|---|---|
| Ubiquitous Language | the entity dictionary — names, labels, allowed values, rules, defaults, read by server and browser alike |
| Entity | a node on the tree: kind plus id; identity survives every field change |
| Aggregate | the entity with its sub-parts — person, addresses, custom parameters, children — loaded and saved as one |
| Aggregate root | the tree node; kind plus id is the only handle the outside gets |
| Aggregate version | the change number on the row, which decides whose change wins |
| Repository | one self-contained SQL per method, no dynamic SQL, ORM types never in the service layer |
| Domain event | every save publishes an entity broadcast on the bus |
| Specification | the validation rules in the dictionary, applied on the server and sent to the browser |
| CQRS read model | the tree cache, fed by events, rebuilt from the canonical tables |
| Anticorruption layer | the service that sits between the Esquire model and Keycloak's |
| Shared kernel | the common and messaging modules |
Three of those deserve a sentence more.
Domain events cost nothing to publish. In a normal project "publish a domain event" is a month of work — an outbox, a broker, ordering, retries, tracing. Here the bus is already there, with ordering rules, a change number to settle which change wins, and a trace that follows the message across the hop. A business operation publishes its event by being written.
The read model is not a cache somebody added for speed. It is the tree shaped the way it is traversed, fed continuously by entity events and rebuildable from the canonical tables, with a content checksum and a sweep that reloads and swaps on drift. Most teams arrive at CQRS after the read shape stops matching the write shape, and skip the rebuild path because it is the expensive part. Here the split is the starting position.
Aggregate boundaries are structural rather than agreed. In DDD you decide where an aggregate ends and then defend that line in review forever. Here ownership is the shape of the data: an entity has a parent, a path and children, and its sub-parts move with it.
The honest half
None of the above is believable without this section, and none of these is an accident.
The services are not bounded contexts. Everyone assumes many services means many contexts. These six do not: they speak one language about one tree and share a kernel. They are a functional partition of one context. The proof is that they can be folded together at will — three of them run as a single process in the compact deployment, and a fourth is carried next to the gateway. Contexts, by definition, cannot be merged like that.
What does not follow is that Esquire hosts only one context. A tree with its own kinds and its own tools is a context, and a deployment carries as many trees as you like. The context map is drawn between trees, not around the framework — which is the same point as the spine: an environment does not occupy a cell on the map, the map is drawn inside it.
The shared kernel is large, and the cost is measured rather than theoretical. DDD warns that a shared kernel must stay small, because a change to it is a change for everyone. Ours is not small. In the last release, adding one method to the messaging module forced every service image to be rebuilt, because each image bakes its own copy of that jar. That is the warning arriving on schedule.
The declaration is data, so no compiler checks it. That is the real cost of the five-column declaration described above. A rich type gets you compile-time checking of the rules; a dictionary entry gets you one description that the server, the API and the browser all obey, changeable without a browser release. Those are different trades, and I would not pretend the second dominates the first.
The shipped accounting workflow is procedural, with anemic entities — a processor reads both accounts, checks the rules, writes both legs. Say whose choice that is: that workflow is the domain adapter, a small worked example plugged into the seam to show how a domain attaches. It is the part a team replaces, and every observation about how it is written describes it and not the framework. In fairness it is also defensible in DDD's own vocabulary — behaviour that belongs to no single entity is a Domain Service, and a two-legged transfer is the textbook case.
It writes both legs in one transaction, where strict DDD would make the second leg an eventually-consistent reaction to an event, because it is a second aggregate. That workflow chose immediate consistency, which is what accounting expects. The bus is right there for a domain that prefers the strict rule.
The dictionary covers nouns, not verbs. It states the language of the data — fields, values, rules. The language of the actions lives in the command surface and the operation codes. A ubiquitous language covers both, and this is the clearest place the framework could go further.
And adopting an environment costs more than calling a library. The tree, the kinds, the dictionary and the bus arrive as a set. Leaving is not a small job. That is the honest price of everything in the first half of this article, and any version of this piece that leaves it out is selling something.
It is also not a low-code tool and not a generator. Real Java, written by developers, in their own modules.
The neighbours
There are tools that carry the label "DDD framework", and it would be poor form to write all this while implying the field is empty. What they supply falls into three kinds:
- The machinery the tactical patterns need — aggregate loading and versioning, command and event dispatch, an event store, sagas, projections. Genuinely hard, genuinely reusable.
- The vocabulary made executable — types and annotations named after the patterns, so the model's structure is visible to tooling instead of living in a reviewer's head.
- Enforcement of what a team otherwise holds by discipline — no object references across aggregates, module boundaries a build fails on.
All three sit inside the domain. Esquire sits around it: identity, permissions, audit, messaging, storage, the screen. Different slots, and in principle they compose — aggregates from such a framework could be the implementation behind an Esquire entity declaration, because the declaration constrains how an entity is described, not how it is modelled.
Which is also why I will not accept the label for this:
A framework can support DDD. It cannot do DDD.
The design is finding the model with the business, naming it, and deciding where the boundaries fall. That is work done with people. Anything claiming otherwise is selling the mechanics under the name of the method.
What a DDD practitioner actually gets
You keep your model and your patterns. You keep your aggregates, your language, your context boundaries — Esquire does not find those for you, and no framework does; deciding where one context ends is work done with the business, and booking that as a framework's limitation would be dishonest.
What you put down is identity, permissions, audit, messaging, storage and the plumbing between them.
What you gain, beyond the time, is one invariant you can no longer forget.
And if the twenty-five years at the top is worth anything to you, it is worth this much: the thing that survived all that replacement underneath was not the pattern catalogue. It was the decision about how entities are structured and where they sit. That is a strategic-half result, not a tactical one, which is the part of DDD that fewer people practise and the part that turned out to matter.
The system is Esquire, a framework for backoffice systems — Java and Spring Boot, an Angular front end, Postgres or Oracle, identity synchronised to Keycloak. It runs as eight processes, or five, or four, from the same code — the composition is chosen at deployment. The four repositories are public at github.com/mir0n-pro. A deployment is running at esquire.mir0n.pro — sign in with mainadmin / q, it is a demonstration tree and it is seeded fresh with each release.