惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
J
Java Code Geeks
Microsoft Security Blog
Microsoft Security Blog
G
Google Developers Blog
D
DataBreaches.Net
Recent Announcements
Recent Announcements
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Y
Y Combinator Blog
博客园 - 【当耐特】
博客园 - 聂微东
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
P
Proofpoint News Feed
量子位
C
Check Point Blog
F
Fortinet All Blogs
罗磊的独立博客
Last Week in AI
Last Week in AI
GbyAI
GbyAI
L
LangChain Blog
博客园 - 司徒正美

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Syncing the Real Market: Consuming Brapi and Feeding Redi...
Roberto de V · 2026-04-27 · via DEV Community

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: