Studi.SimpleWebshopOrder.Distributed
is an intricate webshop order management system designed to showcase the power and challenges of distributed systems in a realistic e-commerce setting. It's a microservices-based system that utilizes MassTransit
for asynchronous communication between services.
Here's a breakdown:
-
Order Service: Initiates the entire order process. When an order is created, it's persisted to a SQL database using EntityFramework. After saving, an
OrderCreated
event is published using MassTransit to notify other services of the new order. -
Payment Service: Handles the payment process. Upon receiving the
OrderCreated
event, it initializes thePaymentStateMachine
to manage the payment flow. This service uses MongoDB for maintaining the state of payments. -
Delivery Service: Responsible for delivering ordered items once a payment succeeds. This service consumes the
OrderCreated
andOrderPaymentSucceeded
events. It uses an in-memory store, structured with the Repository Pattern, to maintain a list of orders.
The system demonstrates a real-world scenario where the Order, Payment, and Delivery services collaborate asynchronously to ensure orders are processed, paid for, and delivered seamlessly.
The project is structured around three main services: Order, Payment, and Delivery. Here's a deeper dive into the key features and how they operate:
-
Controller: The
OrdersController
manages various API endpoints related to order processing. Key methods include:GetAllOrders()
: Fetches all orders.GetOrder(Guid id)
: Retrieves a specific order by its ID.Create(Order order)
: Persists an order to the database and then publishes anOrderCreated
event.CreateRandom()
: Generates a random order using theFaker
library and proceeds as theCreate
method.
-
Data Models:
Order
: Represents an order with properties like ID, CustomerId, and a list of OrderItems.OrderItem
: Represents individual items in an order. Contains ArticleId, ArcticleTitle, Price, and Quantity.
- State Machine (
PaymentStateMachine
): Manages the payment process with states likePaymentPending
,PaymentSuccessful
, andPaymentFailed
. It listens for events likeOrderCreated
,OrderPaymentFailed
, andOrderPaymentSucceeded
to transition between states.
-
Repository (
OrderRepository
): Utilizes the Repository Pattern and maintains an in-memory store of the orders. Provides methods likeAddOrderAsync
andGetOrderByIdAsync
. -
Consumer (
DeliverOrderConsumer
): Consumes theOrderPaymentSucceeded
event. Once an order's payment is successful, it simulates the collection of ordered items, logs the collected items, and then publishes anOrderDelivered
event.
- Event-Driven Architecture: The system leverages
MassTransit
for publishing and consuming events across services. Events likeOrderCreated
drive the flow of data and operations from one service to the next, enabling asynchronous and decoupled interactions.
- OrderService: Uses EntityFramework for order management.
- PaymentService: Leverages MongoDB for maintaining the payment state.
- DeliveryService: Implements a Repository Pattern with an in-memory store for orders.
To ensure that the project runs smoothly, we've containerized some services using Docker. Here's how to get everything up and running:
-
Docker: Make sure you have Docker installed on your system. If not, download Docker and follow the installation instructions for your OS.
-
Docker Compose: Docker Desktop for Windows and Mac includes Compose out-of-the-box, but if you're on Linux, you might need to install it separately.
-
Navigate to the project root: Using your terminal or command prompt, navigate to the directory containing the
docker-compose.yml
file. -
Run Docker Compose: To start the containers defined in
docker-compose.yml
, run the following command:docker-compose up -d
This command will start the following services:
sqldata
: A SQL Server container for storing orders.mongodb
: A MongoDB container for the payment service.mongo-express
: A web-based MongoDB admin interface.rabbitmq
: A messaging broker required for MassTransit.jaeger
: A distributed tracing system.
- Verify the Services:
You can check the running containers and their status using:
docker ps
Additionally, you can access the following interfaces in your browser to verify the services:
- Mongo Express:
http://localhost:8081/
- RabbitMQ Management:
http://localhost:15672/
(guest:guest) - Jaeger UI:
http://localhost:16686/
- Setting Up Visual Studio:
- Open the solution in Visual Studio.
- Right-click on the solution in Solution Explorer
- Choose "Set StartUp Projects".
- Select "Multiple startup projects".
- Ensure that the
OrderService
,PaymentService
, andDeliveryService
projects are all set to "Start".
- Stop the Services:
When you're done, you can stop the services by navigating to the directory containing the
docker-compose.yml
file and running:docker-compose down
That's it! Your environment should now be correctly set up, and you're ready to run and test the solution.
Now that your environment is set up, you're ready to run the solution and familiarize yourself with its capabilities. Here's a step-by-step guide to help you get started:
Before you proceed, ensure all Docker services (sqldata
, mongodb
, rabbitmq
, etc.) are up and running. If you've followed the initial configuration, you should be good to go.
- Visual Studio:
- Simply press
F5
or click the "Start" button in Visual Studio. This should run all the microservices (OrderService
,PaymentService
, andDeliveryService
).
- Simply press
There are two ways to initiate an order:
You can use Postman or any other API testing tool to make a POST request: POST https://localhost:7088/Orders
With the following sample JSON payload:
{
"id": "{{$guid}}",
"customerId": "{{$guid}}",
"items": [
{
"articleId": "{{$guid}}",
"arcticleTitle": "Notebook",
"price": 856.5,
"quantity": 2
},
{
"articleId": "{{$guid}}",
"arcticleTitle": "Docking Station",
"price": 69.98,
"quantity": 2
}
]
}
POST https://localhost:7088/Orders/Random
To further deepen your understanding of the solution and its interdependencies, here are some hands-on tasks for you to complete:
- Familiarize yourself with the solution structure and the core services (
OrderService
,PaymentService
,DeliveryService
). - Create orders using both methods provided (
/Orders
and/Orders/Random
). - Observe the flow of the order across services using distributed tracing in Jaeger.
-
Start the application but refrain from starting the
DeliveryService
. -
Create several orders.
-
Now, initiate the
DeliveryService
. -
Notice that the service output consistently displays
!!!!UNBEKANNT!!!!
for the collected items.public async Task Consume(ConsumeContext<OrderPaymentSucceeded> context) { ... var order = await this.orderRepository.GetOrderByIdAsync(context.Message.OrderId); var itemsListText = order is null ? "!!!!UNBEKANNT!!!!" : string.Join("\n", order.Items.Select(i => $"{i.Quantity,2}x {i.ArticleTitle,-30} {i.PriceTotal,8:n2}")); ... }
-
Why is that the case? Dive into the code of the
DeliverOrderConsumer
class and investigate the reason behind this behavior. -
Create a solution for that problem.
The payment process occasionally fails at random. Your task is to implement a retry mechanism to address this:
-
Familiarize yourself with the
ProcessOrderPaymentConsumer
class. Notice the random failure condition.public async Task Consume(ConsumeContext<OrderCreated> context) { ... var failed = Random.Shared.Next(1, 100) % 2 == 1; ... }
-
Implement the retry mechanism in the
PaymentStateMachine
where it's commented as// TODO: Add Retry behavior
.During(PaymentPending, When(PaymentFailedEvent) .TransitionTo(PaymentFailed), // TODO: Add Retry behavior ... );
-
Consider exit conditions for payment retries. For instance, how many retries should be attempted before a permanent failure? What delay should exist between retries?
In distributed systems, monitoring the performance and health of the services becomes crucial. Metrics provide numerical data that can be analyzed over time, such as the number of requests processed or the average response time. Traces offer insights into the flow of requests through the system.
To view traces:
- Navigate to the Jaeger UI.
- Select the desired service (Order, Payment, or Delivery) from the dropdown.
- Click on "Find Traces" and explore the trace details.