Domain-driven Design Concepts Reference

For my primary software project at work, the solution is structured based on concepts that are part of domain-driven design (DDD). It seems like each time I revisit the concepts, they jell a little more. Given that (1) my company will be hiring another software developer soon and (2) there’s a low probability of that individual being versed in DDD, I wanted to review the concepts.

This post mainly serves as a quick-reference for the main concepts and terminology used in DDD. It’s not exhaustive and probably isn’t suited to someone trying to learn this approach from scratch. When I think about creating reference documents, there’s always that question of where the information should live. I’m following Jeff Atwood’s suggestion of doing it in public (i.e., my blog), because “if nobody knows you did {x}, did you get all the benefits of doing {x}?”

Premise

The problem domains where software is used are typically complex (e.g., banking, logistics). DDD is rooted in understanding that domain and developing (usually iteratively) models of that domain, thus defining the actors, interactions, and layers.

Throughout the entire process of model design, coding, and documentation, DDD stresses the use of ubiquitous language so that everyone working on the project describes the domain using the same terms. If the people developing the code speak the same language as those using it, it reduces the likelihood of contextual mistakes and misunderstandings. Some examples include:

  • In a veterinary scheduling application, is the patient the paying customer (i.e., a human) or the animal being treated?
  • Is the interface used to detect radioactivity called a detector or a multi-channel analyzer?

Layered architecture

Having tiered architecture isn’t anything new to software system design. The most common layers found in DDD-modeled systems are:

  • User interface
  • Application
    • Responsible for directing domain objects
    • Thin; no business rules
    • Coordinates tasks and delegates work
  • Domain
    • “Heart” of the system
    • Business rules
    • Represents concepts in the problem domain
  • Infrastructure
    • UI widgets
    • Message passing service
    • Persistence technology

Each layer knows of and communicates with any of the layers below it.

Associations

To describe how objects relate to one another, we use associations.

  • Direction — unidirectional (X knows about Y, but Y doesn’t know about X); bidirectional (X and Y both know about each other)
  • Multiplicity — one-to-one (one car has one engine), one-to-many (one car has four wheels)

Ideally these should be as simple as possible; that is, eliminate non-essential associations, reduce multiplicity, and impose a traversal direction if able to do so.

Entities

Think of an entity as a class; it’s a reference object that has a life cycle (i.e., holds state). The distinction is that each instance must be uniquely identifiable; that is, an object is defined by its identity, not its collection of attributes.

Examples:

  • There are several black 2015 Chevy Impalas, but their VINs allow them to be uniquely identified.
  • If you are modeling venue booking, for a specific event, the ticket holder maybe assigned seat 14-F, but for a general admission event, the ticket holder doesn’t need to know the specific seat.

Value objects

Unlike entities, value objects are defined by their attributes alone. Use value objects when you care about what they are, not who or which.

Value objects are:

  • Transient and usually immutable
  • Typically passed as arguments or messages
  • Similar to the flyweight pattern

An example is a blue crayon. You don’t really care which blue crayon it is; it only matters that it’s a crayon of the correct color.

Services

This is where DDD differs a bit from typical object-oriented design. I think of the nouns in the problem domain as entities or value objects, and the verbs as services. They are the doers of the system that offer an interface for others to exercise.

  • Don’t make entities or value objects be responsible for domain functionality (i.e., business logic).1
  • Use services for operations that relate domain concepts that aren’t natural parts of the entities or value objects.
  • Services are stateless, such that any client can use any instance of a service.
  • Each layer can have services. For example, suppose we’re modeling a money transfer in a bank:
    • Application level – digest some XML input, coordinate with domain services, start an e-mail notification request
    • Domain level – handle the business logic of the transfer (update ledgers, etc.)
    • Infrastructure – send an e-mail to the account owner

Modules

A module is an encapsulated unit that makes higher-level concepts easier to associate. Modules are like the chapters of a book.

When composing modules, strive for:

  • Loose coupling between modules (i.e., modules should mostly work independently of one another)
  • High cohesion within modules (i.e., tasks done within the module strongly relate to one another)

Aggregates

An aggregate (sometimes called an aggregate root) is a cluster of associated objects that you treat as a single unit for the purposes of data and state changes.

