Summary of Domain-Driven Design
My notes and takeaways from roughly the first 200 pages of Domain-Driven Design, covering domain models, ubiquitous language, aggregates, and design that reflects the business.
These are my notes and takeaways from reading roughly the first 200 pages of Domain-Driven Design. This is a summary of my reading so far, with more to follow.
DDD teaches us that successful software development isn’t just about writing code. It’s about truly understanding the business problem we’re trying to solve.
1. The Big Idea Behind Domain-Driven Design
The book starts with a simple but powerful idea: software succeeds when it truly represents the business it’s meant to serve. Too often, software gets bogged down in technical complexity or poorly understood requirements. Domain-driven design (DDD) bridges the gap between business and software by focusing on building models that reflect the core domain of the business.
- Takeaway: Work closely with domain experts and prioritize the areas of the business that matter most. Don’t let technical concerns distract from the bigger picture.
2. Ubiquitous Language
One of the biggest challenges in software development is communication. Developers and business stakeholders often speak different "languages." DDD tackles this by creating a ubiquitous language: a shared vocabulary based on the domain model that everyone uses in discussions, documentation, and code.
Why it matters: A shared language helps prevent misunderstandings. For example, if everyone agrees that "route cargo" means assigning a shipment to a specific path, there's less room for error.
- Takeaway: Keep the language consistent across the team, and let it evolve as your understanding of the domain deepens.
3. Binding Model and Implementation
A domain model is more than just a data structure; it’s a representation of the business’s core logic, rules, and behaviors. It’s the heart of DDD.
For example, in a shipping system, the domain model might include concepts like "Cargo," "Route Specification," and "Delivery History." These aren’t just tables in a database; they represent how the business thinks about shipping.
- Takeaway: Your model should reflect how the business works, not just how data is stored. A great domain model evolves as you learn more about the business.
4. Isolating the Domain
Keeping the domain layer independent ensures that business logic doesn’t get tangled up with technical concerns. To manage complexity, software can be divided into four layers:
- UI Layer: Manages interactions with users.
- Application Layer: Coordinates tasks and workflows but doesn’t contain business logic.
- Domain Layer: Contains the core logic and rules of the business.
- Infrastructure Layer: Handles technical details like databases and messaging.
- Takeaway: Focus your efforts on the domain layer; it’s the most critical part.
5. Model Expressed in Software
Entities and Value Objects
To model the domain, you need to understand the difference between entities and value objects.
Entities: These represent things in the domain that have a unique identity, like a "Customer" or "Order."
Value Objects: These are simpler and defined by their attributes, like "Money" (with an amount and currency). They’re immutable and don’t have their own identity.
Why it matters: Using the right tool for the job makes your model cleaner and easier to work with.
- Takeaway: Use entities when identity matters and value objects when it doesn’t.
Services
Sometimes, a behavior doesn’t naturally belong to any one entity or value object. That’s where domain services come in. These represent actions or processes in the domain.
Example: A "RoutingService" in a shipping system might calculate the best route for a cargo shipment.
Modules
Modules (also called packages) are used to organize related parts of the domain model into cohesive units. This makes it easier to understand and maintain the system.
Example: In a shipping system, you might have a "Shipping" module that includes "Cargo" and "Route" aggregates.
6. Life Cycle of a Domain Object
Aggregates
An aggregate is a cluster of related entities and value objects. The aggregate root acts as the gatekeeper, ensuring all rules within the cluster are followed.
Example: A "Cargo" aggregate might include its "Delivery History" and "Route Specification." The "Cargo" entity would be the root, controlling access and enforcing consistency.
Factories
Factories are responsible for creating complex objects and ensuring they’re properly initialized. This keeps the construction logic separate from the domain.
Example: A "CargoFactory" could create a "Cargo" object with all the necessary details, like a route and delivery deadline.
Repositories
Repositories handle the retrieval and storage of aggregates. They provide a clean way to abstract data access, so the domain layer doesn’t have to worry about databases.
Example: A "CargoRepository" might offer methods like findById(cargoId) or save(cargo).
Mapping Models to Databases
Designing relational models to support a domain-driven approach can be tricky.
- Avoid designing your domain model around database constraints.
- Use ORM tools to reduce friction between the domain model and the database.
- Takeaway: Let the domain model drive the database design, not the other way around.
7. The Cargo Example
The cargo example brings everything together. In a shipping system, "Cargo" is a central entity. The model includes:
- Aggregates: "Cargo" is the root, with related objects like "Route Specification."
- Services: A "RoutingService" calculates optimal routes.
- Repositories: Handle storing and retrieving cargo data.
8. Breakthrough
In software, not every part of the system is equally important. The idea of breakthroughs is in those moments when your team finds a clear and simple design for the most critical part of the business, the core domain.
Think of it like this: If you’re designing a shipping system, the heart of the business might be the rules and processes for routing cargo. That’s where your energy should go. Less important areas, like generating invoices, can use off-the-shelf solutions or simpler designs.
How do breakthroughs happen? By working closely with domain experts, keeping the team focused on what matters most, and staying open to new insights.
- Takeaway: Not all problems are created equal. Invest your energy in the parts of the system that drive the most value for the business.
9. Making Implicit Concepts Explicit
Have you ever worked on a project where everyone seems to “just know” certain rules, but they’re never written down? These are called implicit concepts, and they’re trouble. They lead to confusion, bugs, and wasted time.
For example, let’s say your shipping system has an unwritten rule that cargo headed to certain destinations needs special approval. If this isn’t explicitly modeled, someone might forget it, and you’ll end up with cargo in the wrong place or worse, in legal trouble.
The solution? Bring those hidden rules into the light. Give them a name, make them part of the domain model, and include them in the ubiquitous language so everyone is on the same page.
- Takeaway: Don’t assume everyone knows the rules. Make them explicit in your model and your communication.
10. Supple Design
A supple design is one that’s easy to work with, flexible enough to adapt to change, and clear enough that it almost feels natural. To achieve this, we can follow these principles:
- Intention-Revealing Methods: Methods and objects should make their purpose obvious. For example, instead of
processCargo(cargoId), use something likeassignRouteToCargo(cargoId, destination). - No Hidden Surprises: Functions should do what they say and nothing more. Avoid side effects like updating unrelated data.
- Logical Grouping: Keep related concepts together and separate the ones that aren’t. This keeps your design clean and intuitive.
11. Applying Analysis and Design
A great model doesn’t just pop out of thin air. It’s the result of ongoing collaboration between developers and domain experts. Here’s how:
- Talk to the experts. Learn how the business works by asking questions and observing.
- Sketch it out. Turn what you’ve learned into a draft model and test it against real-world scenarios.
- Iterate. No model is perfect on the first try. Refine it until it truly fits the business.
- Takeaway: Think of your design as a conversation between you and the business: a back-and-forth process that leads to better understanding and better software.
12. Relating Design Patterns to Models
Connecting DDD with classic design patterns shows how these tools can make your domain model stronger and easier to use.
- Factories: When creating something complex, like a new "Cargo" object, a factory ensures it’s done consistently and correctly.
- Repositories: Instead of worrying about database queries, a repository gives you simple methods like
findByIdorsave. - Strategy: This pattern lets you swap out behaviors. For instance, you might use different routing strategies for different types of cargo.
- Specification: Encapsulates rules or criteria. For example, a “DeliveryWithinTimeFrame” specification could check whether a cargo delivery meets its deadline.
13. Refactoring Toward Deeper Insight
As you learn more about the domain, your model should evolve to reflect those insights. Refactoring isn’t just about cleaning up code; it’s about deepening your understanding of the business.
Key Refactoring Steps:
- Identify Ambiguities: Look for methods, classes, or relationships that are vague or overly complex.
- Redesign for Intent: Replace generic terms with methods that make the purpose specific.
- Test the Model: Apply the refactored model to real-world scenarios to ensure it aligns with business logic.
- Refine Ubiquitous Language: Update the shared vocabulary to reflect the refined model, ensuring everyone on the team is aligned.
...more to follow