Aggregate root invariant enforcement with application quotas

后端 未结 2 1493
感动是毒
感动是毒 2020-12-21 11:54

The application Im working on needs to enforce the following rules (among others):

  1. We cannot register a new user to the system if the active user quota for the
相关标签:
2条回答
  • 2020-12-21 12:38

    An aggregate shoud be just a element that check rules. It can be from a stateless static function to a full state complex object; and does not need to match your persistence schema nor your "real life" concepts nor how you modeled your entities nor how you structure your data or your views. You model the aggregate with just the data you need to check rules in the form that suits you best.

    Do not be affraid about precompute values and persist them (total_active_users in this case).

    My recommendation is keep things as simple as possible and refactor (that could mean split, move and/or merge things) later; once you have all behavior modelled, is easier to rethink and analyze to refactor.

    This would be my first approach without event sourcing:

    TenantData { //just the data the aggregate needs from persistence
      int Id;
      int total_active_users;
      int quota;
    }
    
    UserEntity{ //the User Entity
      int id;
      string name;
      date birthDate;
      //other data and/or behaviour
    }
    
    public class RegistrarionAggregate{
    
        private TenantData fromTenant;//data from persistence
    
        public RegistrationAggregate(TenantData fromTenant){ //ctor
          this.fromTenant = fromTenant;
        }
    
        public UserRegistered registerUser(UserEntity user){
            if (fromTenant.total_active_users >= fromTenant.quota) throw new QuotaExceededException
    
            fromTeant.total_active_users++; //increase active users
    
            return new UserRegisteredEvent(fromTenant, user); //return system changes expressed as a event
        }
    }
    
    RegisterUserCommand{ //command structure
        int tenantId;
        UserData userData;// id, name, surname, birthDate, etc
    }
    
    class ApplicationService{
        public void registerUser(RegisterUserCommand registerUserCommand){
    
          var user = new UserEntity(registerUserCommand.userData); //avoid wrong entity state; ctor. fails if some data is incorrect
    
          RegistrationAggregate agg = aggregatesRepository.Handle(registerUserCommand); //handle is overloaded for every command we need. Use registerUserCommand.tenantId to bring total_active_users and quota from persistence, create RegistrarionAggregate fed with TenantData
    
          var userRegisteredEvent = agg.registerUser(user);
    
          persistence.Handle(userRegisteredEvent); //handle is overloaded for every event we need; open transaction, persist  userRegisteredEvent.fromTenant.total_active_users where tenantId, optimistic concurrency could fail if total_active_users has changed since we read it (rollback transaction), persist userRegisteredEvent.user in relationship with tenantId, commit transaction
    
        eventBus.publish(userRegisteredEvent); //notify external sources for eventual consistency
    
      }
    }
    

    Read this and this for a expanded explanation.

    0 讨论(0)
  • 2020-12-21 12:49

    I'm pretty sure that this is a common scenario that must have a better way to be implemented that my previous examples.

    A common search term for this sort of problem: Set Validation.

    If there is some invariant that must always be satisfied for an entire set, then that entire set is going to have to be part of the "same" aggregate.

    Often, the invariant itself is the bit that you want to push on; does the business need this constraint strictly enforced, or is it more appropriate to loosely enforce the constraint and charge a fee when the customer exceeds its contracted limits?

    With multiple sets -- each set needs to be part of an aggregate, but they don't necessarily need to be part of the same aggregate. If there is no invariant that spans multiple sets, then you can have a separate aggregate for each. Two such aggregates may be correlated, sharing the same tenant id.

    It may help to review Mauro Servienti's talk All our aggregates are wrong.

    0 讨论(0)
提交回复
热议问题