<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged feign</title>
<link>https://coderlegion.com/tag/feign</link>
<description>Powered by Question2Answer</description>
<item>
<title>Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot</title>
<link>https://coderlegion.com/17273/syncing-the-real-market-consuming-brapi-and-feeding-redis-with-spring-boot</link>
<description>&lt;p&gt;Hello everyone!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, today we'll talk about an essential component on the B3 side of our simulator: the &lt;strong&gt;B3 Market Sync API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;b3-market-sync-api&lt;/code&gt; does: &lt;strong&gt;fetches real asset prices from brapi.dev and stores them in Redis&lt;/strong&gt;, ready for real-time consumption.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-the-market-sync-&quot;&gt;️ What is the Market Sync?&lt;/h2&gt;
&lt;p&gt;This microservice has a single, well-defined responsibility: to be the &lt;strong&gt;market data producer&lt;/strong&gt; of the ecosystem. It operates completely asynchronously and independently — no other service needs to call it directly.&lt;/p&gt;
&lt;p&gt;The flow is simple and elegant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[brapi.dev] ◀── Feign Client ── [MarketSyncScheduler]
                                         │
                              market:price:{TICKER}
                                         │
                                      [Redis]
                                         │
                              [B3 Matching Engine] (next post)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvp-focus&quot;&gt;MVP Focus&lt;/h2&gt;
&lt;p&gt;The goal here is clear: &lt;strong&gt;have real prices available in Redis with low latency&lt;/strong&gt;. 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.&lt;/p&gt;
&lt;p&gt;In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheduled sync with market hours guard&lt;/li&gt;
&lt;li&gt;Redis cache with TTL to prevent stale data&lt;/li&gt;
&lt;li&gt;External API rate limit protection&lt;/li&gt;
&lt;li&gt;Critical bug fixes found during code review&lt;/li&gt;
&lt;li&gt;REST API for querying cached prices&lt;/li&gt;
&lt;li&gt;Swagger documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Java 21&lt;/strong&gt; + &lt;strong&gt;Spring Boot 3.5.11&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Cloud OpenFeign&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Declarative HTTP client for brapi.dev&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data Redis&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;High-performance cache&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Scheduling&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Periodic synchronization&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Jackson JSR310&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java 8 Date/Time serialization support&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;SpringDoc OpenAPI&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Swagger UI documentation&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-implementation-pillars&quot;&gt;️ Implementation Pillars&lt;/h2&gt;
&lt;h3 id=&quot;1-feign-client-consuming-brapi&quot;&gt;1. Feign Client — Consuming Brapi&lt;/h3&gt;
&lt;p&gt;Spring Cloud OpenFeign lets you declare an HTTP client as a simple interface, with no boilerplate code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@FeignClient(name = &quot;brapiClient&quot;, url = &quot;${app.brapi.url}&quot;)
public interface BrapiClient {

