<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged flyway</title>
<link>https://coderlegion.com/tag/flyway</link>
<description>Powered by Question2Answer</description>
<item>
<title>From Stream to Database: Processing Market Data with Spring Boot, Redis, and Flyway</title>
<link>https://coderlegion.com/17111/from-stream-to-database-processing-market-data-with-spring-boot-redis-and-flyway</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, today we build &lt;code&gt;trading-broker-asset&lt;/code&gt; — the Java service that &lt;strong&gt;consumes the quotes published to Kafka by trading-broker-market-data&lt;/strong&gt; and transforms them into structured data in MySQL, with high-performance caching in Redis.&lt;/p&gt;
&lt;p&gt;If the Python service is the collector, this one is the processor.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-this-service-s-role-in-the-ecosystem&quot;&gt;️ This Service's Role in the Ecosystem&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[trading-broker-market-data]
         │
         │ Kafka: trading-assets-market-data-v1
         ▼
[trading-broker-asset]
         │
    ┌────┴────┐
    ▼         ▼
  MySQL     Redis
(assets)  market:price:{TICKER}
              │
    [b3-matching-engine]   (reads from here)
    [trading-broker-order] (validates ticker from here)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two critical consumers depend on the data this service maintains: the Matching Engine (for price decisions) and the Order API (to validate whether a ticker exists and is active).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvp-focus&quot;&gt;MVP Focus&lt;/h2&gt;
