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:
-
Consumo: Recebe a ordem da corretora via RabbitMQ (fila
mq-broker-to-b3) -
Preço: Busca o preço atual do ativo no Redis (
market:price:{TICKER}) — dados injetados pelo Market Sync que construímos antes - Decisão: Aplica a regra de matching
- Persistência: Grava o resultado no PostgreSQL
-
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-apiprimeiro. 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:
- Receber a intenção de compra/venda do usuário
- Validar o ticker via REST na
broker-asset-api - Salvar a ordem como
PENDING - Publicar o evento no Kafka
- Enviar a ordem para o Matching Engine via RabbitMQ
- 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:
Top comments (0)