architecturedomain-driven-designrdbmscqrsevent-sourcing

How to handle aggregates relationship in Event Sourcing properly?


When having a somehow "complex" domain model, it's unavoidable to have related entities (that's the sense of aggregate root). But how should I reconstitute relationships from events? Searching by other aggregate id in serialized events is obviously not an option.

I have an idea to use such DB structure (for example). It has aggregates tables with just id and foreign keys, to simplify retrieving all necessary events from different entities. Doesn't this break the ES principles?

enter image description here


Solution

  • DDD says that "rich domain model" consists from entities which handles business logic, and we work with them as they model the domain objects

    Yes, and that's still true when you use event sourcing.

    So I can imagine i.e. order.addItem(product) method, where OrderItem creating and making relationship with Order and Product.

    Aha, no -- not in either approach. If Order and Product are different aggregates, then order.addItem(product) is not a spelling that you would use. The order and the product don't share responsibility for the product's state. Or, put another way, we never nest aggregates roots inside each other.

    The usual spelling, per the rules of DDD, would be order.addItem(product.id). Note this important difference: changing an Order cannot change any of the details of a Product.

    It's part of the point: each bit of state in your domain has a single authority responsible for maintaining its consistency.

    Note: the spelling order.addItem(product) makes sense for a model where Product is an entity that is not an aggregate root, but instead is subordinate to Order (and more precisely, each Product is associated with exactly one order).

    But if Item has many-to-one relationship to Order, shouldn't it contain orderId instead? (Not Order containing list of ItemId). But that would mean it should be Item.AddToOrder(order.Id), which makes not so much sense.

    The short answer is that you get different spellings for the methods depending on how you decide to model the data, and which items are responsible for maintaining the integrity of aggregates.

    Working backwards - part of the motivation for aggregates at all (rather than just having one large consistency boundary around your entire model) is the idea of concurrent modification of different parts of the model. Whether or not OrderItem is a separate aggregate from Order is going to depend -- in part -- on how important it is that two different OrderItems can be modified concurrently.

    For cases like online shopping carts, that's probably not super important.

    For a setting where many parties are trying to modify the same order at the same time, something like

    OrderItem.create(order.id, product.id)
    

    Wouldn't be unreasonable.

    And still, aggregate root contains other aggregates, and OrderItem in that case is aggregate, not aggregate root or value object.

    An aggregate root has responsibility for one aggregate. That aggregate (which fundamentally is just "state") can conceptually be the current state of multiple entities subordinate to the root, each managing a specific part of the whole.

    And if I'm right, aggregate root contains business logic to control inner aggregates, so we still need to build aggregate root which contains other entities in it.

    "Inner aggregate" doesn't make sense -- aggregates don't nest. Entities nest, with the outermost entity playing the role of the aggregate root.

    So, how do we build nested entities?

    Let's step back, and look at how we typically create a single entity. We run some query (like getById) to get the State that we had previously saved.

    Factory {
        Entity fromState(currentState) {
            return new (entity);
        }
    
        State fromEntity(theEntity) {
            return theEntity.getState();
        }
    }
    

    Nested entities work the same way, with the subordinate entities taking over part of the work. For an Order, it might look like...

    Factory {
        Order fromState(currentState) {
            List<OrderItem> items = ...
    
            for (State.Item itemState : currentState.items()) {
                OrderItem orderItem = OrderItem.from(itemState)
                items.add(orderItem)
            }
    
            return new Order(items);
        }
    }
    
    Order {
        State getState() {
            State currentState = State.EMPTY;
    
            for(OrderItem orderItem : this.items) {
                currentState = currentState.addItemState(orderItem.getState())
    
            return currentState
        }
    } 
    

    When we use event sourcing, the bit that changes is that we use collections of events instead of state.

    Factory {
        Order fromEvents(history) {
    
            // The one tricky bit -- the history we will be looking
            // at is a mix of histories from all of the entities that
            // coordinate the changes to the aggregate, so we may need
            // to untangle that.
    
            Map<OrderItemId, Events> = itemHistories
            for (Event e : history )
                items.put(e.orderItemId, e)
    
            List<OrderItem> items = ...
    
            for (Events events: itemHistories.values) {
                OrderItem orderItem = OrderItem.from(events)
                items.add(orderItem)
            }
            return new Order(items);
        }
    }
    
    Order {
        List<Event> getEvents () {
            List<Event> events = new List();
    
            for(OrderItem orderItem : this.items) {
                events.addAll(orderItem.getEvents())
            }
            return events
        }
    }