In plain English, here is everything Northwind does. No code, no database, no framework โ just the business, as its three founders would describe it:
| Term | What it means | Northwind example |
|---|---|---|
| Domain | Real-world subject matter the software serves | "Online retail" |
| Business domain | The specific domain your company operates in | Northwind's business domain is online retail |
| Subdomain | A distinct area of responsibility inside it | Order Management, Inventory, Payments, Auth |
| Bounded Context | The boundary in code where one model applies | The order-management module |
| Ubiquitous Language | Shared vocabulary inside one context | "Product" always means a line item, in Order Mgmt |
Domain and subdomain describe the business. Bounded context and ubiquitous language describe how you organize the code and words around it. Keep those two groups separate and most DDD confusion disappears.
Northwind grew. Now there's an Ordering team and a separate Inventory team, each shipping its own service. Both use a type called Product.
// InventoryService โ "price" is the wholesale cost Northwind pays the supplier class Product { sku: string; price: number; stockCount: number; } // OrderService โ imports the SAME Product type from a shared "models" folder function calculateTotal(product: Product, qty: number) { return product.price * qty; // customer is charged supplier COST, not retail price }
price. The system still shipped a real one: customers were charged wholesale cost for weeks before anyone noticed margins had collapsed.
The term traces back to Eric Evans' 2003 book Domain-Driven Design: Tackling Complexity in the Heart of Software โ Entity, Value Object, Aggregate, Domain Event, Repository all come from that book by name.
DDD is an approach to software design that puts the business domain โ not the schema, not the framework โ at the center of every decision. It requires engineers to collaborate deeply and continuously with domain experts, not just gather requirements once and disappear into tickets.
Legacy codebase? Same method โ look for a class name used two different ways, or a "simple" change needing another team's sign-off.
| Context | Owns | Core model |
|---|---|---|
| Order Mgmt | Orders, checkout | Order, OrderLineItem |
| Inventory | Stock, SKUs | Product, StockCount |
| Payments | Charges, refunds | PaymentAttempt (via ACL) |
This is a candidate, not a final answer. Northwind proposed splitting "Pricing" out of Order Management โ discussion revealed no real seam, and it merged back in. Propose โ discuss โ reject or merge โ standardize.
Ubiquitous language is a shared vocabulary developers and domain experts use everywhere, for one bounded context. Applied to the bug from Slide 4:
// After a 15-minute glossary conversation with the business: // "price" the business pays a supplier is a WholesaleCost. // "price" the customer pays is a RetailPrice. Never the same field. class Product { sku: string; wholesaleCost: Money; retailPrice: Money; stockCount: number; } function calculateTotal(product: Product, qty: number) { return product.retailPrice.multiply(qty); // unambiguous }
Pros: shared meaning, readable code, better decisions. Cons: upfront workshop effort; sloppy naming breaks the whole benefit.
Ubiquitous language is scoped per bounded context โ not company-wide. Each context gets its own internally-consistent vocabulary:
Renaming one field fixes one bug. It doesn't stop the next team from importing the wrong Product again. A bounded context gives each meaning its own model.
// inventory-context/Product.ts โ not shared class Product { sku: string; wholesaleCost: Money; stockCount: number; } // order-context/Product.ts โ not shared class Product { sku: string; retailPrice: Money; }
A bounded context isn't automatically a microservice โ it can be a module inside one monolith. It's the model-and-language boundary that counts, not the deployment.
Customer stays the same person even after their address changes. Entities are mutable.Money has no identity of its own. Value objects are immutable.// Entity โ has an ID that outlives any single attribute class Customer { constructor(public readonly id: string, private address: Address) {} relocate(newAddress: Address) { this.address = newAddress; } // same customer, new address } // Value Object โ immutable, compared by value, no ID class Money { constructor(public readonly amount: number, public readonly currency: string) {} multiply(qty: number) { return new Money(this.amount * qty, this.currency); } }
"You don't adopt DDD by adding patterns โ you adopt it by removing ambiguity."