Skip to main content

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

UserName
string
required
Username of the customer checking out
CustomerId
Guid
required
Unique identifier for the customer
TotalPrice
decimal
required
Total price of all items in the basket
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

  1. Retrieve Basket: Fetch the customer’s basket from the repository
  2. Calculate Total: Use the basket’s calculated total price
  3. Map to Event: Convert the checkout DTO to an integration event
  4. Publish Event: Send the event to RabbitMQ via MassTransit
  5. 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

1

Consume Event

Implement IConsumer<BasketCheckoutEvent> to receive events from RabbitMQ
2

Log Receipt

Log the event for observability and debugging
3

Transform Data

Map the event properties to domain commands and DTOs
4

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

Build docs developers (and LLMs) love