My TypeScript Software Design & Architecture book just prelaunched! Check out solidbook.io.
Close

Challenges in Aggregate Design #1 - Domain-Driven Design w/ TypeScript

Jul 25th, 2019 / 5 min read / Share / Edit on GitHub
In this series, we answer common questions in aggregate design. Here's an article orignating from the question, "How can a domain model reference a model from a different subdomain?"

Here's a question I got recently,

Q: How can a domain model reference a model from a different subdomain?

The person who asked the question also gave me a few details about their actual problem.

Order.create(userId) creates an order, which is only accepted if User.getReputation() is "trusted". A user's reputation is trusted if more than 10 products were shipped, otherwise it returns failure result.

OK, let's dive in.

I'm going to assume we're working on some sort of ecommerce platform containing a Users subdomain and a Shipping one.

User subdomain

In the Users subdomain, let's assume we have an aggregate root for User:

interface UserProps {
  email: UserEmail;
  password: UserPassword;
}

class User extends AggregateRoot<UserProps> {
  private constructor (props: UserProps, id?: UniqueEntityID) {
    super(props, id);
  }
  
  public create (props: UserProps, id?: UniqueEntityID): <User> {
    const guardResult = Guard.againstNullOrUndefinedBulk([
      { argument: props.email, arguementName: 'email' },
      { argument: props.password, arguementName: 'password' }
    ]);

    if (guardResult.isFailure) {
      return Result.fail<User>(guardResult.error);
    }

    return Result.ok<User>(new User(props, id));
  }
}

I was thinking about the user.getReputation() method. Does it make sense for that to be on the User entity?

Is reputation even a concern in the Users subdomain?

I personally don't think so.

Here's why.

Aggregate Boundaries

In Vaughn Vernon's DDD Book, he says:

“When trying to discover the Aggregates in a Bounded Context, we must understand the model’s true invariants. Only with that knowledge can we determine which objects should be clustered into a given Aggregate.” - Excerpt From: Vernon, Vaughn. “Implementing Domain-Driven Design.”

I think there's a slight confusion as to which entity the responsibility of calculating reputation belongs to.

It's our natural instinct to assume that everything is the responsibility of User, because Users are primarily how we think about people using our systems.

But in DDD, we have to think a little more granularly. Especially if there are several subdomains.

In the example given, reputation is apparently calculated from the User aggregate. However, the calculation can only be determined by counting the number of "products shipped".

Products are not within the User aggregate's consistency boundary. The User entity doesn't contain a reference to the collection of products. And it shouldn't need to.

Determine responsibility by assigning use cases to subdomains

Remember from the section titled "We discover use cases through conversation" from this article where we discovered that the Users subdomain is only responsible for Identity & Access Management and contains use cases like:

  • login(userEmail: UserEmail, password: UserPassword)
  • logout(authToken: JWTToken)
  • verifyEmail(emailVerificationToken: EmailVerificationToken)
  • changePassword(passwordResetToken: Token, password: UserPassword)

I think what we're missing is a Merchant aggregate in the Shipping subdomain like so:

type ReputationType = 'unranked' | 'poor' | 'good' | 'great';

interface MerchantProps {
  products: WatchedList<Product>;
  shippedProducts: WatchedList<Product>;
  reputation: ReputationType;
}

export class Merchant extends AggregateRoot<MerchantProps> {
  ...
}

From this subdomain, I would expect to see more use cases like:

  • shipProduct(product: Product)
  • getMerchantById(merchantId: MerchantId)

Calculating Merchant reputation

That solves the problem of us trying to figure out how to refer to an entity from a separate subdomain (which we usually do with application services / use cases in correct cases).

But now, how do we calculate ReputationType?

At first glance, the obvious approach would be to tally up all of the products shipped like so:

export class Merchant extends AggregateRoot<MerchantProps> {
  ...
  public getReputation (): ReputationType {
    const numProductsShipped = products.getItems()
      .reduce((curr, product) => !!e.shipped ? curr + 1 : curr, 0);

    if (shipped < 5) {
      return 'unranked'
    } else if (shipped >= 5) {
      // etc and so on.
    }
  }
}

But since Merchant has a 0-to-many relationship with Product, that unbounded relationship could end up spanning hundreds or thousands of products.

That would mean we would have to ensure we retrieve all products from the repo when we retrieve a Merchant... always.

Not ideal.

Here's a better approach.

Updating Reputation after every ProductShippedEvent domain event

Since Product is under the consistency boundary of Merchant, we could add a method to Merchant to mark a product as shipped.

export class Merchant extends AggregateRoot<MerchantProps> {
  public shipProduct (product: Product): void {
    
    // Add the item to shipped products so that the 
    // Product repo can save it as updated when we
    // persist the domain entity. 
    this.shippedProducts.addItem(product);

    // Create a domain event for the product shipment
    this.addDomainEvent(new ProductShippedEvent(product))
  }
}

And then, from the same subdomain, we could subscribe to the Domain Event.

// src/modules/shipping/subscribers

export class AfterProductShippedEvent implements IHandle<ProductShippedEvent> {
  private merchantRepo: IMerchantRepo;
  private productRepo: IProductRepo;

  constructor (merchantRepo: IMerchantRepo, productRepo: IProductRepo) {
    this.merchantRepo = merchantRepo;
    this.productRepo = productRepo;

    DomainEvents.register(this.onProductShippedEvent.bind(this), ProductShippedEvent.name);
  }

  private onProductShippedEvent (event: ProductShippedEvent) : Promise<any> {
    const numberShippedProducts: number = await this
      .productRepo.countNumberShippedProducts(event.product.merchantId);
    
    // Use a domain service to calculate the reputation.
    // We want to ensure that we encapsulate as much domain logic
    // in the domain layer as possible.

    let newMerchantReputation: ReputationType = MerchantReputationService
      .calculateReputation(numberShippedProducts);

    // Set the reputation
    await this.merchantRepo
      .setMerchantReputation(event.product.merchantId, newMerchantReputation);
  }
}

This is what we call eventual consistency and it's within the realm of topics related to but not constrained to CQRS.



Discussion

Thanks for reading! Share this article if you found it useful.


0 Comments

Be the first to leave a comment

Submit

Stay in touch!



About the author

Khalil Stemmler

Khalil Stemmler is a Developer / Designer and co-founder of Univjobs. He frequently publishes articles about Domain-Driven Design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Domain-Driven Design



You may also enjoy...

A few more related articles

Handling Collections in Aggregates (0-to-Many, Many-to-Many) - Domain-Driven Design w/ TypeScript
Jul 25th, 2019 / 10 min read
In this article, we discuss how we can use a few CQS principles to handle unbounded 0-to-many or many-to-many collections in aggre...
How to Design & Persist Aggregates - Domain-Driven Design w/ TypeScript
Jul 24th, 2019 / 28 min read
In this article, you'll learn how identify the aggregate root and encapsulate a boundary around related entities. You'll also lear...
An Introduction to Domain-Driven Design - DDD w/ TypeScript
Jul 30th, 2019 / 12 min read
Domain-Driven Design is the approach to software development which enables us to translate complex problem domains into rich, expr...
Implementing DTOs, Mappers & the Repository Pattern using the Sequelize ORM [with Examples] - DDD w/ TypeScript
Jun 20th, 2019 / 14 min read
There are several patterns that we can utilize in order to handle data access concerns in Domain-Driven Design. In this article, w...

Want to be notified when new content comes out?

Join 2000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates