Thursday, May 30, 2013

Domain-Driven Design: Referencing Aggregates and Using DTOs

After seeing and reviewing some existing systems, I've been thinking about how to improve the way I practice domain-driven design. In this entry, let me talk about two things that I think helps when implementing DDD:

  • Reference other aggregates by ID.
  • Use data transfer objects.

Reference Other Aggregates by ID

Let aggregates reference other aggregates by ID (identity), not the aggregate itself.

Vaughn Vernon has written about this.

It's quite often that I'd model aggregates and reference other entities/aggregates. But this makes the system more difficult to maintain, as an aggregate would need to know every other entity/aggregate. Take the order and product domain model as an example.

We have an order entity that has one-or-more order items. The order entity forms the aggregate root. Each order item would refer to a product, a quantity, and a total price.

Just to help paint a better picture, we would also have a repository for order (OrderRepository) and a repository for product (ProductRepository).

Usually, we would have the order item reference a product by type. Something like:

class Order {
  private Long id;
  private List<OrderItem> items;
  . . .
}

class OrderItem {
  private Product product;
  private int quantity;
  private Money unitPrice;
}

class Product {
  private Long id;
}

Instead of referencing product by type, we can reference it by ID, like this:

class Order {
  private Long id;
  private List<OrderItem> items;
  public Long id() { return this.id; }
  public List<OrderItem> items() { return Collections.unmodifiableList(this.items); }
  . . .
}

class OrderItem {
  private Long productId;
  private int quantity;
  private Money unitPrice;
  . . .
}

class Product {
  private Long id;
  . . .
}

NOTE: I'm using the Long type for simplicity here. Otherwise, a domain-related value object like ProductId could be used.

NOTE: Notice how getters are not used to expose read-only properties in the Order entity. This seems to be a pattern to help developers get off the JavaBean/getter-setter mindset, and stay with a domain object/entity mindset.

This allows a cleaner implementation, since the order aggregate would not be referencing the product. This also makes the ProductRepository more useful. Otherwise, products would have been retrieved using orders.

If you happen to be using JPA for persistence, you can use the @ManyToOne annotation and specify the targetEntity element:

@Entity
class Order {
  @Id
  private Long id;
  @OneToMany
  private List<OrderItem> items;
  . . .
}

@Entity
class OrderItem {
  @ManyToOne(targetEntity = Product.class)
  private Long productId;
  private int quantity;
  private Money unitPrice;
  . . .
}

@Entity
class Product {
  @Id
  private Long id;
  . . .
}

NOTE: Notice that a uni-directional relation between order and order items is used (not a bi-directional relation). This helps make the implementation simpler, as the order item does not reference its parent order.

The above results to the following DDL.

create table "order" (. . .);

create table order_item (
  . . .
  product_id bigint,
  . . .
);

create table product (
  id bigint generated by default as identity (start with 1),
  . . .
);

. . .
alter table order_item
    add constraint FKxxxxxxxxxxx2
    foreign key (order_id)
    references order;
. . .
alter table order_item
    add constraint FKxxxxxxxxxxx7
    foreign key (product_id)
    references product;
. . .

Given the above simple adjustment, we now have an order aggregate that does not directly reference the product aggregate. This creates a cleaner separation and a more modular domain model. It might even be possible to have an order that can reference a different type of product (as long as the ID type is compatible). This might allow a more re-usable order domain model.

Use Data Transfer Objects

Use simple structures (DTOs) to transfer data from the domain model to an interface and vice-versa.

I often see the domain model entities with all of its fields being modifiable via getters and setters. Also, they have zero-arguments constructors. This makes it difficult to apply invariants or business rules (as the entity can be in an invalid state). And the usual excuse I get for this is because they need to create a UI that allows the fields/properties to be modifiable.

For me, I'd rather have an entity that is always valid.

To preserve the business rules or invariants of the domain model, we cannot afford to have all of the entity fields being modifiable. This is where DTOs are needed.

package order.domain.model;

@Entity
class Order {
  . . .
  public int addItem(Long productId, int quantity, Money unitPrice) {
    . . .
  }
}

@Entity
class OrderItem {
}

. . .

package order.interfaces.dto;

class OrderDTO {
  private List<OrderItemDTO> items;
  public List<OrderItemDTO> getItems() {...}
  public void setItems(List<OrderItemDTO> ...) {...}
}

class OrderItemDTO {
  private Long productId;
  private int quantity;
  // getters and setters
}

The UI shall use the DTOs when displaying and receiving inputs. Once the data is captured in DTOs, assemblers are used to apply the changes to domain objects. This pattern can be seen in the dddsample project.

package order.interfaces.assembler;

