0

Where exactly to put business logic. While everyone said put it in the aggregate.

Yes, But what if business logic need datas of multiples aggregate?

Or if business logic need to update multiple aggregates.

Does it acceptable to be in command handlers or need to be in saga? And Why?


My first approach

  • Room

    • ID
    • Capacity
    • IsDestroyed
  • UserRoomSession

    • ID
    • RoomID
    • UserID
    • Status // Idle, Away, Leaved

I have CreateUserRoomSession command this command use for join the room

In command handler

  • Create a UserRoomSession()
  • Validate whether room is full or not. by get all of user
  • Validate room is not destroyed.
  • Validate user is already in a room.
  • If invalid then call method UserRoomSessionJoinRoomFail(roomID)
  • If valid then call method JoinRoom(roomID)
  • Save to ES

As example, We need to rely on Room to create UserRoomSession.

I tried to think in difference approaches all of them can't end the business logic validation within aggregate itself.

4

If you commonly need to update multiple aggregates, your aggregate boundaries might be wrong.

Of course, there can be scenarios, in which multiple aggregates do need to be updated. A saga is the typical way to deal with this. Note, however, that you are working across consistency boundaries here.

If you have the need to update multiple aggregates atomically, it's very likely that you have to rethink your boundaries.


Your chat application is a perfect example to show how this can be very easy (if possibly counter-intuitive sometimes), as well as quite involved.

Boundaries

Let's say you have a simple requirement: a user cannot be in the same room twice.

One option is to make the Room your aggragate, to which you can apply commands like JoinUser, GrantPrivileges, PostMessage, etc. The Room will be responsible for enforcing the invariant.

Another option is to have a Chatter aggregate (essentially the user). Again, you have commands like JoinRoom and the Chatter enforces the invariant.

Which layout you use depends on which other invariants you want to enforce.

  • The Room approach allows you specify restrictions between actions in the same room (e.g. no more than 20 users can be in a room).

  • The Chatter approach allows you to specify restrictions of your user, possibly regarding multiple rooms (e.g. a user can only be in one room at a time).

Sagas

Where it becomes tricky is if you want some combination of those restrictions. If you can't find a way to slice your aggregates accordingly (without creating some kind of singleton-super-aggregate that encompasses your entire application), you may be able to work around this with a saga.

In this scenario, adding the concept of a Session might be the right move. After the session is created, your saga tries to register it with the User and Room aggregates (and kills the session if either one fails).

I would still spend some time trying to see if there's another (perhaps non-obvious) way to slice the aggregates first. Remember that they don't have to mirror your read-models: if you don't have any consistency constraints that apply to a whole room, you may not need a Room aggregate on the command side at all.

  • How about validation which use another aggregate? – b.ben Aug 11 '18 at 11:14
  • 2
    @b.ben Each aggregate should validate its own data. A saga is more about one event triggering a number of additional commands (e.g. 'okay, the money has been transferred, let's tell the shipping aggregate to handle the logistics). Maybe you can elaborate a bit on what you're doing? – doubleYou Aug 11 '18 at 11:21
  • I just give an example in my question. – b.ben Aug 11 '18 at 18:14
  • @b.ben sorry, I've been a bit busy lately. I've edited my answer to incorporate your example. – doubleYou Aug 15 '18 at 17:35
1

When a many-to-many relationship presents itself ([Room] <- [UserRoomSession] -> [User]), the simplest approach is to choose a single side of the relationship to "own" the data. In this case we are choosing Room (which is probably the correct choice). The idea of an Aggregate is that all of the data within its consistency boundary is treated as one. Consider the following (yeah yeah PHP... happened to be what my editor had open today):

class JoinRoomCommand
{
    public $roomId;

    public $userId;
}

class JoinRoomCommandHandler
{
    private $roomRepository;

    private $sessionRepository;

    public function handle( JoinRoomCommand $cmd ) : void
    {
        $room = $this->roomRepository->find( $cmd->roomId );

        // how you "find or create" is up to you
        try {
            $session = $this->sessionRepository->findByUserId( $cmd->userId );
        } catch( \Exception $e ) { // SessionNotFound
            $session = $room->createSessionFor( $cmd->userId );
        }

        $room->startSession( $session );

        $this->roomRepository->save( $room );
    }
}

// Aggregate
class Room
{
    private $id;

    private $capacity;

    private $isDestroyed;

    private $sessions = [];

    public function createSessionFor( int $userId ) : RoomSession
    {
        return new RoomSession( $this->roomId, $userId );
    }

    public function startSession( RoomSession $session ) : void
    {   
        // room must not be destroyed
        if( $this->isDestroyed )
            throw new \Exception("Room does not exist");

        // room cannot be full
        if( false === ($this->capacity < count($this->sessions)) ) 
            throw new \Exception("Room is full");

        // session cannot already be in a romm
        if( NULL !== $session->roomId )
            throw new \Exception("Session is in another room");

        // two sessions cannot exist for same user
        foreach( $this->sessions as $currentSession ) {
            if( $currentSession->userId === $session->userId ) 
                throw new \Exception("Session already exists");
        }

        // add session to private collection
        $this->sessions[] = $session;
    }
}

// Value Object (pretend public "get", private "set")
class RoomSession
{
    public $roomId;

    public $userId;

    public $status;

    public function __construct( ?int $roomId, ?int $userId, string $status = 'ACTIVE' )
    {
        $this->roomId = $roomId;
        $this->userId = $userId;
        $this->status = $status;
    }
}

In the above example, all validation is located in the Room aggregate. I would argue, in this case, that separating the idea of "creating" RoomSession and "joining" a Room is likely not faithful to your domain/UL. It seems to me that RoomSessions are just a technical detail that is necessary to facilitate the use-case of "User joins room".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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