Domain-Driven Design: What I Wish Someone Told Me
Look, I'm going to be honest with you. When I first picked up Eric Evans' DDD book, I thought I'd found the answer to all my architectural problems. Spoiler: I hadn't. But after three years of actually implementing DDD in production systems (and making every mistake you can imagine), I've got some thoughts.
What DDD Actually Is (and isn't)
Here's the thing about Domain-Driven Design: it's not a framework, it's not a library you can npm install, and it's definitely not something you can learn from a weekend tutorial. DDD is more like a philosophy for building software that actually matches how your business works, rather than forcing your business into whatever database schema seemed good at 2am.
Eric Evans coined the term back in 2003, and the core idea is simple but hard: model your software based on the actual business domain, not your technical preferences. I know, I know, you're thinking "well duh, doesn't everyone do that?" Trust me, most codebases I've seen are structured around database tables or whatever REST conventions someone read about, not the actual business logic.
The main concepts you need to wrap your head around:
Ubiquitous Language is probably the most underrated part of DDD. It means developers and business people use the SAME words for the SAME things. Sounds obvious, right? But I've been in meetings where developers called something a "user record" while the product team called it a "customer profile" and accounting called it a "billing entity." That's three different mental models for one concept, and it's a nightmare.
// Bad: Technical jargon that business people don't use
class UserRepository {
async persistEntity(data: UserDTO): Promise<void> { }
}
// Good: Language that matches business conversations
class CustomerRepository {
async registerNewCustomer(customer: Customer): Promise<void> { }
}
Entities and Value Objects sound academic but they're actually pretty practical. Entities have identity (a Customer with ID 12345 is always that customer, even if they change their email). Value Objects don't have identity, they're just... values (an EmailAddress of "test@example.com" is the same as any other instance with that string).
// Entity: has identity, can change over time
class Order {
constructor(
private readonly id: OrderId,
private status: OrderStatus,
private items: OrderItem[]
) {}
ship(): void {
this.status = OrderStatus.Shipped;
}
}
// Value Object: immutable, compared by value
class Money {
constructor(
readonly amount: number,
readonly currency: string
) {}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error("Can't add different currencies");
}
return new Money(this.amount + other.amount, this.currency);
}
}Aggregates are where things get interesting (and complicated). An Aggregate is basically a cluster of objects that you treat as one unit. The classic example is an Order and its OrderItems. You don't modify OrderItems directly, you go through the Order. This keeps your data consistent and your invariants enforced.
Repositories are just the pattern for getting Aggregates in and out of storage. Nothing fancy, but they abstract away whether you're using Postgres, MongoDB, or carrier pigeons.
Domain Events are how you broadcast that something important happened. "OrderShipped", "PaymentReceived", that kind of thing. I love these because they let different parts of your system react without tight coupling.
When DDD Actually Helps (In My Experience)
I'm going to be blunt: DDD is overkill for most projects. If you're building a CRUD app or a simple REST API, just use Rails or Django or whatever and move on with your life. Seriously.
But DDD shines when you've got genuinely complex business logic. Last year I worked on a supply chain system where "fulfilling an order" meant coordinating inventory across three warehouses, calculating shipping based on contracts that changed quarterly, and handling partial shipments with different SLAs. That's where DDD saved us, because we could model the actual business rules instead of drowning in if-else statements.
The communication benefits are real. When your code uses the same language as your business stakeholders, you can actually show them the domain model and they'll understand it. I've literally walked product managers through aggregate diagrams and had them point out business logic we'd missed. Try doing that with a typical Rails controller.
The flexibility is nice too. When business rules change (and they always do), you're modifying domain objects that represent actual business concepts, not untangling spaghetti code where business logic is mixed with database queries and HTTP handling.
The Parts Nobody Warns You About
Let me tell you what the books don't emphasize enough: DDD is HARD to learn. Not "read a tutorial" hard, more like "fundamentally rethink how you structure code" hard. I spent six months feeling like I was doing it wrong, and honestly, I probably was for at least four of those months.
It's slow at first. Really slow. You'll spend hours in meetings with domain experts trying to figure out what a "shipment" actually means in your business context. Your velocity will tank. Management will get nervous. I've been there.
And the overhead... look, if your app is straightforward, all the Aggregates and Bounded Contexts and Repository interfaces will feel like ceremony. Because they are. DDD adds abstraction layers, and abstraction always has a cost.
// Simple approach
async function createOrder(userId: string, items: Item[]) {
const order = await db.orders.create({ userId, items });
return order;
}
// DDD approach (more layers, more concepts)
class OrderService {
constructor(
private orderRepository: OrderRepository,
private customerRepository: CustomerRepository,
private eventBus: EventBus
) {}
async placeOrder(customerId: CustomerId, items: OrderItem[]): Promise<Order> {
const customer = await this.customerRepository.find(customerId);
if (!customer) throw new CustomerNotFound();
const order = Order.create(customer, items);
await this.orderRepository.save(order);
await this.eventBus.publish(new OrderPlaced(order.id));
return order;
}
}
Is the second version better? Depends. For a simple e-commerce site, probably not. For a complex B2B platform with weird business rules, absolutely.
My Actual Advice for Implementing DDD
First, you need real domain experts. Not just "someone from the business team," but people who actually know the domain deeply. If you can't get access to these people regularly, DDD will be painful.
Seriously invest in the Ubiquitous Language. I mean it. Have those awkward conversations where you debate whether something is a "shipment" or a "delivery" or a "dispatch." Write it down. Put it in a glossary. Reference it in code reviews. This alone will save you months of confusion.
Bounded Contexts are your friend, but they're also confusing at first. Think of them as different subsystems with their own models. In an e-commerce system, "Customer" in the shopping context might be different from "Customer" in the billing context. That's okay! They can have different properties and behaviors. Don't try to create one giant unified model.
Start small. Pick one subdomain, implement DDD there, see how it feels. Don't try to DDD-ify your entire codebase at once. I tried that once. It went poorly.
Keep iterating. Your domain model will be wrong at first. That's fine. The whole point of DDD is that the model evolves as you understand the domain better. Refactor it. A lot.
Stuff Worth Reading
If you're going to do this for real, read Eric Evans' original book. Yes, it's dense. Yes, it uses Java examples from 2003. Read it anyway. It's the foundation.
Vaughn Vernon's "Implementing Domain-Driven Design" is more practical and has better examples. I actually liked this one more for implementation details.
Martin Fowler's website has some good articles on DDD patterns without all the ceremony. Good for quick reference.
My Take After All These Years
Domain-Driven Design is not a silver bullet. It won't magically make your codebase better. It won't make complexity disappear.
What it WILL do, if you're working on genuinely complex business logic, is give you a structured way to model that complexity. It'll improve how you talk about the system with non-developers. It'll make your code align with business concepts instead of technical details.
But honestly? For most projects, it's overkill. And that's okay. Use the right tool for the job.
If you're building something where the business logic is the hard part (not the tech stack, not scaling, but the actual rules and workflows), then yeah, give DDD a serious look. If you're building a typical CRUD app or a thin API over a database, save yourself the headache and use something simpler.
I've seen DDD transform messy codebases into something maintainable. I've also seen teams waste months trying to force DDD onto problems that didn't need it. Know which situation you're in.
