Skip to content

Request flows

Each diagram below follows the actual call chain: gateway route, controller, mediator request, handler, repository and store. The file references let you follow along in the code.

A shopper opens the store, which requests the first page of products with a brand filter.

sequenceDiagram
  autonumber
  actor U as Browser (store MFE)
  participant GW as Ocelot gateway
  participant CC as CatalogController
  participant M as IMediator
  participant H as GetAllProductsHandler
  participant R as ProductRepository
  participant DB as MongoDB (Products)

  U->>GW: GET /Catalog/GetAllProducts?pageIndex=1&pageSize=12&brandId=...
  GW->>CC: GET /api/v1/Catalog/GetAllProducts?...
  CC->>M: Send(GetAllProductsQuery(CatalogSpecParams))
  M->>H: Handle(query)
  H->>R: GetProducts(specParams)
  R->>R: Build filter (search regex, Brands.Id, Types.Id)
  R->>DB: CountDocumentsAsync(filter)
  DB-->>R: total
  R->>DB: Find(filter).Sort(...).Skip(12*(page-1)).Limit(12)
  DB-->>R: products
  R-->>H: Pagination of Product
  H-->>CC: Pagination of ProductResponse (Mapperly)
  CC-->>GW: 200 OK
  GW-->>U: 200 OK { pageIndex, pageSize, count, data }

Code path: route in src/ApiGateways/Ocelot.ApiGateway/ocelot.Development.json, then src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs, src/Services/Catalog/Catalog.Application/Handlers/GetAllProductsHandler.cs and src/Services/Catalog/Catalog.Infrastructure/Repositories/ProductRepository.cs.

Brands and types for the filter sidebar come from GET /Catalog/GetAllBrands and GET /Catalog/GetAllTypes, which follow the same shape with a plain Find(_ => true). Product images are loaded by the browser directly from S3 (or LocalStack), using the URL stored in ImageFile. They never pass through the API.

2. Add to cart, with a discount lookup over gRPC

Section titled “2. Add to cart, with a discount lookup over gRPC”

The cart is saved as a whole on every change. For each item that has no discount yet, Basket asks Discount for a coupon.

sequenceDiagram
  autonumber
  actor U as Browser (checkout MFE)
  participant GW as Ocelot gateway
  participant BC as BasketController
  participant H as CreateShoppingCartCommandHandler
  participant DG as DiscountGrpcService
  participant DS as Discount.API (gRPC)
  participant DH as GetDiscountQueryHandler
  participant PG as PostgreSQL (Coupon)
  participant R as Redis

  U->>GW: POST /Basket/CreateBasket { userName, items[] }
  GW->>BC: POST /api/v1/Basket/CreateBasket
  BC->>H: mediator.Send(CreateShoppingCartCommand)
  loop each item where DiscountAmount == 0 and Price == OriginalPrice
    H->>DG: GetDiscount(productName)
    DG->>DS: DiscountProtoService.GetDiscount (HTTP/2, h2c :8080)
    DS->>DH: mediator.Send(GetDiscountQuery)
    DH->>PG: SELECT * FROM Coupon WHERE ProductName = @p
    PG-->>DH: coupon row, or none (placeholder Amount 0)
    DH-->>DS: CouponModel
    DS-->>DG: CouponModel { amount }
    DG-->>H: coupon
    H->>H: DiscountAmount = amount, Price = OriginalPrice - amount
  end
  alt Discount unavailable (RpcException)
    DG-->>H: CouponModel { amount = 0 }, warning logged
  end
  H->>R: SET userName = JSON(cart)
  H->>R: GET userName
  R-->>H: cart
  H-->>BC: ShoppingCartResponse (TotalPrice computed)
  BC-->>GW: 200 OK
  GW-->>U: 200 OK

Code path: src/Services/Basket/Basket.Application/Handlers/CreateShoppingCartCommandHandler.cs, src/Services/Basket/Basket.Application/GrpcService/DiscountGrpcService.cs, src/Services/Discount/Discount.API/Services/DiscountService.cs, src/Services/Discount/Discount.Application/Handlers/GetDiscountQueryHandler.cs and src/Services/Basket/Basket.Infrastructure/Repositories/BasketRepository.cs.

