DDD without the textbook: how I’d actually get business language into the code
Owen MacAllister
September 18, 2026
I stopped drawing aggregates for sport. The first time I “did DDD” I had a wall of yellow stickies, a folder named domain, and a product manager who still said “the thing” while we said SettlementBatchAggregateRoot. The business did not get clearer. The pull requests got longer.
What I do now is smaller and ruder. I steal the words the business already fights about, I put those words in the code and the tests, and I refuse patterns that exist to look like the book. Ubiquitous language is the part that paid. The rest is optional machinery I add only when a boundary is already hurting.
Start with the argument, not the layered architecture
I sit in a real conversation — a finance huddle, a claims call, a support war room — and I write down the nouns they correct each other on. Not the nouns in the Jira. The corrections. “That’s not a refund, that’s a reversal.” “Locked is not closed.” “A merchant and a store are not the same.” Those sentences are the domain. If I cannot collect five of them, I do not have a DDD problem. I have a CRUD app and a blog post I should not write.
I put those words in a glossary in the repo, twenty lines, not a bible. Then I grep. If the code still says status = 3 or doThing(), the glossary is a lie. The first pull request is often a rename. Renames are DDD. Hexagonal architecture is not automatically DDD.
I also put the words in tests that a non-engineer can almost read. should_reject_reversal_when_payout_is_locked. If product cannot hear their own rule in the test name, I do not have ubiquitous language. I have a translation layer I will drift from.
The parts that helped the business
Bounded contexts as separately deployable honesty. Catalog is not billing. Billing is not identity. I do not need four microservices to say that. I need two modules that do not import each other’s tables, and a word that means something different on each side — Customer in billing is not Customer in the CRM. When we forced one Customer type across the company, support and finance started lying to the same column. Splitting the word was the win. The Kubernetes part was optional.
A written invariant per painful rule. “Locked payouts cannot change beneficiary.” That sentence lives in the spec, the test, and a comment on the unique constraint. It does not live in a 12-page tactical-design chapter. The business can argue with a sentence. They cannot argue with a repository interface named IPayoutAggregateRepository.
Domain events we actually publish when something happened. PayoutLocked, not EntityChanged. Downstream jobs subscribe to the fact. We do not use events as a pretty message bus for CRUD. When we did, we invented a second source of truth and called it eventual consistency.
A person who owns the language. Not a DDD coach. The lead who will say no when a ticket says “update the flag.” I have been that person. It is unglamorous. It is the job.

The parts I dropped
Aggregates drawn before we had a transaction problem. I used to make every noun an aggregate. Then I needed a query that joined three of them and I was ashamed to write SQL. Now I start with a table and a transaction. I promote a cluster of entities to an aggregate when two writers keep stepping on each other or when a rule spans objects I keep forgetting. Until then, a module and a transaction is enough.
Value objects for everything. Money is worth it. UserId is worth it when we pass the wrong UUID. MerchantName as a class is a religion. I have deleted those classes. The tests got shorter. Nobody in finance noticed, because they never said MerchantName. They said “the doing-business-as line.”
A pure domain layer that cannot import the database. Beautiful in a sample. In a 2018 Spring app it became a pile of mappers that were the real domain. I will isolate the rule. I will not pretend Hibernate does not exist if the rule is “this row version must increment.” Sometimes the constraint is the domain.
Event sourcing as a default. I like an audit table. I like a ledger. I do not like replaying 400,000 events to answer “what is the balance.” When the business asks “what was true on Tuesday,” I reach for a snapshot or Datomic-shaped thinking, not a CQRS starter kit.
Ubiquitous language that only engineers speak. If the glossary uses Saga and Anti-Corruption Layer as first-class business words, I have failed. Those are our words. Their words are “reversal” and “store.”
How I get the language into a living codebase
Week one: record the arguments. Glossary. Grep. Rename the worst lie.
Week two: pick one painful rule and write it as a test that hits the real database. If the rule cannot be tested without mocks, it is not in the system yet. It is in a slide.
Week three: stop new names from entering through tickets. I review for handleData and processItem. I ask the author to use the glossary word or to add a word after talking to the business. Agents will generate processItem all day. The review is the language work now.
I do not run a two-day event storm as a prerequisite for every service. I will storm when the process is genuinely unknown and several teams disagree. That is a later essay’s job. Most weeks, a one-hour argument and a rename is the DDD.

A concrete rename that paid
We had Order.status with values that meant different things to warehouse and to finance. Warehouse meant “picked.” Finance meant “recognized.” We split the words: fulfillment_state and revenue_state. We put both in the glossary. We added two tests named after the sentences finance and warehouse used in Slack. Support stopped asking which “status” the customer email meant. That was DDD. We did not introduce a domain service named OrderLifecycleCoordinator. We almost did. I am glad we did not.
Another time we introduced an AntiCorruptionLayer package for a vendor. Useful. We also started calling the vendor’s SKU a Product inside that layer and a Sku outside, then mixed them. The pattern did not save us. The mixed word hurt us. I would still wrap a vendor. I would not let the wrap invent a second product concept without a glossary line.
Agents make the language work more important
Generated code loves generic names. processOrder, handleUpdate, StatusEnum. If the glossary is not in the prompt and the review, the model will wash the business out of the repo in a week. I paste the twenty lines into the job file. I fail the PR if a new public type is a synonym we already rejected. That is DDD in 2026: not more workshops, a lint on the words.
I also do not let the model invent a context map. It will draw boxes. The boxes will look responsible. The database will still be shared. I want the import rule and the table ownership, not a generated PNG.
What I tell teams that want “to adopt DDD”
Do not adopt DDD. Adopt the words. Adopt one invariant in a test. Adopt a boundary you will not join across. Leave the textbook on the shelf until a specific pain names a pattern — an aggregate for contention, an ACL for a vendor, a context map when two teams share a word and a database.
If your company is a hundred people and every squad names DDD as a goal, you will get ten different books and one still-confused finance team. Give finance a sentence in the code. That is the adoption.
I get business language into the code by listening for corrections, writing them down, grepping for the lies, and testing the rule under the name the business uses. I draw an aggregate when two writers force me to. I do not draw one because a chapter said the order is an aggregate. The book is a catalog of tools. The argument in the hallway is the work.
If you need a Monday start: steal five corrections, make a twenty-line glossary, rename one lie, write one test a PM can hear. Then stop. If that did not help, more diagrams will not help. If it did, you are already doing DDD without the textbook, which is the only version I have seen survive contact with a backlog.
The language is the product’s memory. Put it where the compiler and the test runner can see it. Everything else is optional ink on a whiteboard I will wipe on Friday. I have wiped a lot of aggregates. I have never regretted a glossary line that matched a fight I actually heard. That is the score I keep now. Not how many patterns I can name from memory. How many of their sentences survive a git grep.