DEV Community

Roberto de Vargas Neto
Roberto de Vargas Neto

Posted on

Sincronizando o Mercado Real: Consumindo a Brapi e Alimentando o Redis com Spring Boot

Olá, pessoal!

Dando continuidade à série My Broker B3, hoje vamos falar sobre um componente essencial para o lado da B3 no nosso simulador: o B3 Market Sync API.

Antes de construirmos o motor que decide se uma ordem é executada ou rejeitada, precisamos garantir que ele tenha acesso a preços reais do mercado. É exatamente isso que o b3-market-sync-api faz: busca cotações reais de ativos brasileiros via brapi.dev e as armazena no Redis, pronto para consumo em tempo real.


🏗️ O que é o Market Sync?

Este microserviço tem uma responsabilidade única e bem definida: ser o produtor de dados de mercado do ecossistema. Ele opera de forma completamente assíncrona e independente — nenhum outro serviço precisa chamá-lo diretamente.

O fluxo é simples e elegante:

[brapi.dev] ◀── Feign Client ── [MarketSyncScheduler]
                                         │
                              market:price:{TICKER}
                                         │
                                      [Redis]
                                         │
                              [B3 Matching Engine] (próximo post)
Enter fullscreen mode Exit fullscreen mode

🎯 Foco no MVP

Neste serviço o objetivo é claro: ter preços reais disponíveis no Redis com baixa latência. O Matching Engine que construiremos no próximo post não vai consultar banco de dados nem fazer chamadas HTTP para obter preços — ele vai direto no Redis. Isso garante que a decisão de match aconteça em microssegundos.

Nesta fase, priorizei:

  • Sincronização agendada com guard de horário de pregão
  • Cache Redis com TTL para evitar dados obsoletos
  • Proteção de rate limit da API externa
  • Correção de bugs críticos encontrados na revisão
  • API REST para consulta dos preços em cache
  • Documentação via Swagger

🛠️ Stack Tecnológica

Tecnologia Uso
Java 21 + Spring Boot 3.5.11 Core do serviço
Spring Cloud OpenFeign Cliente HTTP declarativo para brapi.dev
Spring Data Redis Cache de alta performance
Spring Scheduling Sincronização periódica
Jackson JSR310 Suporte a tipos de data Java 8
SpringDoc OpenAPI Documentação via Swagger UI

🏗️ Os Pilares da Implementação

1. Feign Client — Consumindo a Brapi

O Spring Cloud OpenFeign permite declarar um cliente HTTP como uma simples interface, sem código boilerplate:

@FeignClient(name = "brapiClient", url = "${app.brapi.url}")
public interface BrapiClient {

    @GetMapping("/quote/{ticker}")
    BrapiResponseDTO getQuote(
        @PathVariable String ticker,
        @RequestParam String token
    );
}
Enter fullscreen mode Exit fullscreen mode

A URL e o token são injetados via application.yaml, mantendo o código limpo e configurável por ambiente.

2. Scheduler com Guard de Pregão

O coração do serviço é um job agendado que roda a cada 30 minutos. Mas antes de qualquer chamada à API, ele verifica se o mercado está aberto:

@Scheduled(fixedRateString = "${app.sync.interval:1800000}")
public void sync() {
    if (!isMarketOpen()) {
        log.info("Sync aborted: market is closed (outside trading hours or weekend).");
        return;
    }
    // ... busca e salva os preços
}

private boolean isMarketOpen() {
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of("America/Sao_Paulo"));
    DayOfWeek day = now.getDayOfWeek();
    int hour = now.getHour();

    if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) {
        return false;
    }

    // B3 trading hours: 10:00 to 18:00 (Sao Paulo time)
    return hour >= 10 && hour < 18;
}
Enter fullscreen mode Exit fullscreen mode

Detalhe importante: usamos ZonedDateTime com America/Sao_Paulo explicitamente. Dentro de um container Docker, a JVM pode usar UTC por padrão — o que faria o guard de horário funcionar errado. Além disso, passamos -Duser.timezone=America/Sao_Paulo no ENTRYPOINT do Dockerfile para garantir que a JVM também respeite o timezone correto:

ENTRYPOINT ["java", "-Duser.timezone=America/Sao_Paulo", "-jar", "app.jar"]
Enter fullscreen mode Exit fullscreen mode

3. Cache Redis com TTL Correto

O MarketDataService salva cada cotação no Redis com uma chave padronizada e TTL de 5 minutos:

private static final Duration CACHE_TTL = Duration.ofMinutes(5);