For example, suppose there is a car with four wheels. As mentioned above, we need to uniquely identify the car, so it’s an entity. It would also be helpful to identify the wheels so that we know where they are located on the car (for tire rotations); therefore, wheels are also entities. The problem is that the software will likely never need to find a wheel by ID; only the car cares about the references to the wheels. In this case, the aggregate root is the car. When we read the car information from the database, the wheel information is also populated as part of a unit.

Here are some more properties of aggregates:

  • Aggregate roots are uniquely identifiable in the system; however, sub-entities only need to be uniquely identifiable with respect to the root. For example, there can’t be multiple cars with the same VINs, but the tires can be labeled 1 through 4.
  • They are responsible for checking invariants (i.e., a condition that must stay true for an object to be in a valid state).
  • Nothing outside the root can have a reference to internal entities. Use value objects instead if you need to provide this state.
  • Database queries work with the aggregate, not the internal pieces.
  • Deleting the root means also deleting things within the aggregate boundary. For example, removing a specific car also removes the four tires from the system as well.

Factories

A factory provides encapsulation when the creation of an object (or aggregate) becomes too complicated or reveals too much internal structure.

  • If you have class constructors calling other constructors, you should probably be using a factory.
  • Factory method signatures have return types that are interfaces, but they return concrete instances.

Repositories

repository allows you to reconstitute an object instance and abstracts away the details of how or where this happens.

Repositories are also a convenient way of handling concurrency issues. Let’s say that a customer entity has a list of purchases. Presumably the point-of-sale system modifies that list in some way (adding recent purchases or handling returns). A reporting system is also interested in that customer, so it reads the list of purchases into memory. At this point, the point-of-sale system is responsible (i.e., high coupling) for communicating whenever something changes so that the reporting system can stay current. Instead, the customer can offer up the list when it is requested (by either system) by using the purchase repository to manipulate the single source of truth (i.e., the database).

Bounded contexts

Some nouns take on different meanings depending on their context. For example, to the billing department, a customer is someone with a billing address, balance due, etc. However, to the marketing department, a customer is someone with buying patterns, brand preferences, etc., and his payment history may be of little value. In this case the various departments provide bounded contexts in which the customer entity takes on different meanings and is involved in different workflows.

The goal is to prevent unifying everything into a god object. You should define the context within which a model applies.

To visually make sense of the contexts, use a context map to show the relationships between bounded contexts so that you can communicate the design more effectively with others.

Shared kernel

If you have entities that occupy multiple bounded contexts, there may be some overlap. For example, in the diagnostic medical device my company sells, there are two types of tests: blood volume, and quality control (QC). Each type of test lives in a different bounded context, because running a QC test is significantly different than running a patient study. However, both tests have a collection of samples, an outcome (e.g., in progress, completed), etc. Those common attributes are the shared kernel, which in my case is implemented via an abstract Test class that resides in a high-level assembly.

Anticorruption layer

With legacy systems, there’s often a need to connect the new, well-designed subsystem to an older, poorly-modeled system. The anticorruption layer acts as an adapter between the two so that both can function without adversely affecting one another (or the design).

Anemic and rich models

There are two schools of thought as to how to build out domain models.

  • Anemic — the entity or value object class simply holds state, and maybe has a few simple methods; the logic is implemented using services
  • Rich — the entity or value object class has both the state and the business logic

Martin Fowler prefers rich domain models, and calls anemic models an anti-pattern. I prefer anemic models, as they are more adherent to the Single Responsibility Principle and the Open/Closed Principle. They also have increased unit testing affordance.

Wrapping Up

That’s the nickel tour of the big moving pieces for domain-driven design. I’ve been practicing this approach for about six years now and it’s really grown on me. That being said, it took me a while to really wrap my head around how everything hangs, because the systems one would model are already inherently complex. Hopefully this post was a good refresher if you’re involved in DDD, or will spark your interest to learning more about it for upcoming projects.

References

Endnotes

  1. As Paul Rayner mentioned in his comment, my statement (now in strikethrough) did not align with Eric Evans’ definition of a service. There may be business logic that squarely lies with an entity or value object, so it should live there; however, “When a significant process or transformation in the domain is not a natural responsibility of an entity or value object, add an operation to the model as a standalone interface declared as a service.” (p. 106)

3 Comments

Comments are closed.