Account Boundary Context
- A user can sign in one of the roles: member or admin
- Identity aggregate issues and validate JWT token that also maintains roles
- Basic functionalities - sign up, sign in and sign out are required, and sign in is established by emails and password
- When entering my page, the user's information should be shown as follows
- Name, email, preferred language
User
is everyone who uses this service regardless of the purpose. User
has roles - Member
and Administrator
. Member
means someone who signs up to use the system from the outside, and Administrator
means someone who operates and develops the system. A User
can have one or two roles, but only one can be chosen when signing in with an e-mail and password.
Identity
is something that can prove who the requestor is. We'll use JWT, and the payload should have the role that a user chooses when signing in.
When requesting Sign Up
, the user must provide this information: e-mail, password, name, preferred language. E-mail is a key for Sign in
, so it should be unique. The role is pre-defined as Member
with the public Sign Up
API. The password must be encrypted securely.
The user must provide e-mail and password information when requesting Sign In
. When the password matches, our system responds with two JWT tokens: access token (with the role in the claim) and refresh token. The refresh token is persisted to detect refresh token reuse.
The user's refresh token is removed when requesting Sign Out
.
If I were to design in the real world, I would use DDD Crew's Bounded Context Canvas to dive deep. This is an examination for me so I will skip it.
Based on the current requirements, Identity and User aggregates cooperate closely, and the incoming traffic variance would be similar. So, I determined that two aggregates in a single bounded context would be beneficial, and I named the bounded context Account. Account will be the first service, which means two aggregates are deployed in a single service.
POST /<bounded context>/<owner aggregate>/<command>
GET /<bounded context>/<owner aggregate>/<query>
A bounded context will be a single deployment. In this situation, I agonised over whether an aggregate name should be in the API path. Just for now, it could add complexity and extra naming burden. However, what if we split the bounded context into two bounded contexts - User and Identity bounded contexts? In this situation, we should convert internal calls to external API calls and would be in trouble deciding which API belongs to which aggregate. I assumed it is good to determine the owner aggregate for an API in advance.
When a single request from a client is required to execute multiple commands over aggregates? For instance, assume that a user is signing in. User aggregate executes the SignInWithCredential
command to verify the given password is correct, and Identity aggregate executes IssueAccessToken
to transfer the JWT token to the client. There are several ways to achieve the flow.
---
title: Option 1 - Call Identity Aggregate from Account Interface (Application Service)
---
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: Sign in
Interface ->>+ User: VerifyCredential
User ->> User: Verify ID and password
User ->>- Interface: OK
Interface ->>+ Identity: IssueAccessToken
Identity ->>- Interface: OK
Interface ->>- Client: OK
---
title: Option 2 - Call Identity Aggregate from User Aggregate
---
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: Sign in
Interface ->>+ User: VerifyCredential
User ->> User: Verify ID and password
User ->>+ Identity: IssueAccessToken
Identity ->>- User: OK
User ->>- Interface: OK
Interface ->>- Client: OK
Option 1 decouples business flow concerns into the application service layer. This choice would be great when introducing BFF and context manager (or SAGA) because we only need to move the business flow into the orchestration mechanism.
Option 2 recognises that issuing an access token is a part of the user's sign-in business logic. So, it assumes that the User aggregate must know the existence of the Identity aggregate (or service) and interact with it directly.
There are pros and cons, and they may differ in every case. For this exercise, I will go to Option 1. I prefer to separate business flow from the single domain.
All diagrams illustrate the happy path of use-cases.
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: POST /account/user/sign-up
Interface ->>+ User: RegisterUser
User ->> User: Execute
User ->>- Interface: OK
Interface ->>+ Identity: RegisterIdentity
Identity ->> Identity: Execute
Identity ->>- Interface: OK
Interface ->>- Client: OK
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: POST /account/user/sign-in
Interface ->>+ User: VerifyCredential
User ->>- Interface: OK
Interface ->>+ Identity: IssueTokens
Identity ->>- Interface: OK
Interface ->>- Client: OK
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: POST /account/user/sign-out
Interface ->>+ Identity: InvalidateTokens
Identity ->>- Interface: OK
Interface ->>- Client: OK
sequenceDiagram
actor Client
participant Interface as Account Interface
participant User as User Aggregate
participant Identity as Identity Aggregate
Client ->>+ Interface: POST /account/identity/refresh-tokens
Interface ->>+ Identity: RefreshTokens
Identity ->>- Interface: OK
Interface ->>- Client: OK