Overview
The BasketCheckoutEvent demonstrates a complete event-driven workflow between the Basket and Ordering services. When a customer checks out their basket, this event carries all necessary information to create an order.
Event Definition
BasketCheckoutEvent Structure
namespace BuildingBlocks . Messaging . Events ;
public record BasketCheckoutEvent : IntegrationEvent
{
public string UserName { get ; set ; } = default ! ;
public Guid CustomerId { get ; set ; } = default ! ;
public decimal TotalPrice { get ; set ; } = default ! ;
// Shipping and BillingAddress
public string FirstName { get ; set ; } = default ! ;
public string LastName { get ; set ; } = default ! ;
public string EmailAddress { get ; set ; } = default ! ;
public string AddressLine { get ; set ; } = default ! ;
public string Country { get ; set ; } = default ! ;
public string State { get ; set ; } = default ! ;
public string ZipCode { get ; set ; } = default ! ;
// Payment
public string CardName { get ; set ; } = default ! ;
public string CardNumber { get ; set ; } = default ! ;
public string Expiration { get ; set ; } = default ! ;
public string CVV { get ; set ; } = default ! ;
public int PaymentMethod { get ; set ; } = default ! ;
}
Property Groups
Customer Info
Shipping Address
Payment Info
Username of the customer checking out
Unique identifier for the customer
Total price of all items in the basket
Customer’s email address for notifications
Street address for shipping
State or province for shipping
Payment method identifier
In production, never send sensitive payment information like card numbers and CVV through message queues. Use payment tokens or references instead.
Publishing the Event
Basket Service - CheckoutBasketHandler
The Basket service publishes the event during checkout:
using BuildingBlocks . Messaging . Events ;
using MassTransit ;
public class CheckoutBasketCommandHandler
( IBasketRepository repository , IPublishEndpoint publishEndpoint )
: ICommandHandler < CheckoutBasketCommand , CheckoutBasketResult >
{
public async Task < CheckoutBasketResult > Handle (
CheckoutBasketCommand command ,
CancellationToken cancellationToken )
{
// Get existing basket with total price
var basket = await repository . GetBasket (
command . BasketCheckoutDto . UserName ,
cancellationToken
);
if ( basket == null )
{
return new CheckoutBasketResult ( false );
}
// Map DTO to event and set total price
var eventMessage = command . BasketCheckoutDto . Adapt < BasketCheckoutEvent >();
eventMessage . TotalPrice = basket . TotalPrice ;
// Publish event to RabbitMQ
await publishEndpoint . Publish ( eventMessage , cancellationToken );
// Delete the basket after successful checkout
await repository . DeleteBasket (
command . BasketCheckoutDto . UserName ,
cancellationToken
);
return new CheckoutBasketResult ( true );
}
}
Key Steps
Retrieve Basket : Fetch the customer’s basket from the repository
Calculate Total : Use the basket’s calculated total price
Map to Event : Convert the checkout DTO to an integration event
Publish Event : Send the event to RabbitMQ via MassTransit
Clean Up : Delete the basket after successful publication
The handler uses Mapster (Adapt<T>()) for object mapping. This automatically maps matching properties from the DTO to the event.
Consuming the Event
Ordering Service - BasketCheckoutEventHandler
The Ordering service consumes the event and creates an order:
using BuildingBlocks . Messaging . Events ;
using MassTransit ;
using Ordering . Application . Orders . Commands . CreateOrder ;
public class BasketCheckoutEventHandler
( ISender sender , ILogger < BasketCheckoutEventHandler > logger )
: IConsumer < BasketCheckoutEvent >
{
public async Task Consume ( ConsumeContext < BasketCheckoutEvent > context )
{
logger . LogInformation (
"Integration Event handled: {IntegrationEvent}" ,
context . Message . GetType (). Name
);
var command = MapToCreateOrderCommand ( context . Message );
await sender . Send ( command );
}
private CreateOrderCommand MapToCreateOrderCommand ( BasketCheckoutEvent message )
{
// Create address DTO from event data
var addressDto = new AddressDto (
message . FirstName ,
message . LastName ,
message . EmailAddress ,
message . AddressLine ,
message . Country ,
message . State ,
message . ZipCode
);
// Create payment DTO from event data
var paymentDto = new PaymentDto (
message . CardName ,
message . CardNumber ,
message . Expiration ,
message . CVV ,
message . PaymentMethod
);
var orderId = Guid . NewGuid ();
// Create complete order DTO
var orderDto = new OrderDto (
Id : orderId ,
CustomerId : message . CustomerId ,
OrderName : message . UserName ,
ShippingAddress : addressDto ,
BillingAddress : addressDto ,
Payment : paymentDto ,
Status : OrderStatus . Pending ,
OrderItems :
[
new OrderItemDto ( orderId , new Guid ( "5334c996-8457-4cf0-815c-ed2b77c4ff61" ), 2 , 500 ),
new OrderItemDto ( orderId , new Guid ( "c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914" ), 1 , 400 )
]
);
return new CreateOrderCommand ( orderDto );
}
}
Processing Flow
Key Components
Consume Event
Implement IConsumer<BasketCheckoutEvent> to receive events from RabbitMQ
Log Receipt
Log the event for observability and debugging
Transform Data
Map the event properties to domain commands and DTOs
Execute Command
Use MediatR to send the CreateOrderCommand for processing
Complete Workflow
End-to-End Process
Service Responsibilities
Basket Service Publisher Responsibilities:
Validate checkout data
Calculate basket total
Publish BasketCheckoutEvent
Clean up basket data
Handle publish failures
Ordering Service Consumer Responsibilities:
Listen for BasketCheckoutEvent
Validate event data
Create order record
Start fulfillment process
Handle processing failures
Testing
Unit Testing the Publisher
public class CheckoutBasketHandlerTests
{
[ Fact ]
public async Task Handle_ValidBasket_PublishesEvent ()
{
// Arrange
var publishEndpoint = Substitute . For < IPublishEndpoint >();
var repository = Substitute . For < IBasketRepository >();
var basket = new ShoppingCart
{
UserName = "[email protected] " ,
TotalPrice = 100.00m
};
repository . GetBasket ( Arg . Any < string >(), Arg . Any < CancellationToken >())
. Returns ( basket );
var handler = new CheckoutBasketCommandHandler ( repository , publishEndpoint );
var command = new CheckoutBasketCommand ( new BasketCheckoutDto
{
UserName = "[email protected] "
});
// Act
await handler . Handle ( command , CancellationToken . None );
// Assert
await publishEndpoint . Received ( 1 ). Publish (
Arg . Is < BasketCheckoutEvent >( e => e . TotalPrice == 100.00m ),
Arg . Any < CancellationToken >()
);
}
}
Unit Testing the Consumer
public class BasketCheckoutEventHandlerTests
{
[ Fact ]
public async Task Consume_ValidEvent_CreatesOrder ()
{
// Arrange
var sender = Substitute . For < ISender >();
var logger = Substitute . For < ILogger < BasketCheckoutEventHandler >>();
var handler = new BasketCheckoutEventHandler ( sender , logger );
var context = Substitute . For < ConsumeContext < BasketCheckoutEvent >>();
var basketEvent = new BasketCheckoutEvent
{
UserName = "[email protected] " ,
CustomerId = Guid . NewGuid (),
TotalPrice = 100.00m ,
FirstName = "John" ,
LastName = "Doe" ,
EmailAddress = "[email protected] "
};
context . Message . Returns ( basketEvent );
// Act
await handler . Consume ( context );
// Assert
await sender . Received ( 1 ). Send (
Arg . Is < CreateOrderCommand >( cmd =>
cmd . Order . CustomerId == basketEvent . CustomerId
),
Arg . Any < CancellationToken >()
);
}
}
Error Handling
Retry Configuration
Configure automatic retries in MassTransit:
services . AddMassTransit ( config =>
{
config . AddConsumer < BasketCheckoutEventHandler >( configurator =>
{
configurator . UseMessageRetry ( retry =>
{
retry . Interval ( 3 , TimeSpan . FromSeconds ( 5 ));
});
});
});
Dead Letter Queue
Failed messages automatically move to a dead letter queue after retries are exhausted:
Queue: basket-checkout-event_error
Messages include failure reason and retry count
Manual inspection and reprocessing available
Best Practices
Include all data needed by consumers to avoid callbacks: ✅ Good : Event contains all order data var event = new BasketCheckoutEvent
{
CustomerId = basket . CustomerId ,
TotalPrice = basket . TotalPrice ,
Items = basket . Items // Complete item list
};
❌ Bad : Consumer must query back var event = new BasketCheckoutEvent
{
BasketId = basket . Id // Consumer must fetch items
};
Consumers should handle duplicate messages safely: public async Task Consume ( ConsumeContext < BasketCheckoutEvent > context )
{
var eventId = context . MessageId ;
// Check if already processed
if ( await _processedEvents . ExistsAsync ( eventId ))
{
_logger . LogInformation ( "Event {EventId} already processed" , eventId );
return ;
}
// Process event
await ProcessOrder ( context . Message );
// Mark as processed
await _processedEvents . AddAsync ( eventId );
}
Never include sensitive data in plain text: // Instead of raw card data
public string CardNumber { get ; set ; }
// Use tokens
public string PaymentToken { get ; set ; }
public string PaymentProvider { get ; set ; }
Messaging Overview Learn about the messaging infrastructure setup
Integration Events Understand the IntegrationEvent base class