Points to notice:

  • The lookups are sequential, one round trip per new item. For large carts, a batched GetDiscounts(names[]) RPC or Task.WhenAll would cut latency.
  • The discount is applied once, and the item’s state (DiscountAmount != 0) records that it was applied. The coupon is not re-evaluated if it changes later.
  • The gRPC hop is traced end to end: Basket registers the gRPC client instrumentation, and Discount’s ASP.NET Core instrumentation picks up the propagated context.

3. Checkout: BasketCheckoutEvent to Ordering

Section titled “3. Checkout: BasketCheckoutEvent to Ordering”

Checkout is split into a synchronous half, which completes with 202 Accepted, and an asynchronous half, where Ordering creates the order.

sequenceDiagram
  autonumber
  actor U as Browser (checkout MFE)
  participant GW as Ocelot gateway
  participant BC as BasketController (v1)
  participant R as Redis
  participant MQ as RabbitMQ
  participant OC as BasketOrderingConsumer
  participant V as ValidationBehaviour
  participant OH as CheckoutOrderCommandHandler
  participant SQL as SQL Server
  participant AC as OrderActivityConsumer

  U->>GW: POST /Basket/Checkout { userName, address, payment... }
  Note over GW: RateLimitOptions: Limit 1 per 3s period
  GW->>BC: POST /api/v1/Basket/Checkout
  BC->>R: GetBasket(userName)
  alt no cart
    BC-->>U: 400 Bad Request
  end
  BC->>BC: Map to BasketCheckoutEvent, TotalPrice = cart.TotalPrice
  BC->>MQ: Publish(BasketCheckoutEvent)
  BC->>R: DeleteBasket(userName)
  BC-->>GW: 202 Accepted
  GW-->>U: 202 Accepted

  Note over MQ,OC: asynchronous from here
  MQ->>OC: deliver from basketcheckout-queue
  OC->>V: mediator.Send(CheckoutOrderCommand)
  V->>V: FluentValidation (UserName, TotalPrice, Email, names)
  V->>OH: next()
  OH->>SQL: INSERT Orders (audit fields set in SaveChangesAsync)
  SQL-->>OH: new Id
  OH-->>OC: orderId
  OC->>MQ: Publish(OrderActivityEvent { OrderId, Actor })
  MQ->>AC: deliver from order-activity-queue
  AC->>SQL: EXISTS Activities WHERE EventId = ?
  AC->>SQL: INSERT Activities ("Order.Created")

Code path: src/Services/Basket/Basket.API/Controllers/BasketController.cs, src/Services/Ordering/Ordering.API/EventBusConsumer/BasketOrderingConsumer.cs, src/Services/Ordering/Ordering.Application/Behaviour/ValidationBehaviour.cs, src/Services/Ordering/Ordering.Application/Handlers/CheckoutOrderCommandHandler.cs and src/Services/Ordering/Ordering.API/EventBusConsumer/OrderActivityConsumer.cs.

POST /Basket/CheckoutV2 goes to api/v2/Basket/Checkout. That endpoint publishes BasketCheckoutEventV2 (user name and total only) to basketcheckout-queue-v2, where BasketOrderingConsumerV2 creates a minimal order through CheckoutOrderCommandV2. It does not publish an activity event.

sequenceDiagram
  autonumber
  actor A as Admin MFE
  participant CC as CatalogController
  participant Mongo as MongoDB
  participant MQ as RabbitMQ
  participant PC as ProductActivityConsumer (Ordering)
  participant SQL as SQL Server (Activities)

  A->>CC: POST /Catalog/CreateProduct
  CC->>Mongo: InsertOne(product)
  CC->>MQ: Publish(ProductActivityEvent { Created, Actor = "system" })
  CC-->>A: 200 ProductResponse
  MQ->>PC: product-activity-queue
  PC->>SQL: skip if EventId exists, else INSERT
  A->>SQL: later: GET /Activity?entityType=Product (gateway, ActivityController)

The activity feed lives in Ordering’s database even though it records Catalog events. It works as a read model built from events, and the admin dashboard reads it through GET /Activity, which the gateway caches for 10 seconds.