Skip to main content

Basket Service Features

The Basket service provides four core features for managing shopping carts in the e-commerce platform.

Get Basket

Retrieve a user’s shopping cart with all items and calculated total price.

Implementation

Query Handler (GetBasketHandler.cs:6-15):
public class GetBasketQueryHandler(IBasketRepository repository)
    : IQueryHandler<GetBasketQuery, GetBasketResult>
{
    public async Task<GetBasketResult> Handle(GetBasketQuery query, CancellationToken cancellationToken)
    {
        var basket = await repository.GetBasket(query.UserName);
        return new GetBasketResult(basket);
    }
}
Endpoint (GetBasketEndpoints.cs:10-16):
app.MapGet("/basket/{userName}", async (string userName, ISender sender) =>
{
    var result = await sender.Send(new GetBasketQuery(userName));
    var respose = result.Adapt<GetBasketResponse>();
    return Results.Ok(respose);
})

Behavior

  • Checks Redis cache first for performance
  • Falls back to PostgreSQL if not cached
  • Automatically caches result for subsequent requests
  • Throws BasketNotFoundException if basket doesn’t exist

Response Model

public record GetBasketResponse(ShoppingCart Cart);

Store Basket

Create or update a shopping cart with automatic discount application.

Implementation

Command Handler (StoreBasketHandler.cs:17-39):
public class StoreBasketCommandHandler
    (IBasketRepository repository, DiscountProtoService.DiscountProtoServiceClient discountProto)
    : ICommandHandler<StoreBasketCommand, StoreBasketResult>
{
    public async Task<StoreBasketResult> Handle(StoreBasketCommand command, CancellationToken cancellationToken)
    {
        await DeductDiscount(command.Cart, cancellationToken);
        
        await repository.StoreBasket(command.Cart, cancellationToken);

        return new StoreBasketResult(command.Cart.UserName);
    }

    private async Task DeductDiscount(ShoppingCart cart, CancellationToken cancellationToken)
    {
        // Communicate with Discount.Grpc and calculate lastest prices of products into sc
        foreach (var item in cart.Items)
        {
            var coupon = await discountProto.GetDiscountAsync(
                new GetDiscountRequest { ProductName = item.ProductName }, 
                cancellationToken: cancellationToken);
            item.Price -= coupon.Amount;
        }
    }
}
Endpoint (StoreBasketEndpoints.cs:10-18):
app.MapPost("/basket", async (StoreBasketRequest request, ISender sender) =>
{
    var command = request.Adapt<StoreBasketCommand>();
    var result = await sender.Send(command);
    var response = result.Adapt<StoreBasketResponse>();
    return Results.Created($"/basket/{response.UserName}", response);
})

Key Features

  1. Discount Integration: Automatically fetches and applies discounts via gRPC
  2. Price Calculation: Deducts coupon amounts from item prices
  3. Dual Persistence: Saves to both PostgreSQL and Redis cache
  4. Validation: Ensures cart and username are provided

Validation Rules

public class StoreBasketCommandValidator : AbstractValidator<StoreBasketCommand>
{
    public StoreBasketCommandValidator()
    {
        RuleFor(x => x.Cart).NotNull().WithMessage("Cart can not be null");
        RuleFor(x => x.Cart.UserName).NotEmpty().WithMessage("UserName is required");
    }
}

Request Model

public record StoreBasketRequest(ShoppingCart Cart);

Delete Basket

Remove a shopping cart from both database and cache.

Implementation

Command Handler (DeleteBasketHandler.cs:14-24):
public class DeleteBasketCommandHandler(IBasketRepository repository) 
    : ICommandHandler<DeleteBasketCommand, DeleteBasketResult>
{
    public async Task<DeleteBasketResult> Handle(DeleteBasketCommand command, CancellationToken cancellationToken)
    {
        await repository.DeleteBasket(command.UserName, cancellationToken);
        return new DeleteBasketResult(true);
    }
}
Endpoint (DeleteBasketEndpoints.cs:10-16):
app.MapDelete("/basket/{userName}", async (string userName, ISender sender) =>
{
    var result = await sender.Send(new DeleteBasketCommand(userName));
    var response = result.Adapt<DeleteBasketResponse>();
    return Results.Ok(response);
})

Behavior

  • Removes basket from PostgreSQL
  • Invalidates Redis cache entry
  • Returns success status

Validation Rules

public class DeleteBasketCommandValidator : AbstractValidator<DeleteBasketCommand>
{
    public DeleteBasketCommandValidator()
    {
        RuleFor(x => x.UserName).NotEmpty().WithMessage("UserName is required");
    }
}

Checkout Basket

Process basket checkout, publish event to message broker, and clean up cart.

Implementation

Command Handler (CheckoutBasketHandler.cs:21-47):
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);
        }

        // Set totalprice on basketcheckout event message
        var eventMessage = command.BasketCheckoutDto.Adapt<BasketCheckoutEvent>();
        eventMessage.TotalPrice = basket.TotalPrice;

        // send basket checkout event to rabbitmq using masstransit
        await publishEndpoint.Publish(eventMessage, cancellationToken);

        // delete the basket
        await repository.DeleteBasket(command.BasketCheckoutDto.UserName, cancellationToken);

        return new CheckoutBasketResult(true);
    }
}
Endpoint (CheckoutBasketEndpoints.cs:10-18):
app.MapPost("/basket/checkout", async (CheckoutBasketRequest request, ISender sender) =>
{
    var command = request.Adapt<CheckoutBasketCommand>();
    var result = await sender.Send(command);
    var response = result.Adapt<CheckoutBasketResponse>();
    return Results.Ok(response);
})

Checkout Process

  1. Retrieve Basket: Gets the basket with calculated total price
  2. Create Event: Maps checkout data to BasketCheckoutEvent
  3. Set Total Price: Adds calculated total to the event
  4. Publish Event: Sends event to RabbitMQ via MassTransit
  5. Clean Up: Deletes the basket from storage and cache

Checkout Data Transfer Object

public class BasketCheckoutDto
{
    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!;
}

Validation Rules

public class CheckoutBasketCommandValidator : AbstractValidator<CheckoutBasketCommand>
{
    public CheckoutBasketCommandValidator()
    {
        RuleFor(x => x.BasketCheckoutDto).NotNull().WithMessage("BasketCheckoutDto can't be null");
        RuleFor(x => x.BasketCheckoutDto.UserName).NotEmpty().WithMessage("UserName is required");
    }
}

Event-Driven Integration

The checkout operation publishes a BasketCheckoutEvent that triggers downstream processes:
  • Order creation in the Ordering service
  • Payment processing
  • Inventory updates
  • Email notifications

Price Calculation

The shopping cart automatically calculates the total price:
public decimal TotalPrice => Items.Sum(x => x.Price * x.Quantity);
  • Multiplies each item’s price by quantity
  • Prices already include applied discounts from gRPC calls
  • Calculated on-the-fly when accessed

Error Handling

All features include comprehensive error handling:
  • Validation Errors: Returns 400 Bad Request with validation details
  • Not Found Errors: Returns 404 when basket doesn’t exist
  • Server Errors: Returns 500 with problem details

Build docs developers (and LLMs) love