DDD for Rails Developers. Part 3: Aggregates.

This entry is part 2 of 3 in the series DDD for Rails Developers

DDD for Rails Developers

My Previous Articles About DDD for Rails Developers

In Part 1, I talked about using the layered architecture for tackling domain complexity. I showed a few typical violations of the layered architecture and gave some advice on how to fix them.

In Part 2, I started talking about the building blocks of Domain Driven Design. I wrote about an important distinction between Entities and Value Objects. I also gave some advice on how to implement Value Objects in Rails.

Aggregates

This time I’d like to go into another building block of Domain Driven Design. I’d like to talk about Aggregates.

We’ve all experienced this situation before:

You start with nicely designed groups of objects. All the objects have clear responsibilities, and all interactions among them are explicit. Then, you have to consider additional requirements, such as transactions, integration with external systems, event generation. Satisfying all of them and not making all the objects interconnected is a nontrivial task. What usually happens is database hooks, conditional validations, and remote calls are added on an ad hoc basis. The result is more connections among objects. Hence, the boundaries of object groups become fuzzy and enforcing invariants becomes harder. Remember all the cases when you were thinking, “Maybe I need to reload this object?” It indicates that your objects are interconnected, and you cannot reason about your code with confidence. Instead, you just guess.

Defining Aggregates is a good remedy for the described situation.

  • “An Aggregate is a cluster of associated objects that are treated as a unit for the purpose of data changes.” (see Resources)
  • An Aggregate consists of a few Entities and Value Objects, one of which is chosen to be the root of the Aggregate.
  • All external references are restricted to the root. Objects outside the Aggregate can hold references to the root only.
  • Accessing other members of the Aggregate happens through the root. Therefore, nobody (outside the Aggregate) should hold references to those objects.
  • As all external objects can hold reference only to the root, enforcing invariants becomes easier.
  • Aggregates help to reduce the number of bidirectional associations among objects in the system because you are allowed to store references only to the root. That significantly simplifies the design and reduces the number of blindsided changes in the object graph.

Example

It may sound too abstract, so I’d like to show you an example. I’m going to model an online bookstore. The main responsibility of the model will be selling and shipping books. Hopefully the example will bring some clarity to the definition of Aggregates and will demonstrate how they can be implemented in Rails.

Sketch

This is a sketch illustrating all the classes that will form the model.

Sketch

As you can see, I have:

  • Entities: Order, Item, User, Book, Payment
  • Value Objects: Address
  • Services: ShipmentService, PaymentService
  • View: OrderPresenter

Defining Aggregate Boundaries

Now, after I am done with sketching, I can establish Aggregate boundaries and choose roots.

There are a few rules of thumb to use:

  • Entities forming the parent-child relationship, most likely, should form an Aggregate. In this case, the parent class becomes the root.
  • Entities that are semantically close to each other are good candidates for forming an Aggregate as well. For instance, Book and Payment have no obvious connections with each other. Having them inside an Aggregate is awkward. On the other hand, Order and Item are closely related. Thus, we should consider putting them inside an Aggregate.
  • If two Entities have to be modified inside a transaction, they should be parts of the same Aggregate.

Note:
These rules should just help you get started. After your first sketch you should look at all the invariants that need to be maintained and finalize your Aggregate boundaries based on them.

As you may have already guessed, Order and Item form an Aggregate. What other Entities should we include? Including Book does not make much sense because I can easily imagine clients using Book without Order (e.g. you may need to display the list of all available books in the store).
Including User into the same Aggregate with Order is not the best idea either. Just imagine if User becomes the root, you will have to access all the orders of a user through the user itself. Furthermore, updating two orders of the same user simultaneously will be tricky. Clearly neither Book nor User should be a part of the Aggregate.

The situation with the Payment class is different. Conceptually, Payment is an important part of Order. Also, you cannot simultaneously pay for an Order and modify it. It’s decided, Payment becomes a part of the Aggregate.

An updated sketch with the defined boundary:

Sketch 2

Implementation

Let’s get started with the code. First, let’s define the Book and User classes:

There is nothing really interesting here. There are two Entities (Book and User) and one Value Object (Address). It gets interesting when we implement the Order and Item classes:

That is how the creation of an order may look like:

Reading the same order from the database may look like this:

There are a few important things I’d like to point out:

  • All items and payments are created through an instance of Order.
  • I don’t update attributes of an Order directly.
  • I don’t use DataMapper methods directly. I wrote a few methods to decouple our model from DataMapper as much as possible.
  • There are no bidirectional associations. Every order knows about its items, but the items don’t have any references to their orders. Why? Because they don’t need to. Order is the root; therefore, any client can get an item only through its order. This means that an item’s order will always be known. * Bidirectional associations are a bad practice established in the Rails community. If you can avoid it, please, do it. *
  • A user does not know about its orders. Apart from the fact that it’s an unnecessary bidirectional association, it also makes testing much harder.

Don’t believe me? Take a look at this line of code:

It looks so simple. Some people might even say that it’s nicer than this one:

However, if you try to stub it in your test you may write something similar to this:

