DEV Community

Roberto de Vargas Neto
Roberto de Vargas Neto

Posted on

Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot

Hello everyone!

Continuing the My Broker B3 series, today we'll talk about an essential component on the B3 side of our simulator: the B3 Market Sync API.

Before building the engine that decides whether an order gets executed or rejected, we need to ensure it has access to real market prices. That's exactly what b3-market-sync-api does: fetches real asset prices from brapi.dev and stores them in Redis, ready for real-time consumption.


🏗️ What is the Market Sync?

This microservice has a single, well-defined responsibility: to be the market data producer of the ecosystem. It operates completely asynchronously and independently — no other service needs to call it directly.

The flow is simple and elegant:

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

🎯 MVP Focus

The goal here is clear: have real prices available in Redis with low latency. The Matching Engine we'll build in the next post won't query a database or make HTTP calls to get prices — it goes straight to Redis. This ensures the matching decision happens in microseconds.

In this phase, I prioritized:

  • Scheduled sync with market hours guard
  • Redis cache with TTL to prevent stale data
  • External API rate limit protection
  • Critical bug fixes found during code review
  • REST API for querying cached prices
  • Swagger documentation

🛠️ Tech Stack

Technology Usage
Java 21 + Spring Boot 3.5.11 Service core
Spring Cloud OpenFeign Declarative HTTP client for brapi.dev
Spring Data Redis High-performance cache
Spring Scheduling Periodic synchronization
Jackson JSR310 Java 8 Date/Time serialization support
SpringDoc OpenAPI Swagger UI documentation

🏗️ Implementation Pillars

1. Feign Client — Consuming Brapi

Spring Cloud OpenFeign lets you declare an HTTP client as a simple interface, with no boilerplate code:

@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

The URL and token are injected via application.yaml, keeping the code clean and environment-configurable.

2. Scheduler with Market Hours Guard

The heart of the service is a scheduled job that fires every 30 minutes. But before any API call, it checks whether the market is open:

@Scheduled(fixedRateString = "${app.sync.interval:1800000}")
public void sync() {
    if (!isMarketOpen()) {
        log.info("Sync aborted: market is closed (outside trading hours or weekend).");
        return;
    }
    // ... fetch and store prices
}

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

Important detail: we use ZonedDateTime with America/Sao_Paulo explicitly. Inside a Docker container, the JVM may default to UTC — which would make the market hours guard behave incorrectly. We also pass -Duser.timezone=America/Sao_Paulo in the Dockerfile ENTRYPOINT to ensure the JVM honors the correct timezone:

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

3. Redis Cache with Correct TTL

The MarketDataService stores each quote in Redis with a standardized key and 5-minute TTL:

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

The TTL is fundamental: if the service goes down, prices expire in 5 minutes. The Matching Engine then fails to find the price in Redis and rejects orders — correct and predictable behavior, much better than using hours-old prices.

4. Epoch Timestamp Deserialization

A subtle bug I found: brapi.dev returns the regularMarketTime field as a Unix epoch (integer), but Jackson was trying to deserialize it as LocalDateTime (ISO string). This causes silent deserialization failure.

The solution was a custom deserializer:

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

And annotating it on the DTO:

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

5. Rate Limit Protection

Since we use the brapi.dev free plan, we added a delay between calls to avoid 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

The per-ticker try/catch is essential — if one asset fails, the loop continues for the rest.

6. REST API + Swagger

Although the Matching Engine consumes prices directly from Redis, we added REST endpoints to make debugging and observability easier:

Method Endpoint Description
GET /api/v1/quotes List prices for all configured tickers
GET /api/v1/quotes/{ticker} Get price for a specific ticker

Documented via Swagger UI at http://localhost:8096/swagger-ui.html.


🔒 Security — Token Out of Code

A critical issue I fixed: the brapi.dev token was hardcoded as a default value in application.yaml:

# ❌ Before — token exposed in public repository
token: ${BRAPI_TOKEN:mFk3HMLijAtxsP5y1ZjhsY}

# ✅ After — no fallback, variable is required
token: ${BRAPI_TOKEN}
Enter fullscreen mode Exit fullscreen mode

Never expose credentials as default values in public repositories. The application now fails on startup if the variable is not provided — correct behavior.


✅ Validating the Execution

With the application running locally:

  • ✅ Tomcat started on port 8096
  • ✅ Application initialized in under 3 seconds
  • ✅ Scheduler fired and logged: Sync aborted: market is closed (outside trading hours or weekend)
  • ✅ Swagger UI accessible at http://localhost:8096/swagger-ui.html

🚀 What's Next?

With b3-market-sync-api up and feeding Redis with real prices, we have the foundation the next service needs to work. In the next post we'll build the B3 Matching Engine — the engine that will consume these prices and decide whether the broker's orders are executed or rejected.

Got any questions about the Redis integration or the market hours guard? Drop them in the comments!


🔎 About the Series

⬅️ Previous Post: From Stream to Database: Processing Market Data with Spring Boot, Redis and Flyway

➡️ Next Post: The Heart of B3: Building the Matching Engine with RabbitMQ and Redis (coming soon)

📘 Series Index: Series Roadmap


Links:

Top comments (0)