public void saveToCache(BrapiResultDTO quote) {
    String key = CACHE_KEY_PREFIX + quote.getTicker();
    // TTL of 5 minutes — if sync stops, stale prices expire quickly
    // preventing the Matching Engine from using outdated data
    redisTemplate.opsForValue().set(key, quote, CACHE_TTL);
    log.info("Ticker {} updated in cache: R$ {}", quote.getTicker(), quote.getRegularMarketPrice());
}
Enter fullscreen mode Exit fullscreen mode

O TTL é fundamental: se o serviço cair, os preços expiram em 5 minutos. O Matching Engine passa a não encontrar o preço no Redis e rejeita as ordens — comportamento correto e previsível, muito melhor do que usar preços de horas atrás.

4. Deserialização do Timestamp Epoch

Um bug sutil que encontrei: a brapi.dev retorna o campo regularMarketTime como epoch Unix (número inteiro), mas o Jackson tentava desserializar como LocalDateTime (string ISO). Isso causa falha silenciosa na desserialização.

A solução foi criar um deserializer customizado:

public class EpochToLocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {

    private static final ZoneId SAO_PAULO = ZoneId.of("America/Sao_Paulo");

    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
            throws IOException {
        long epoch = parser.getLongValue();
        return LocalDateTime.ofInstant(Instant.ofEpochSecond(epoch), SAO_PAULO);
    }
}
Enter fullscreen mode Exit fullscreen mode

E anotá-lo no DTO:

@JsonDeserialize(using = EpochToLocalDateTimeDeserializer.class)
private LocalDateTime regularMarketTime;
Enter fullscreen mode Exit fullscreen mode

5. Proteção de Rate Limit

Como utilizamos o plano gratuito da brapi.dev, implementamos um delay entre as chamadas para evitar throttling:

for (String ticker : brapiProperties.getTickers()) {
    try {
        BrapiResponseDTO response = brapiClient.getQuote(ticker.trim(), brapiProperties.getToken());
        if (nonNull(response.getResults()) && !response.getResults().isEmpty()) {
            marketDataService.saveToCache(response.getResults().getFirst());
        }
        // Small delay between calls to avoid hitting rate limit on free plan
        Thread.sleep(200);
    } catch (Exception e) {
        log.error("Failed to sync ticker {}: {}", ticker, e.getMessage());
    }
}
Enter fullscreen mode Exit fullscreen mode

O try/catch por ticker é essencial — se um ativo falhar, o loop continua para os próximos.

6. API REST + Swagger

Embora o Matching Engine consuma os preços diretamente do Redis, adicionamos endpoints REST para facilitar debug e observabilidade:

Método Endpoint Descrição
GET /api/v1/quotes Lista preços de todos os tickers configurados
GET /api/v1/quotes/{ticker} Retorna o preço de um ticker específico

Documentados via Swagger UI em http://localhost:8096/swagger-ui.html.


🔒 Segurança — Token Fora do Código

Um problema crítico que corrigi: o token da brapi.dev estava hardcoded como valor padrão no application.yaml:

# ❌ Antes — token exposto no repositório público
token: ${BRAPI_TOKEN:mFk3HMLijAtxsP5y1ZjhsY}

# ✅ Depois — sem fallback, variável obrigatória
token: ${BRAPI_TOKEN}
Enter fullscreen mode Exit fullscreen mode

Nunca exponha credenciais com valores default em repositórios públicos. A aplicação agora falha na inicialização se a variável não for fornecida — comportamento correto.


✅ Validando a Execução

Com a aplicação rodando localmente:

  • ✅ Tomcat subiu na porta 8096
  • ✅ Aplicação inicializou em menos de 3 segundos
  • ✅ Scheduler disparou e exibiu: Sync aborted: market is closed (outside trading hours or weekend)
  • ✅ Swagger UI acessível em http://localhost:8096/swagger-ui.html

🚀 O que vem a seguir?

Com o b3-market-sync-api pronto e alimentando o Redis com preços reais, temos a base que o próximo serviço precisa para funcionar. No próximo post vamos construir o B3 Matching Engine — o motor que vai consumir esses preços e decidir se as ordens da corretora serão executadas ou rejeitadas.

Ficou com alguma dúvida sobre a integração com Redis ou sobre o guard de horário de pregão? Deixe nos comentários!


🔎 Sobre a série

⬅️ Post Anterior: Do Stream para o Banco: Processando Market Data com Spring Boot, Redis e Flyway

➡️ Próximo Post: O Coração da B3: Construindo o Matching Engine com RabbitMQ e Redis (em breve)

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


Links:

Top comments (0)