We are in the process of redesigning few our REST API endpoints to transition to a micro service architecture.
Here we are working on the endpoint /invitations/:id/confirm
.
This endpoint creates a User
, Account
using the provided Invitation
.
We have 3 aggregates Invitation
, User
and Account
.
The nominal flow we are currently following is:
Invitation
existsUser
Account
Invitation
UserId
This operation is done in-process which explained why we can return a UserId right away. We simply load our aggregates from the db, perform the associated business logic and persist the result.
Introducing micro services will require asynchronous processing. In other words, we should send a command to a bus and return status code 202.
In our plan, we want to fire a command named RequestInvitationConfirmation
. Basic validation will occur while instantiating this command.
Then this command will be sent through the bus to a consumer in charge of:
- Loading the invitation aggregates (make sure it exists)
- Calling the RequestConfirmation methods (will check that the invitation can be confirmed)
- Raising the InvitationConfirmationRequested
event
The InvitationConfirmationRequested
event will trigger a SAGA in charge of orchestrating the cross services communication
OnInvitationConfirmationRequested
CreateUser
commandOnUserCreated
CreateAccount
commandOnAccountCreated
DeleteInvitation
commandOnInvitationDeleted
InvitationConfirmed
Since it's asynchronous we need to provide a way to get the current operation state. I saw (https://www.adayinthelifeof.nl/2011/06/02/asynchronous-operations-in-rest/, https://asyncrestapi.docs.apiary.io/#) that a common approach
is to offer a /queue/:id
OR /actions/:id
endpoints.
This is where we get confused. How can you offer a single endpoint when states may be totally different from a SAGA to another?
Thx
For your saga to process messages within the scope of a single flow, you must correlate all your messages with the proper instance. When a saga is started by the first message, the saga identity is generated according to the rules:
Event(() => ItemAdded, x => x.CorrelateBy(cart => cart.UserName, context => context.Message.UserName)
.SelectId(context => Guid.NewGuid()));
So this id will be used as the identity of your saga that is persisted to the saga repository.
class ShoppingCart :
SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
Here, the CorrelationId
is the saga id, therefore is the correlation id of the whole process.
If you have access to your saga repository (and you do), it is quite easy to expose an HTTP API endpoint to retrieve the current saga state by looking at the value of the CurrentState
property in your saga state in the database that you use to persist sagas.