VOOZH about

URL: https://dev.to/rvneto/o-coracao-da-b3-construindo-o-matching-engine-com-rabbitmq-redis-e-spring-boot-1ob0

⇱ O Coração da B3: Construindo o Matching Engine com RabbitMQ, Redis e Spring Boot - DEV Community


Olá, pessoal!

Dando continuidade à série My Broker B3, chegamos em um dos componentes mais aguardados do ecossistema: o B3 Matching Engine API.

No post anterior, construímos o b3-market-sync-api que sincroniza preços reais da brapi.dev e os armazena no Redis. Agora é hora de usar esses dados — este é o serviço que simula a própria Bolsa de Valores, decidindo se uma ordem de compra ou venda será executada ou rejeitada com base nos preços que acabamos de colocar no cache.


🏗️ O que é o Matching Engine?

No mundo real, quando você envia uma ordem de compra pela sua corretora, ela vai para a B3, que verifica se existe uma contraparte disposta a negociar naquele preço. Esse processo é chamado de matching.

No nosso simulador, o b3-matching-engine-api reproduz essa lógica de forma simplificada:

  1. Consumo: Recebe a ordem da corretora via RabbitMQ (fila mq-broker-to-b3)
  2. Preço: Busca o preço atual do ativo no Redis (market:price:{TICKER}) — dados injetados pelo Market Sync que construímos antes
  3. Decisão: Aplica a regra de matching
  4. Persistência: Grava o resultado no PostgreSQL
  5. Notificação: Devolve o resultado para a corretora via RabbitMQ (fila mq-b3-to-broker)
[Broker] ──RabbitMQ──▶ [mq-broker-to-b3] ──▶ [Matching Engine]
 │
 Redis: market:price:{TICKER}
 (alimentado pelo Market Sync ⬆️)
 │
 ┌────────────┴────────────┐
 FILLED REJECTED
 │ │
 PostgreSQL PostgreSQL
 │ │
 [mq-b3-to-broker] ◀────────────────┘
 │
 [Broker]

🎯 Foco no MVP

Antes de mergulhar no código, vale o mesmo disclaimer dos posts anteriores: estamos construindo a base. O objetivo é ter o fluxo ponta a ponta funcionando com robustez suficiente para validar a POC.

Nesta fase, priorizei:

  • Fluxo principal funcionando de ponta a ponta
  • Tratamento correto de falhas (sem ordens "sumindo" silenciosamente)
  • Dead Letter Queue para mensagens que falham no processamento
  • API REST para consulta do histórico de execuções
  • Documentação via Swagger

🛠️ Stack Tecnológica

Tecnologia Uso
Java 21 + Spring Boot 3.5 Core do serviço
Spring RabbitMQ Consumo de ordens e envio de resultados
Spring Data Redis Consulta de preços em tempo real
Spring Data JPA + PostgreSQL Persistência das execuções
Flyway Versionamento do schema do banco
SpringDoc OpenAPI Documentação via Swagger UI

🏗️ Os Pilares da Implementação

1. Configuração do RabbitMQ

Um ponto crítico que aprendi neste serviço: nunca dependa que as filas já existam no broker. Se a aplicação subir antes do RabbitMQ ter as filas criadas, o consumer falha na inicialização.

A solução é declarar todos os beans de infraestrutura diretamente no Spring — ele garante que filas, exchanges e bindings existam antes de qualquer mensagem trafegar:

@Bean
public DirectExchange exchange() {
 return new DirectExchange(exchangeName);
}

@Bean
public Queue queueIn() {
 return QueueBuilder.durable(queueIn)
 .withArgument("x-dead-letter-exchange", dlxName)
 .withArgument("x-dead-letter-routing-key", dlqName)
 .build();
}

@Bean
public Binding bindingQueueIn(Queue queueIn, DirectExchange exchange) {
 return BindingBuilder.bind(queueIn).to(exchange).with(routingKey);
}

Note o withArgument — ele já configura a Dead Letter Queue diretamente na declaração da fila principal. Qualquer mensagem que falhe no processamento é automaticamente redirecionada.

2. A Lógica de Matching

O coração do serviço. A regra é simples e direta:

  • COMPRA (BUY): Se o preço que o usuário aceita pagar >= preço de mercado → FILLED
  • VENDA (SELL): Se o preço que o usuário quer receber <= preço de mercado → FILLED
  • Caso contrário → REJECTED
private static boolean isCanExecute(OrderEventDTO order, BigDecimal marketPrice) {
 if (SideStatus.BUY.name().equalsIgnoreCase(order.getSide())) {
 return order.getPrice().compareTo(marketPrice) >= 0;
 } else if (SideStatus.SELL.name().equalsIgnoreCase(order.getSide())) {
 return order.getPrice().compareTo(marketPrice) <= 0;
 }
 return false;
}

3. Tratamento de Falhas — Sem Ordens Sumindo

Um bug silencioso que identifiquei: quando o ticker não estava no Redis, o método simplesmente retornava sem fazer nada. A corretora ficava esperando uma resposta que nunca chegava.