Now, compare it with the stubbing of Order.active_orders(user):

The second test is much easier to read and understand. In addition, in large applications such models as User tend to grow. In a few years, you may get the User class having many orders, promotions, friends, wish lists etc.

Accessing Our Model in View

Please, don’t be mistaken. The fact that Order is the root of the Aggregate does not mean I cannot access its payment or items. Of course, I can. The only rule is not to store references to those objects. For instance, I’d probably need a presenter. As presenters store references to the objects they present, creating ItemPresenter or PaymentPresenter is a violation of the Aggregate boundary. Instead, we can create OrderPresenter and pass an instance of Order to it. OrderPresenter can access the order’s items or payment through the order itself.

Invariants

Remember that Aggregates are not only about access restriction. They also define the boundaries of invariants and transactions. There is a common opinion that those invariants must be enforced by the root. It’s also common to make the root responsible for managing all transactions. Although sometimes it may make perfect sense, there are lots of situations when it does not. Just to give you an example, let’s take a look at the PaymentService class:

PaymentService has several responsibilities. First, it masks the credit card. Next, it updates the database to mark the order as paid. After that, it makes a remote call to some external service that processes credit card transactions. Also, it wraps everything into a transaction. PaymentService ensures an important invariant that updating the status of the order and making the remote call must be done together. An alternative would be to make Order responsible for calling external services. The result would be a violation of Single Responsibility Principle and a few nasty dependencies of Order on external payment systems.

Wrapping Up

Sometimes invariants need to be applied not to discrete objects, but to clusters of objects. Defining Aggregates and restricting access to Aggregate members is an arrangement that makes enforcing all the invariants possible.

Resources


* Aggregate | Domain-Driven Design Community

DDD for Rails Developers

<< DDD for Rails Developers. Part 1: Layered Architecture.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Dan

    Very interesting article.

    Quick question – Where is the Enum from? Is that part of ruby or rails? I’ve never seen an Enum class but have always wanted it in ruby/rails.

    • http://victorsavkin.com Victor Savkin

      Thanks for you comment Dan. I highly recommend you to check out DataMapper. In my view, it’s a better persistence framework that AR. For example, it has the Enum type DataMapper – DM-Types. If you are stuck with AR, validates_inclusion_of will do the job.

      Cheers,
      Victor

  • http://rinaldifonseca.com RInaldi Fonseca

    How can write this using active record insteand of data mapper?
    Because I dont want to inherit from active record. I want just include ActiveModel::Validations but avoid this: http://boblail.com/post/2528265548/using-validates-associated-with-composed-of-and

    • http://victorsavkin.com Victor Savkin

      Thank you for your comment. Honestly, I’m not sure how to solve your problem in an elegant way. I’d probably just override freeze:

      def freeze; end

      Cheers,
      Victor

  • Luis Hurtado

    Another great article Victor!. Keep them going. I bought the book you recommended in the first article and I personally the book and your articles really improved my skills as developer. Again, thanks a lot for that.

    I have a simple question about PaymentService: why a model instead of a class?

    • http://victorsavkin.com Victor Savkin

      Thank you Luis. I made a module just to emphasize that PaymentService is stateless. I’s not going to mix it into another class.
      Cheers,
      Victor

  • gumnaam

    Making a buyer, item, payment etc are these responsibility of Order. Shoud it not be applications responsibility?

    • http://victorsavkin.com Victor Savkin

      In my view, it’s alright when an Aggregate Root is responsible for building its children. Item (which I should have probably called OrderItem) and Payment are parts of the Order Aggregate, so I think it’s fine that Order can build those objects. Buyer, on the other hand, is a separate Aggregate, and it’s not built by Order.

      Cheers,
      Victor

  • Hong

    Excellent read. I love the idea of using services to implement domain logic. The architecture is easy to implement and also faster in code execution than the DCI role-based approach.

    Although, after some practicing, I have found it difficult to completely decouple services from controllers. For example, services often requires cookies/sessions/params to operate business logic, but passing those objects directly into services class feels inappropriate. Another example is that controllers and views often benefit a lot from business-related helper methods or filters, which should be defined in services.

    I try very hard to separate controller and services, though sometimes I feel I am complicating the code base for no good reason. I wonder if you have any insights on the subject.

    • http://victorsavkin.com Victor Savkin

      Thank you for your comment. I’m sorry for not answering earlier.

      1. Services can be created in the domain and application layers. Domain layer services shouldn’t know anything about the view (no cookies, no form data). They should operate on domain objects. On the other hand, application level services can be aware of Rails and the view. They are interfacers between the domain and Rails. They take simple data structures (hashes or structs) and return simple data structures back. Thus, it’s alright to pass some hashes (like a subhash of params) to this kind of services.

      2. I agree with you that it’s not trivial to separate controllers and services. As I’ve already mentioned, building high-level application level services/facades is the way I prefer doing it these days.

      Cheers,
      Victor

  • Sebastian

    Great article!
    You write in plain English with good, distilled information – traits that make a good technical communicator. Many thanks.