Skip to content

Design Patterns

tien.nguyen edited this page Aug 15, 2026 · 3 revisions

Software Design Patterns in Emporia

The Emporia Trading Platform incorporates enterprise architectural and object-oriented design patterns to ensure low latency, high throughput, maintainability, and strict concurrency safety.


🏛️ Architectural Patterns

1. API Gateway Pattern

  • Implementation: gateway module using Spring Cloud Gateway.
  • Role: Single entry point for external client traffic (:8082). Handles OIDC authentication header injection, CORS configuration, path-based routing (/api/static/**, /api/orders/**, /api/market-data/**), and rate limiting.

2. Event-Driven Architecture (EDA)

  • Implementation: trading-contracts, order-management (which now contains execution routing).
  • Role: Immutable domain events decouple state persistence from execution routing. The transport is in-process, not a broker: ShardedOrderDispatcher partitions OrderDomainEvents across single-threaded shard executors by order id, which preserves the strict per-order ordering a Kafka partition key used to give, without the round trip. The three topics that carried this between processes (emporia.orders.v1, emporia.order.commands.v1, emporia.execution.commands.v1) are gone.

3. Command Query Responsibility Segregation (CQRS)

  • Implementation:
    • Command Path: order-management receives POST/PUT /api/orders/** directly in-process (REST controller and Aeron subscriber both call the handler with no broker round trip); execution's SMART/VWAP child orders reach the same handler by direct call.
    • Query Path: order-management maintains read-optimized database models (TradingOrder) queried via GET /api/orders/{id} and GET /api/orders.

4. Database-per-Service Pattern

  • Implementation: Separate PostgreSQL databases (emporia_authorisation, emporia_static_data, emporia_client_config, emporia_order_data, emporia_portfolio).
  • Role: Ensures strict domain boundary isolation, preventing tight database coupling and allowing independent service scaling.

5. Choreography-based Saga Pattern

  • Implementation: Distributed order lifecycle processing across Kafka topic channels without a centralized orchestrator.
  • Role: OrderManagementService $\rightarrow$ ExecutionService $\rightarrow$ OrderManagementService $\rightarrow$ PortfolioService. Each service reacts to events and publishes state transitions.

🧩 Behavioral & Structural Software Patterns

Pattern Location / Class Architectural Role & Purpose
Strategy Pattern execution (OrderRoutingStrategy, SmartRoutingStrategy, VwapRoutingStrategy) Dynamically selects order routing logic based on order type, size, and market execution conditions.
State Machine Pattern order-management (TradingOrder) Enforces valid status transitions (PENDING_NEW $\rightarrow$ LIVE $\rightarrow$ PARTIALLY_FILLED $\rightarrow$ FILLED / CANCELLED / REJECTED) and mathematical quantity invariants.
Observer Pattern market-data (QuoteAggregator, MarketDataSseController) Broadcasts streaming tick updates and aggregated depth updates to connected client subscribers via HTTP SSE and gRPC streams.
Adapter Pattern execution (ExchangeCoreAdapter) Bridges Emporia's Kafka order events to the in-memory LMAX Disruptor exchange-core matching engine protocol.
Facade Pattern order-management (StaticDataClient) Provides a simplified fluent RestClient interface to query instrument snapshots from static-data.
Decorator Pattern market-data (ResilientMarketDataProvider) Wraps external IEX / FIX market data sources with fallback logging, retry mechanisms, and rate limit guards.

🔄 Synchronized State Machine Transitions

The TradingOrder entity in order-management implements thread-safe state machine transitions:

public synchronized void applyFill(BigDecimal fillQuantity, BigDecimal fillPrice) {
    require(status == OrderStatus.LIVE || status == OrderStatus.PARTIALLY_FILLED
                    || status == OrderStatus.CANCELLED,
            "Only active or cancelled orders can receive fills");
    
    // Recalculate traded quantity, remaining quantity, and volume-weighted average price
    BigDecimal newTradedQuantity = tradedQuantity.add(fillQuantity);
    tradedQuantity = newTradedQuantity;
    remainingQuantity = quantity.subtract(newTradedQuantity);
    averageTradePrice = newTradeValue.divide(newTradedQuantity, 6, RoundingMode.HALF_UP);
    
    if (remainingQuantity.signum() == 0) {
        status = OrderStatus.FILLED;
    } else if (status != OrderStatus.CANCELLED) {
        status = OrderStatus.PARTIALLY_FILLED;
    }
    validateInvariants();
}
  • Thread Safety: All state-modifying methods (applyFill, requestCancel, confirmCancel, reject) use synchronized monitors.
  • Invariant Enforcement: validateInvariants() guarantees that tradedQuantity + remainingQuantity == quantity at all times.
  • Late Fill Safety: Accepts late fills received from execution venues even after order cancellation.

Clone this wiki locally