A correção foi simples mas fundamental — qualquer falha deve notificar o broker:

if (marketDataOpt.isEmpty()) {
 log.warn("Price for ticker {} not found in Redis. Rejecting order {}",
 order.getTicker(), order.getOrderId());
 OrderResponseEvent response = new OrderResponseEvent(
 order.getOrderId(), ExecutionStatus.REJECTED.name(), BigDecimal.ZERO);
 orderProducer.sendToBroker(response);
 return;
}

💡 Este é exatamente o motivo pelo qual construímos o b3-market-sync-api primeiro. Se o Redis não tiver o preço, a ordem é imediatamente rejeitada — comportamento correto e previsível.

4. Garantia Transacional

Outro ponto crítico: persistir no PostgreSQL e publicar no RabbitMQ são duas operações distintas. Se o banco salvar mas o publish falhar, a execução fica gravada mas o broker nunca é notificado — inconsistência silenciosa.

A solução foi adicionar @Transactional no método que orquestra as duas operações:

@Transactional
private void saveAndNotify(OrderEventDTO order, BigDecimal price, ExecutionStatus status) {
 // 1. Persists in PostgreSQL
 OrderExecution execution = OrderExecution.builder()
 .orderId(order.getOrderId())
 .ticker(order.getTicker())
 .side(SideStatus.valueOf(order.getSide()))
 .quantity(order.getQuantity())
 .executedPrice(price)
 .status(status)
 .build();
 repository.save(execution);

 // 2. Notifies the Broker
 OrderResponseEvent response = new OrderResponseEvent(
 order.getOrderId(), status.name(), price);
 orderProducer.sendToBroker(response);
}

5. Dead Letter Queue

Para mensagens que falham no consumer (erro inesperado no processamento), configuramos uma DLQ completa:

@Bean
public DirectExchange deadLetterExchange() {
 return new DirectExchange(dlxName);
}

@Bean
public Queue deadLetterQueue() {
 return QueueBuilder.durable(dlqName).build();
}

@Bean
public Binding bindingDlq(Queue deadLetterQueue, DirectExchange deadLetterExchange) {
 return BindingBuilder.bind(deadLetterQueue)
 .to(deadLetterExchange)
 .with(dlqName);
}

No consumer, relançamos a exceção para que o RabbitMQ saiba que a mensagem falhou e a redirecione:

@RabbitListener(queues = "${app.rabbitmq.queue-in}")
public void receiveOrder(OrderEventDTO event) {
 log.info("New order received: ID {} | Ticker {} | Side {}",
 event.getOrderId(), event.getTicker(), event.getSide());
 try {
 matchingService.process(event);
 } catch (Exception e) {
 log.error("Failed to process order {}: {}", event.getOrderId(), e.getMessage(), e);
 throw e; // RabbitMQ routes to DLQ
 }
}

6. API REST + Swagger

Como o serviço expõe endpoints de consulta, aproveitei para configurar o Swagger UI desde o início:

@Bean
public OpenAPI customOpenAPI() {
 return new OpenAPI()
 .info(new Info()
 .title("B3 Matching Engine API")
 .version("1.0.0")
 .description("Simulates the B3 stock exchange matching engine..."));
}

Os endpoints disponíveis:

Método Endpoint Descrição
GET /api/v1/executions Lista todas as execuções
GET /api/v1/executions/order/{orderId} Busca por ID de ordem
GET /api/v1/executions/ticker/{ticker} Busca por ticker
GET /api/v1/executions/status/{status} Busca por status

✅ Validando a Execução

Com a aplicação rodando localmente, todos os componentes subiram corretamente:

  • ✅ Flyway aplicou a migration da tabela order_executions
  • ✅ Hibernate validou o schema
  • ✅ RabbitMQ conectado com filas e DLQ declaradas
  • ✅ Tomcat rodando na porta 8091
  • ✅ Swagger UI acessível em http://localhost:8091/swagger-ui.html

🚀 O que vem a seguir?

Com o b3-market-sync-api alimentando o Redis e o b3-matching-engine-api pronto para processar ordens, toda a parte da B3 está operacional.

O próximo passo é construir o broker-order-api — o maestro que vai orquestrar todo o ciclo de vida de uma ordem do lado da corretora:

  1. Receber a intenção de compra/venda do usuário
  2. Validar o ticker via REST na broker-asset-api
  3. Salvar a ordem como PENDING
  4. Publicar o evento no Kafka
  5. Enviar a ordem para o Matching Engine via RabbitMQ
  6. Consumir o feedback e atualizar o status final

Quando esse serviço estiver pronto, teremos o primeiro fluxo completo ponta a ponta rodando no ecossistema.

Ficou com alguma dúvida sobre a lógica de matching ou sobre a configuração do RabbitMQ com DLQ? Deixe nos comentários!


🔎 Sobre a série

⬅️ Post Anterior: Sincronizando o Mercado Real: Consumindo a Brapi e Alimentando o Redis com Spring Boot

📘 Índice da Série: Guia da Série


Links: