1

Scenario

Let's say that there is an entity in my domain called Shop. As a shop owner (user), I am able to create rewards that customer users will be able to exchange.

It also could be possible to have some kind of reward creation policy (eg. a maximum number of rewards that as store owner I can create: if that value is 4 -> I won't be able to add more than 4 rewards to my store).

Reward creation (for a shop)

Considering the following 1-to-N relationship: Shop (1)->(N) Reward, what would be the most appropriate way to manage rewards (taking into account that Shop would have a list of Reward's)?

In case there was any reward policy that could be somehow abstracted out by a IRewardCreationPolicy interface and used where needed:

interface IRewardCreationPolicy {
   isAllowed(shop: Shop);
}

Below are some design alternatives that I am taking into consideration.

 Alternative #1

Reward management is fully handled by the Shop class:

class Reward {
   /* props, methods... */
}

class Shop {
   private id: number;
   private name: string;
   private rewards: Reward[];
   
   constructor(rewardCreationPolicy: IRewardCreationPolicy) {}

   addReward(reward: Reward): void {
      if (!this.rewardCreationPolicy.isAllowed(this)) {
         // throw exception
      }
      this.rewards.push(reward);
   }

   removeReward(id: number): void {
      /* Remove reward with the given id */
   }
}

class AddRewardUseCase {
   constructor(shopStorage: IShopStorage) {}

   execute(request: AddRewardRequest) {
      const shop = this.shopStorage.findById(request.shopId);
      shop.addReward(request.reward);
      this.shopStorage.update(shop);
   }
}

 Alternative #2

Reward management is fully handled outside the Shop class:

class Reward {
   /* props, methods... */
}

class Shop {
   private id: number;
   private name: string;
   private rewards: Reward[];
   
   constructor() {}

   getRewards(): Reward[] {
      return this.rewards;
   }
}

class AddRewardUseCase {
   constructor(
      shopStorage: IShopStorage,
      rewardCreationPolicy: IRewardCreationPolicy
   ) {}

   execute(request: AddRewardRequest) {
      const shop = this.shopStorage.findById(request.shopId);

      if (!this.rewardCreationPolicy.isAllowed(shop)) {
         // throw exception
      }

      shop.getRewards().push(request.reward);
      this.shopStorage.update(shop);
   }
}

Questions

In this case there is class (Shop) that holds a list of objects (Reward's). Should any possible action on this list be encapsulated inside the holder class? In this case, I assume that if we had any other list aside from the rewards it would be also managed by the holder itself too.

I understand that we would be talking about encapsulation here, so in that case I guess the Alternative #1 would be the best choice. What are your thoughts on this?

2 Answers 2

1

It depends, and more specifically it depends whether or not it makes sense that a RewardCreationPolicy is linked to a Shop or not.

Let's substitute this scenario with a simpler example:

  • Your problem is that you need to create a list of a random subset of another list. You instanciate your target list as empty, and add elements based on a random predicate function. This predicate have no reason to belong to a list implementation. Most lists don't need predicates to be built.

  • Your problem is that you need to provide a list implementation that size is limited to 4, and being used as a normal list by your program. The list must be able to know if it adds an element to itself or not: the predicate is part of the custom list implementation.

So based on your usage of Shop and RewardCreationPolicy, and the link between them, it makes sense or not to tie them by implementation.

Additional context is needed, but based on the existing context, making Shop responsible does provide stronger encapsulation and does sound more appealing. It wouldn't be so if you realize after a while there is several more predicates you need to add to it to make the rewards being managed properly.

1

From your example, I think it is pretty clear that Rewards are part of the transactional consistency boundary of Shop aggregate.

In DDD lingo, the Aggregate should preferably satisfy all invariant rules at all time. To ensure that a Shop object meets all business rules (including the limit of 4 rewards) at all times, you have no alternative but to manipulate rewards via the shop object.

How you establish, validate, and enforce the invariants (like the check for the current number of rewards vs. limit) is flexible and can change over time. But what cannot change is the fact that these invariants should be fully satisfied by the time you persist the shop object.

The only condition that may lead you in a different path, one of eventual consistency/validation, could be if you end up having to load a huge number of reward objects as part of your shop object. Since you are establishing a clear limit in your case, and that limit is pretty small, this condition may not be applicable.

2
  • I see! So what do you think would be the way to go if rules change and the possibility of ending up having a big number of rewards objects exists? I am wondering if breaking the shop->reward[] aggregate and managing a shop's reward list independently would make more sense in that case... what would be your point of view in this scenario you very well brought up? Commented Sep 10, 2019 at 15:01
  • Because you cannot load all reward objects along with shop object for performance reasons, but want to enforce constraints, you would go with eventual consistency. You would add a reward object outside the shop class, but publish a domain event. Then a subscriber listening to the domain event will ensure the business constraint is satisfied in the Shop object. If the invariant is not satisfied, then you would do a compensating transaction or raise an exception to be handled manually by an administrator. That's the price to pay in complexity when you separate them into two aggregates.
    – Subhash
    Commented Sep 10, 2019 at 15:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.