How the FTGO Order History Service maintains a DynamoDB read model via CQRS, consuming Order Service domain events to enable fast per-consumer history queries.
Use this file to discover all available pages before exploring further.
This page covers how the FTGO application applies the Command Query Responsibility Segregation (CQRS) pattern through its dedicated Order History Service. Rather than querying the Order Service’s write database directly, a separate read model built on Amazon DynamoDB is kept in sync by consuming domain events published by the Order Service.
In a microservices system the write side of a service (its command model) is optimized for transactional correctness. Queries that span multiple services, require rich filtering, or need a different access pattern would force either a costly join across service boundaries or tight coupling between services.CQRS splits responsibilities:
Command side — the Order Service writes to its own MySQL database and publishes domain events.
Query side — the Order History Service subscribes to those events and maintains a denormalized DynamoDB table shaped for consumer queries.
The Order History Service is a standalone Spring Boot application in ftgo-order-history-service. It has no write path of its own; its database is populated entirely by events.
Each event write includes the source event identifier as a DynamoDB condition expression. If the same event is delivered twice ConditionalCheckFailedException is caught and silently ignored, making every update idempotent.
findOrderHistory queries the GSI with an optional keyword and status filter. DynamoDB pagination is handled by passing through the LastEvaluatedKey token.
// OrderHistoryDaoDynamoDb.java@Overridepublic OrderHistory findOrderHistory(String consumerId, OrderHistoryFilter filter) { QuerySpec spec = new QuerySpec() .withScanIndexForward(false) .withHashKey("consumerId", consumerId) .withRangeKeyCondition( new RangeKeyCondition("creationDate") .gt(filter.getSince().getMillis())); filter.getStartKeyToken().ifPresent(token -> spec.withExclusiveStartKey(toStartingPrimaryKey(token))); // optional keyword and status filter expressions ... filter.getPageSize().ifPresent(spec::withMaxResultSize); ItemCollection<QueryOutcome> result = index.query(spec); return new OrderHistory( StreamSupport.stream(result.spliterator(), false) .map(this::toOrder) .collect(toList()), Optional.ofNullable( result.getLastLowLevelResult() .getQueryResult() .getLastEvaluatedKey()) .map(this::toStartKeyToken));}
Keywords are tokenized at write time and stored as a DynamoDB set, allowing contains(keywords, :keyword) filter expressions at query time.
OrderHistoryController exposes two endpoints under /orders:
// OrderHistoryController.java@RestController@RequestMapping(path = "/orders")public class OrderHistoryController { // GET /orders?consumerId=<id> @RequestMapping(method = RequestMethod.GET) public ResponseEntity<GetOrdersResponse> getOrders( @RequestParam(name = "consumerId") String consumerId) { OrderHistory orderHistory = orderHistoryDao .findOrderHistory(consumerId, new OrderHistoryFilter()); return new ResponseEntity<>( new GetOrdersResponse( orderHistory.getOrders().stream() .map(this::makeGetOrderResponse) .collect(toList()), orderHistory.getStartKey().orElse(null)), HttpStatus.OK); } // GET /orders/{orderId} @RequestMapping(path = "/{orderId}", method = RequestMethod.GET) public ResponseEntity<GetOrderResponse> getOrder( @PathVariable String orderId) { return orderHistoryDao.findOrder(orderId) .map(order -> new ResponseEntity<>( makeGetOrderResponse(order), HttpStatus.OK)) .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND)); }}
The GET /orders endpoint is routed through the API Gateway. The OrderConfiguration Spring Cloud Gateway bean maps GET /orders to the Order History Service URL rather than the Order Service URL.
Order Service (MySQL write model) │ publishes domain events via Eventuate Tram ▼ Kafka (event bus) │ consumed by OrderHistoryEventHandlers ▼Order History Service (DynamoDB read model) │ queried via REST ▼ Consumer clients / API Gateway
What fields are stored per order in DynamoDB?
Field
Type
Notes
orderId
String (hash key)
Matches the Order Service ID
consumerId
String
GSI hash key for history queries
creationDate
Number
Epoch ms; GSI sort key
orderStatus
String
APPROVAL_PENDING, APPROVED, CANCELLED, REJECTED
lineItems
List
Denormalized menu item name, ID, price, quantity
keywords
StringSet
Tokenized restaurant name + item names for free-text filter