This solution uses the Vertical Slice Architecture, where each feature is implemented end-to-end, encapsulating all necessary components, in a single file, or you can use folders if things get to unwieldy. This approach contrasts with traditional layered architecture or Clean Architecture by focusing on delivering complete features rather than separating concerns into distinct layers.
Patterns in this solution:
- Each set of classes comprising a feature are nested inside a static class, giving them a dot notation of
Feature.Component. ApiEndpointclasses handle HTTP requests, perform validation, and send commands to NServiceBus for asynchronous processing. Minimal APIs are used, along with the Carter library for endpoint discovery.CommandHandlerclasses are NServiceBus handlers that process the various commands sent by API endpoints and events published by theOrderExpirySaga.OrderExpirySagamanages the lifecycle of the order, ensuring it is either approved or marked as expired within a specified timeframe.
Each vertical slice, such as submitting an order or handling order expiry, is self-contained, promoting better maintainability, scalability, and a clear separation of concerns. This architecture allows developers to focus on individual features, making the codebase easier to understand and modify.
The SubmitOrder class handles the process of submitting an order through an API endpoint, processing the order, and saving it to the database. Here is a summary of how it works:
- Receives an HTTP POST request to submit an order.
- Validates the order number to ensure it is unique.
- If the order number already exists, it returns a
409 Conflictresponse.
- If the order number already exists, it returns a
- Sends a command to the message bus to process the order.
- Returns a
202 Acceptedresponse indicating the order is being processed, with aLocationheader pointing to the order status endpoint.
- Asynchronously handles the command to process the order.
- Saves the order to the database.
- Publishes an event indicating the order has been submitted.
- Error handling If multiple commands are dispatched by the
ApiEndpointfor the sameOrderNumberaSqlExceptionwill be thrown when saving the Order. NServiceBus retries will kick in to retry the message. Ideally we would have additional validation logic in theCommandHandlerto prevent this. Always validate your inputs! However, I left this here to demonstrate NServiceBus retries. ;)
flowchart TD
A[HTTP POST /orders] -. ApiRequest .-> B[ApiEndpoint.Handle]
B --> C{Is OrderNumber Unique?}
C -- No --> D[Return 409 Conflict]
C -- Yes --> E[Create Command]
E --> F[Send Command to<br>Message Bus]
F -. ApiResponse .-> G[Return 202 Accepted<br>with Location Header]
F -. Command .-> H[Handle Command]
H --> I[Save Order to Database]
I --> J[Publish OrderSubmitted<br>Event]
J -. OrderSubmitted .-> K((End))
The ApproveOrder class handles the process of approving an order through an API endpoint, processing the approval, and updating the order status in the database. Here is a summary of how it works:
- Receives an HTTP POST request to approve an order.
- Validates the order number to ensure the order exists.
- If the order number does not exist, it returns a
404 Not Foundresponse.
- If the order number does not exist, it returns a
- Sends a command to the message bus to process the order approval.
- Returns a
202 Acceptedresponse indicating the order approval is being processed, with aLocationheader pointing to the order status endpoint.
- Asynchronously handles the command to approve the order.
- Fetches the order from the database using the order number.
- If the order is not found, an exception is thrown. NServiceBus retries will kick in and retry until hopefully the
SubmitOrdercommand is handled creating the order. Remember, you can't guarantee the order of events!
- If the order is not found, an exception is thrown. NServiceBus retries will kick in and retry until hopefully the
- Checks the current status of the order:
- If the order is already approved, it logs the information and returns.
- If the order has expired, it logs a warning and returns.
- If the order is submitted, it updates the status to "Approved", saves the changes to the database, and publishes an
OrderApprovedevent.
flowchart TD
A[HTTP POST<br>/orders/#/approve] -. ApiRequest .-> B[ApiEndpoint]
B --> C[Check if Order Exists<br>in Database]
C --> D{Order Found?}
D -- No --> E[Return 404 Not Found]
D -- Yes --> F[Create Command]
F --> G[Send Command to<br>Message Bus]
G --> H[Return 202 Accepted<br>with Location Header]
G --> I[Handle Command]
I --> J[Fetch Order from Database]
J --> K{Order Found?}
K -- No --> L[Throw Exception]
K -- Yes --> M{Order Status}
M -- Approved --> N[Log Info:<br>Order Already Approved]
M -- Expired --> O[Log Warning:<br>Order Expired]
M -- Submitted --> P[Update Order Status<br>to Approved]
P --> Q[Save Changes to Database]
Q --> R[Publish OrderApproved Event]
R --> S((End))
The ExpireOrder class handles the process of expiring an order by updating its status in the database and publishing an event. Unlike the other two features, there is no Expire Order API endpoint. Here is a summary of how it works:
- Asynchronously handles the command to expire the order.
- Fetches the order from the database using the order number.
- If the order is not found, an exception is thrown. NServiceBus retries will kick in and retry until hopefully the
SubmitOrdercommand is handled creating the order. Remember, you can't guarantee the order of events!
- If the order is not found, an exception is thrown. NServiceBus retries will kick in and retry until hopefully the
- Checks the current status of the order:
- If the order is already approved, it logs a warning and returns.
- If the order is already expired, it logs the information and returns.
- If the order is submitted, it updates the status to "Expired", saves the changes to the database, and publishes an
OrderExpiredevent.
flowchart TD
A[Receive ExpireOrder<br>Command] -. Command .-> B[Handle Command]
B --> C[Fetch Order from Database]
C --> D{Order Found?}
D -- No --> E[Throw Exception]
D -- Yes --> F{Order Status}
F -- Approved --> G[Log Warning:<br>Order Cannot be Expired]
F -- Expired --> H[Log Info:<br>Order Already Expired]
F -- Submitted --> I[Update Order Status<br>to Expired]
I --> J[Save Changes to Database]
J --> K[Publish OrderExpired Event]
K -. OrderExpired .-> L((End))
The OrderExpirySaga is a saga that manages the lifecycle of an order, ensuring it is either approved or marked as expired within a specified timeframe. Here is a summary of how the saga works:
- The saga is started by the
OrderSubmittedevent, which indicates that a new order has been submitted. - Upon receiving this event, the saga logs the start of the process and requests a timeout for the order expiry, set to 30 seconds (
EXPIRES_IN_SECONDS).
- If the timeout elapses before the order is approved, the
OrderExpirationTimeoutis triggered. - The saga logs that the expiration timeout has elapsed and sends a local command to expire the order (
ExpireOrder.Command).
- If the order is approved before the timeout elapses, the
OrderApprovedevent is handled. - The saga logs that the order has been accepted and marks the saga as complete, ending the process.
- If the order is marked as expired, the
OrderExpiredevent is handled. - The saga logs that the order has expired and marks the saga as complete, ending the process.
- The saga uses
SqlSagato persist its state, withSagaDatacontaining theOrderNumberas the correlation property. - The
ConfigureMappingmethod maps theOrderNumberproperty to the corresponding events (OrderSubmitted,OrderApproved, andOrderExpired).
In summary, the OrderExpirySaga ensures that an order is either approved or marked as expired within a specified timeframe, handling the necessary events and logging the process at each step.
flowchart TD
A[OrderSubmitted] --> B[Start OrderExpirySaga]
B --> C[Request Timeout<br>for Order Expiry]
C --> D{Order Approved<br>Before Timeout?}
D -- Yes --> E[Handle OrderApproved]
E --> F[Mark Saga as Complete]
F --> G[End Saga]
D -- No --> H[Timeout Elapsed]
H --> I[Handle Timeout]
I --> J[Send ExpireOrder Command]
J --> K[Handle OrderExpired]
K --> F[Mark Saga as Complete]
F --> G((End))