class OrderAssembler {
  . . .
  public OrderDTO toDTO(Order order) {
    OrderDTO dto = new OrderDTO();
    . . .
    dto.setOrderDate(order.orderDate());
    dto.setEntryDate(order.entryDate());
    dto.setCustomerId(order.customerId());
    . . .
    return dto;
  }
  . . .
  public Order fromDTO(OrderDTO dto) {
    Order order;
    if (dto.getId() != null) {
      order = orderRepository.find(dto.getId());
    } else {
      order = new Order(..., dto.getCustomerId());
    }
    . . .
    Long productIds[] = dto.getProductIds();
    // Can use IN operator to get products with given IDs
    Map<Long, Product> products = productRepository.find(productIds);
    for (OrderItemDTO itemDTO : dto.getItems()) {
      Product product = products.get(itemDTO.getProductId());
      order.addItem(
          item.getProductId(),
          item.getQuantity(),
          product.getPrice());
    }
    return order;
  }
  . . .
}

This has helped me understand DDD implementations better. I hope it helps others too.

10 comments:

  1. Your post are brilliant, I would imagine that you should figure much higher up in the search results :) I enjoyed reading your DDD post !! Thanks

    ReplyDelete
  2. Consider the use of CQRS along with DDD, this way you can directly construct the DTO's used in the queries from your persistence storage often your RDBMS. I have written a short post on such a approach here http://noobjuggler.blogspot.in/2011/10/mind-map-for-ddd-cqrs-base-project.html

    ReplyDelete
  3. One comment about OrderAssembler class: for-loop of OrderItems is N+1 select issue. Consider one query outside for-loop to retrieve all OrderItems using a left outer join.

    ReplyDelete
    Replies
    1. Thanks for pointing that out. I've updated the sample. Your suggestion of using a left outer join will work too.

      Delete
  4. Lorenzo, I replicate your example, in which you explain how to achieve referencing other aggregates only with his id.
    Although the code has compiled and generates tables correctly, I got the following error when trying to instantiate OrderItem.

    java.lang.IllegalArgumentException: Can not set java.lang.Long field com.aggregates.model.OrdenItem.ordenId to com.aggregates.model.Orden
    at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167) ~[na:1.8.0_51]
    at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171) ~[na:1.8.0_51]
    at sun.reflect.UnsafeObjectFieldAccessorImpl.set(UnsafeObjectFieldAccessorImpl.java:81) ~[na:1.8.0_51]


    What I gather is that when instantiating OrdenItem, JPA assumes that the attribute must be the same type of aggregate. I had to remove annotations in OrdenItem
    @ManyToOne (TargetEntity = Orden.class, optional = false)
    @ManyToOne (TargetEntity = Product.class, optional = false)
    to make this work, but I lost some unidirectional foreign key in database.

    I searched how to implement the recommendations of Vaughn Vernon, even in the code of his book, but his examples do not use the JPA. Practically, The only example that I found was this, other references suggests that JPA can only set these references using the same type of aggregates as attribute.

    The consultation is implemented with SpringData

        @query ( ""
                + "SELECT b"
                + "FROM OrdenItem b"
                )
        List findByYYYY ();


    I use the following technologies.

    Spring Data
    Hibernate 4.3.11.Final
    SQL Server 2012

    ReplyDelete
    Replies
    1. Thanks for asking Albam.

      Yes, if I remember correctly, the JPA mapping shown here only resulted into the desired DDL (with FK constraint). But, after that, the code couldn't be run. So, we had to change it back to reference the entity type.

      We ended up with a separate DDL with FKs if needed (not auto-generated by JPA), and Java code that referenced by ID (e.g. Long, or ProductId) without JPA relationship mapping.

      HTH (Hope this helps)

      Delete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Lorenzo, thank you. Your code is very useful for me. This works with both the @ManyToOne and the @ManyToMany associations.

    @ManyToOne(targetEntity = Bird.class)
    @JoinColumn(name = "ksf_tsb_id")
    private Long birdId;

    @ManyToMany(targetEntity = Bird.class)
    @JoinTable(name = "test_leaf_bird",
    joinColumns = @JoinColumn(name = "tsd_tsf_id",
    referencedColumnName = "tsf_id"),
    inverseJoinColumns = @JoinColumn(name = "tsd_tsb_id",
    referencedColumnName = "tsb_id"))
    private List< Long> birdIds;

    ReplyDelete
  7. At this point, depending on the complexity of the MEMS component under development, the entire team may consist of at least four people. The main goals of the engineering team during this stage are to develop the technology and stabilize the function of the device. The main goals of the business team during this stage are to secure financing and validate the product and market opportunity. what is thought leadership

    ReplyDelete