&lt;p&gt;In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Robust Kafka consumer with error handling&lt;/li&gt;
&lt;li&gt;Asset upsert: creates on first event, updates on subsequent ones&lt;/li&gt;
&lt;li&gt;Redis cache with 10-minute TTL&lt;/li&gt;
&lt;li&gt;REST API for catalog queries&lt;/li&gt;
&lt;li&gt;Status filter: only &lt;code&gt;ACTIVE&lt;/code&gt; assets are returned&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 Kafka&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Quote event consumption&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;MySQL&lt;/strong&gt; + &lt;strong&gt;Flyway&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Versioned asset catalog&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;Real-time price cache&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-the-database-schema-flyway-&quot;&gt;1. The Database Schema (Flyway)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE assets (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    ticker VARCHAR(20) NOT NULL UNIQUE,
    name VARCHAR(200),
    current_price DECIMAL(19, 4),
    last_update DATETIME,
    status ENUM('ACTIVE', 'INACTIVE') NOT NULL DEFAULT 'ACTIVE',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why Flyway?&lt;/strong&gt; Schema versioning ensures that any environment (local, CI, production) has exactly the same database structure, without surprises.&lt;/p&gt;
&lt;h3 id=&quot;2-the-kafka-consumer&quot;&gt;2. The Kafka Consumer&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@KafkaListener(
        topics = &quot;trading-assets-market-data-v1&quot;,
        groupId = &quot;trading-broker-asset&quot;
)
public void consume(AssetMarketDataDTO dto) {
    log.info(&quot;Market data received for ticker: {}&quot;, dto.getTicker());
    try {
        assetService.updateAsset(dto);
    } catch (Exception e) {
        log.error(&quot;Failed to process market data for ticker {}: {}&quot;,
                dto.getTicker(), e.getMessage(), e);
        // Rethrow so Kafka can apply retry policy instead of silently discarding
        throw e;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why rethrow the exception?&lt;/strong&gt; If processing fails silently, the offset is committed and the message is discarded — the asset is never updated. Rethrowing lets Kafka apply its retry policy.&lt;/p&gt;
&lt;h3 id=&quot;3-the-asset-upsert&quot;&gt;3. The Asset Upsert&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
public void updateAsset(AssetMarketDataDTO dto) {
    Asset asset = assetRepository.findByTicker(dto.getTicker())
            .orElse(Asset.builder()
                    .ticker(dto.getTicker())
                    .status(AssetStatus.ACTIVE)
                    .build());

    asset.setName(dto.getName());
    asset.setCurrentPrice(dto.getPrice());
    // Uses real market timestamp from event — not LocalDateTime.now()
    asset.setLastUpdate(dto.getUpdatedAt() != null ? dto.getUpdatedAt() : LocalDateTime.now());

    assetRepository.save(asset);

    // Update Redis cache after persisting
    marketPriceCacheService.updatePrice(dto.getTicker(), dto.getPrice());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important decision:&lt;/strong&gt; we use the real &lt;code&gt;updatedAt&lt;/code&gt; from the Kafka event, not &lt;code&gt;LocalDateTime.now()&lt;/code&gt;. This ensures &lt;code&gt;lastUpdate&lt;/code&gt; in the database reflects when the price was generated by the market, not when it was processed by the service.&lt;/p&gt;
&lt;h3 id=&quot;4-redis-cache-with-ttl&quot;&gt;4. Redis Cache with TTL&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static final Duration CACHE_TTL = Duration.ofMinutes(10);

public void updatePrice(String ticker, BigDecimal price) {
    String key = &quot;market:price:&quot; + ticker;
    // TTL of 10 minutes — prevents stale prices for inactive or delisted assets
    redisTemplate.opsForValue().set(key, price.toString(), CACHE_TTL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why TTL?&lt;/strong&gt; If an asset is deactivated or delisted, its price will expire from Redis in 10 minutes. Without TTL, the Matching Engine could use prices for assets that no longer exist.&lt;/p&gt;
&lt;h3 id=&quot;5-status-filter-in-queries&quot;&gt;5. Status Filter in Queries&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public List&amp;lt;AssetDTO&amp;gt; findAllActive() {
    // Query directly by status — not findAll() + filter in memory
    return assetRepository.findAllByStatus(AssetStatus.ACTIVE).stream()
            .map(AssetDTO::fromEntity)
            .toList();
}

public Optional&amp;lt;AssetDTO&amp;gt; findByTicker(String ticker) {
    // Only returns ACTIVE assets — inactive returns 404
    return assetRepository.findByTickerAndStatus(ticker.toUpperCase(), AssetStatus.ACTIVE)
            .map(AssetDTO::fromEntity);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;trading-broker-order&lt;/code&gt; validates a ticker before creating an order, it calls &lt;code&gt;GET /api/v1/assets/{ticker}&lt;/code&gt;. If the asset is inactive, it gets a 404 and the order is rejected. Correct and predictable.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;rest-api&quot;&gt;REST API&lt;/h2&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/assets&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List all active assets&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/assets/{ticker}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find asset by ticker (404 if inactive)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt; &lt;strong&gt;Swagger UI&lt;/strong&gt;: &lt;code&gt;http://localhost:8083/swagger-ui.html&lt;/code&gt;&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 and &lt;code&gt;trading-broker-market-data&lt;/code&gt; publishing to Kafka:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Consumer connected to the &lt;code&gt;trading-assets-market-data-v1&lt;/code&gt; topic&lt;/li&gt;
&lt;li&gt;✅ Assets being created/updated in MySQL in real time&lt;/li&gt;
&lt;li&gt;✅ Redis being updated with TTL after each event&lt;/li&gt;
&lt;li&gt;✅ Logs showing &lt;code&gt;Updating ticker PETR4 with price R$ 47.37&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 the asset catalog working, the next step is &lt;code&gt;b3-market-sync-api&lt;/code&gt; — which synchronizes prices directly to the B3's Redis — followed by &lt;code&gt;b3-matching-engine-api&lt;/code&gt;, which uses those prices to execute orders.&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/tooling-tips-visualizing-your-data-in-mongodb-and-kafka-1k1p&quot;&gt;Tooling Tips: MongoDB and Kafka&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;➡️ Next Post: Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot&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-broker-asset&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17111/from-stream-to-database-processing-market-data-with-spring-boot-redis-and-flyway</guid>
<pubDate>Fri, 08 May 2026 20:51:06 +0000</pubDate>
</item>
</channel>
</rss>