    @GetMapping(&quot;/quote/{ticker}&quot;)
    BrapiResponseDTO getQuote(
        @PathVariable String ticker,
        @RequestParam String token
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URL and token are injected via &lt;code&gt;application.yaml&lt;/code&gt;, keeping the code clean and environment-configurable.&lt;/p&gt;
&lt;h3 id=&quot;2-scheduler-with-market-hours-guard&quot;&gt;2. Scheduler with Market Hours Guard&lt;/h3&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Scheduled(fixedRateString = &quot;${app.sync.interval:1800000}&quot;)
public void sync() {
    if (!isMarketOpen()) {
        log.info(&quot;Sync aborted: market is closed (outside trading hours or weekend).&quot;);
        return;
    }
    // ... fetch and store prices
}

private boolean isMarketOpen() {
    ZonedDateTime now = ZonedDateTime.now(ZoneId.of(&quot;America/Sao_Paulo&quot;));
    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 &amp;gt;= 10 &amp;amp;&amp;amp; hour &amp;lt; 18;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important detail:&lt;/strong&gt; we use &lt;code&gt;ZonedDateTime&lt;/code&gt; with &lt;code&gt;America/Sao_Paulo&lt;/code&gt; explicitly. Inside a Docker container, the JVM may default to UTC — which would make the market hours guard behave incorrectly. We also pass &lt;code&gt;-Duser.timezone=America/Sao_Paulo&lt;/code&gt; in the Dockerfile &lt;code&gt;ENTRYPOINT&lt;/code&gt; to ensure the JVM honors the correct timezone:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;ENTRYPOINT [&quot;java&quot;, &quot;-Duser.timezone=America/Sao_Paulo&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-redis-cache-with-correct-ttl&quot;&gt;3. Redis Cache with Correct TTL&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;MarketDataService&lt;/code&gt; stores each quote in Redis with a standardized key and 5-minute TTL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;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(&quot;Ticker {} updated in cache: R$ {}&quot;, quote.getTicker(), quote.getRegularMarketPrice());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id=&quot;4-epoch-timestamp-deserialization&quot;&gt;4. Epoch Timestamp Deserialization&lt;/h3&gt;
&lt;p&gt;A subtle bug I found: brapi.dev returns the &lt;code&gt;regularMarketTime&lt;/code&gt; field as a &lt;strong&gt;Unix epoch&lt;/strong&gt; (integer), but Jackson was trying to deserialize it as &lt;code&gt;LocalDateTime&lt;/code&gt; (ISO string). This causes silent deserialization failure.&lt;/p&gt;
&lt;p&gt;The solution was a custom deserializer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class EpochToLocalDateTimeDeserializer extends StdDeserializer&amp;lt;LocalDateTime&amp;gt; {

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

    @Override
    public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
            throws IOException {
        long epoch = parser.getLongValue();
        return LocalDateTime.ofInstant(Instant.ofEpochSecond(epoch), SAO_PAULO);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And annotating it on the DTO:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@JsonDeserialize(using = EpochToLocalDateTimeDeserializer.class)
private LocalDateTime regularMarketTime;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-rate-limit-protection&quot;&gt;5. Rate Limit Protection&lt;/h3&gt;
&lt;p&gt;Since we use the brapi.dev free plan, we added a delay between calls to avoid throttling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;for (String ticker : brapiProperties.getTickers()) {
    try {
        BrapiResponseDTO response = brapiClient.getQuote(ticker.trim(), brapiProperties.getToken());
        if (nonNull(response.getResults()) &amp;amp;&amp;amp; !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(&quot;Failed to sync ticker {}: {}&quot;, ticker, e.getMessage());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The per-ticker &lt;code&gt;try/catch&lt;/code&gt; is essential — if one asset fails, the loop continues for the rest.&lt;/p&gt;
&lt;h3 id=&quot;6-rest-api-swagger&quot;&gt;6. REST API + Swagger&lt;/h3&gt;
&lt;p&gt;Although the Matching Engine consumes prices directly from Redis, we added REST endpoints to make debugging and observability easier:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Method &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Endpoint &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/quotes&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List prices for all configured tickers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;GET &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/api/v1/quotes/{ticker}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Get price for a specific ticker&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Documented via Swagger UI at &lt;code&gt;http://localhost:8096/swagger-ui.html&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-token-out-of-code&quot;&gt;Security — Token Out of Code&lt;/h2&gt;
&lt;p&gt;A critical issue I fixed: the brapi.dev token was hardcoded as a default value in &lt;code&gt;application.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# ❌ Before — token exposed in public repository
token: ${BRAPI_TOKEN:mFk9YRLijAtxsP5y1XxxxX}

# ✅ After — no fallback, variable is required
token: ${BRAPI_TOKEN}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Never expose credentials as default values in public repositories. The application now fails on startup if the variable is not provided — correct behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the application running locally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Tomcat started on port &lt;code&gt;8096&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Application initialized in under 3 seconds&lt;/li&gt;
&lt;li&gt;✅ Scheduler fired and logged: &lt;code&gt;Sync aborted: market is closed (outside trading hours or weekend)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Swagger UI accessible at &lt;code&gt;http://localhost:8096/swagger-ui.html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;b3-market-sync-api&lt;/code&gt; 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 &lt;strong&gt;B3 Matching Engine&lt;/strong&gt; — the engine that will consume these prices and decide whether the broker's orders are executed or rejected.&lt;/p&gt;
&lt;p&gt;Got any questions about the Redis integration or the market hours guard? Drop them in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/from-stream-to-database-processing-market-data-with-spring-boot-redis-and-flyway-35m9&quot;&gt;From Stream to Database: Processing Market Data with Spring Boot, Redis and Flyway&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;➡️ Next Post: The Heart of B3: Building the Matching Engine with RabbitMQ and Redis &lt;em&gt;(coming soon)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-b3-market-sync&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17273/syncing-the-real-market-consuming-brapi-and-feeding-redis-with-spring-boot</guid>
<pubDate>Sun, 10 May 2026 11:03:34 +0000</pubDate>
</item>
</channel>
</rss>