Building a Distributed Online Store with Spring Boot, RabbitMQ, and OpenTelemetry

In this post, we’ll walk through the architecture and implementation of a simple microservices-based online store built with:

  • Spring Boot 3
  • RabbitMQ (asynchronous messaging)
  • OpenTelemetry for distributed tracing
  • Jaeger for visualizing traces

Microservices Architecture

Our online store is made up of the following services:

ServiceResponsibility
catalog-serviceManages products (name, price)
order-serviceHandles orders, triggers workflow events
inventory-serviceReserves stock when an order is placed
payment-serviceSimulates payment processing
shipment-serviceSimulates shipment creation

All services are loosely coupled and communicate via RabbitMQ using topic exchanges.

Event-Driven Flow

sequenceDiagram
    participant User
    participant Catalog
    participant Order
    participant Rabbit
    participant Inventory
    participant Payment
    participant Shipment

    User->>Catalog: View product
    User->>Order: Place order (HTTP)

    Order->>Rabbit: OrderPlacedEvent
    Rabbit->>Inventory: inventory.order_placed

    Inventory->>Rabbit: InventoryReservedEvent / OutOfStockEvent
    Rabbit->>Order: order.inventory_reserved or order.inventory_out_of_stock

    alt Inventory reserved
        Order->>Rabbit: OrderReadyForPaymentEvent
        Rabbit->>Payment: payment.order_ready

        Payment->>Rabbit: PaymentProcessedEvent / PaymentFailedEvent
        Rabbit->>Order: order.payment_processed or order.payment_failed

        alt Payment successful
            Order->>Rabbit: OrderPaidEvent
            Rabbit->>Shipment: shipment.order_paid

            Shipment->>Rabbit: OrderShippedEvent
            Rabbit->>Order: order.shipment_created
        else Payment failed
            Order->>Order: Mark order as FAILED
        end

    else Out of stock
        Order->>Order: Mark order as FAILED
    end

Setting Up RabbitMQ

We use RabbitMQ as our message broker. In development, RabbitMQ is run as a Docker container behind the scenes.

Each service:

  • Declares its own queue
  • Binds to an appropriate exchange using a routing key
  • Does not share queues with other services

In RabbitMQ, producers only know exchanges + routing keys.
Consumers define their own queues and bind to those routing keys.

Setting Up OpenTelemetry + Jaeger

We use OpenTelemetry to trace HTTP requests and message flow between services.

You can quickly setup jaeger with this docker compose:

services:
  jaeger:
    image: jaegertracing/all-in-one:1.51
    ports:
      - "16686:16686"  # Jaeger UI
      - "4317:4317"    # OTLP gRPC

  # Add RabbitMQ here if desired

The agent is attached at runtime via:

-javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.exporter.otlp.endpoint=http://YOUR_SERVER:4317 \
-Dotel.exporter.otlp.protocol=grpc

Where YOUR_SERVER is the IP address/hostname where you run the service.

You can download the java agent here:

https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases

Messaging Patterns

Here’s how messaging is wired:

EventPublisherConsumersRouting Key
OrderPlacedEventorder-serviceinventory, paymentorder.placed
InventoryReservedEventinventory-serviceorder-serviceinventory.reserved
InventoryOutOfStockEventinventory-serviceorder-serviceinventory.out-of-stock
PaymentProcessedEventpayment-serviceorder-servicepayment.processed
PaymentFailedEventpayment-serviceorder-servicepayment.failed
OrderPaidEventorder-serviceshipment-serviceorder.paid
OrderShippedEventshipment-serviceorder-serviceshipment.created

Domain Modeling

Each service has its own entities and data logic. Example from catalog-service:

@Entity
public class Product extends BaseEntity {
    private String name;
    private BigDecimal price;
}

Common DTOs and events are placed in a shared common module.

Messaging Configuration Example

Each service defines its queues and bindings like this:

@Bean
public Queue orderPlacedQueue() {
    return QueueBuilder.durable("inventory.order_placed").build();
}

@Bean
public Binding bindOrderPlaced(Queue orderPlacedQueue, TopicExchange orderExchange) {
    return BindingBuilder.bind(orderPlacedQueue)
            .to(orderExchange)
            .with(MessagingTopics.Order.ROUTING_KEY_ORDER_PLACED);
}

This binds inventory-service to the order.placed event without knowing the publisher.

Tracing in Action

Each message span is automatically traced when using the OpenTelemetry Java agent and Jackson message converter. All spans are linked across services and visible in Jaeger.

Examples of spans:

  • HTTP: POST /orders
  • Messaging: send order.placed
  • Messaging: receive inventory.reserved
Tracing in jaeger

Summary

We’ve built a distributed system with:

  • Clean microservice separation
  • Asynchronous messaging via RabbitMQ
  • Full distributed tracing with OpenTelemetry + Jaeger

This project demonstrates how to:

  • Model a system using events and topics
  • Trace cross-service operations
  • Build loosely coupled, observable systems

You’re welcome to checkout the repo to run for yourself here: https://github.com/datmt/spring-microservices-rabbit-otel

Leave a Comment