<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged spring</title>
<link>https://coderlegion.com/tag/spring</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Heart of B3: Building the Matching Engine with RabbitMQ, Redis and Spring Boot</title>
<link>https://coderlegion.com/17341/the-heart-of-b3-building-the-matching-engine-with-rabbitmq-redis-and-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, we've reached one of the most anticipated components of the ecosystem: the &lt;strong&gt;B3 Matching Engine API&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the previous post, we built &lt;code&gt;b3-market-sync-api&lt;/code&gt; which synchronizes real prices from brapi.dev and stores them in Redis. Now it's time to use that data — this is the service that simulates the stock exchange itself, deciding whether a buy or sell order will be &lt;strong&gt;executed or rejected&lt;/strong&gt; based on the prices we just put in cache.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-what-is-the-matching-engine-&quot;&gt;️ What is the Matching Engine?&lt;/h2&gt;
&lt;p&gt;In the real world, when you place a buy order through your broker, it gets sent to B3, which checks whether there's a counterpart willing to trade at that price. This process is called &lt;strong&gt;matching&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In our simulator, the &lt;code&gt;b3-matching-engine-api&lt;/code&gt; reproduces this logic in a simplified way:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Intake&lt;/strong&gt;: Receives the order from the broker via RabbitMQ (queue &lt;code&gt;mq-broker-to-b3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Price&lt;/strong&gt;: Fetches the current asset price from Redis (&lt;code&gt;market:price:{TICKER}&lt;/code&gt;) — data injected by the Market Sync we built previously&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decision&lt;/strong&gt;: Applies the matching rule&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistence&lt;/strong&gt;: Records the result in PostgreSQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notification&lt;/strong&gt;: Returns the result to the broker via RabbitMQ (queue &lt;code&gt;mq-b3-to-broker&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[Broker] ──RabbitMQ──▶ [mq-broker-to-b3] ──▶ [Matching Engine]
                                                      │
                                             Redis: market:price:{TICKER}
                                             (fed by Market Sync ⬆️)
                                                      │
                                         ┌────────────┴────────────┐
                                      FILLED                   REJECTED
                                         │                         │
                                   PostgreSQL                 PostgreSQL
                                         │                         │
                               [mq-b3-to-broker] ◀────────────────┘
                                         │
                                      [Broker]
&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;Before diving into the code, same disclaimer as previous posts: &lt;strong&gt;we're building the foundation&lt;/strong&gt;. The goal is to have an end-to-end flow working with enough robustness to validate the POC.&lt;/p&gt;
&lt;p&gt;In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;End-to-end flow working correctly&lt;/li&gt;
&lt;li&gt;Proper failure handling (no orders silently disappearing)&lt;/li&gt;
&lt;li&gt;Dead Letter Queue for messages that fail during processing&lt;/li&gt;
&lt;li&gt;REST API for querying execution history&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&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 RabbitMQ&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order intake and result notification&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 lookup&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Spring Data JPA&lt;/strong&gt; + &lt;strong&gt;PostgreSQL&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Execution persistence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Flyway&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Database schema versioning&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-rabbitmq-configuration&quot;&gt;1. RabbitMQ Configuration&lt;/h3&gt;
&lt;p&gt;A critical lesson learned here: &lt;strong&gt;never rely on queues already existing in the broker&lt;/strong&gt;. If the application starts before RabbitMQ has the queues created, the consumer fails on startup.&lt;/p&gt;
&lt;p&gt;The solution is to declare all infrastructure beans directly in Spring — it ensures queues, exchanges and bindings exist before any message is processed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public DirectExchange exchange() {
    return new DirectExchange(exchangeName);
}

@Bean
public Queue queueIn() {
    return QueueBuilder.durable(queueIn)
            .withArgument(&quot;x-dead-letter-exchange&quot;, dlxName)
            .withArgument(&quot;x-dead-letter-routing-key&quot;, dlqName)
            .build();
}

@Bean
public Binding bindingQueueIn(Queue queueIn, DirectExchange exchange) {
    return BindingBuilder.bind(queueIn).to(exchange).with(routingKey);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;withArgument&lt;/code&gt; calls — they configure the &lt;strong&gt;Dead Letter Queue&lt;/strong&gt; directly in the main queue declaration. Any message that fails processing is automatically redirected.&lt;/p&gt;
&lt;h3 id=&quot;2-the-matching-logic&quot;&gt;2. The Matching Logic&lt;/h3&gt;
&lt;p&gt;The heart of the service. The rule is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BUY&lt;/strong&gt;: If the price the user accepts to pay &amp;gt;= market price → &lt;code&gt;FILLED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SELL&lt;/strong&gt;: If the price the user wants to receive &amp;lt;= market price → &lt;code&gt;FILLED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Otherwise → &lt;code&gt;REJECTED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;private static boolean isCanExecute(OrderEventDTO order, BigDecimal marketPrice) {
    if (SideStatus.BUY.name().equalsIgnoreCase(order.getSide())) {
        return order.getPrice().compareTo(marketPrice) &amp;gt;= 0;
    } else if (SideStatus.SELL.name().equalsIgnoreCase(order.getSide())) {
        return order.getPrice().compareTo(marketPrice) &amp;lt;= 0;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-failure-handling-no-silent-order-loss&quot;&gt;3. Failure Handling — No Silent Order Loss&lt;/h3&gt;
&lt;p&gt;A silent bug I identified: when the ticker wasn't in Redis, the method simply returned without doing anything. The broker would wait for a response that never came.&lt;/p&gt;
&lt;p&gt;The fix was simple but critical — any failure must notify the broker:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;if (marketDataOpt.isEmpty()) {
    log.warn(&quot;Price for ticker {} not found in Redis. Rejecting order {}&quot;,
             order.getTicker(), order.getOrderId());
    OrderResponseEvent response = new OrderResponseEvent(
        order.getOrderId(), ExecutionStatus.REJECTED.name(), BigDecimal.ZERO);
    orderProducer.sendToBroker(response);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt; This is exactly why we built &lt;code&gt;b3-market-sync-api&lt;/code&gt; first. If Redis doesn't have the price, the order is immediately rejected — correct and predictable behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;4-transactional-guarantee&quot;&gt;4. Transactional Guarantee&lt;/h3&gt;
&lt;p&gt;Another critical point: persisting to PostgreSQL and publishing to RabbitMQ are two distinct operations. If the database saves but the publish fails, the execution is recorded but the broker is never notified — silent inconsistency.&lt;/p&gt;
&lt;p&gt;The solution was adding &lt;code&gt;@Transactional&lt;/code&gt; to the method that orchestrates both operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Transactional
private void saveAndNotify(OrderEventDTO order, BigDecimal price, ExecutionStatus status) {
    // 1. Persists in PostgreSQL
    OrderExecution execution = OrderExecution.builder()
            .orderId(order.getOrderId())
            .ticker(order.getTicker())
            .side(SideStatus.valueOf(order.getSide()))
            .quantity(order.getQuantity())
            .executedPrice(price)
            .status(status)
            .build();
    repository.save(execution);

    // 2. Notifies the Broker
    OrderResponseEvent response = new OrderResponseEvent(
        order.getOrderId(), status.name(), price);
    orderProducer.sendToBroker(response);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;5-dead-letter-queue&quot;&gt;5. Dead Letter Queue&lt;/h3&gt;
&lt;p&gt;For messages that fail in the consumer (unexpected processing error), we configured a full DLQ setup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public DirectExchange deadLetterExchange() {
    return new DirectExchange(dlxName);
}

@Bean
public Queue deadLetterQueue() {
    return QueueBuilder.durable(dlqName).build();
}

@Bean
public Binding bindingDlq(Queue deadLetterQueue, DirectExchange deadLetterExchange) {
    return BindingBuilder.bind(deadLetterQueue)
            .to(deadLetterExchange)
            .with(dlqName);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the consumer, we rethrow the exception so RabbitMQ knows the message failed and routes it to the DLQ:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@RabbitListener(queues = &quot;${app.rabbitmq.queue-in}&quot;)
public void receiveOrder(OrderEventDTO event) {
    log.info(&quot;New order received: ID {} | Ticker {} | Side {}&quot;,
            event.getOrderId(), event.getTicker(), event.getSide());
    try {
        matchingService.process(event);
    } catch (Exception e) {
        log.error(&quot;Failed to process order {}: {}&quot;, event.getOrderId(), e.getMessage(), e);
        throw e; // RabbitMQ routes to DLQ
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;6-rest-api-swagger&quot;&gt;6. REST API + Swagger&lt;/h3&gt;
&lt;p&gt;Since the service exposes query endpoints, I configured &lt;strong&gt;Swagger UI&lt;/strong&gt; from the start:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Bean
public OpenAPI customOpenAPI() {
    return new OpenAPI()
            .info(new Info()
                    .title(&quot;B3 Matching Engine API&quot;)
                    .version(&quot;1.0.0&quot;)
                    .description(&quot;Simulates the B3 stock exchange matching engine...&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Available endpoints:&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/executions&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;List all executions&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/executions/order/{orderId}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by order ID&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/executions/ticker/{ticker}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by ticker&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/executions/status/{status}&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Find by status&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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, all components started correctly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Flyway applied the &lt;code&gt;order_executions&lt;/code&gt; table migration&lt;/li&gt;
&lt;li&gt;✅ Hibernate validated the schema&lt;/li&gt;
&lt;li&gt;✅ RabbitMQ connected with queues and DLQ declared&lt;/li&gt;
&lt;li&gt;✅ Tomcat running on port &lt;code&gt;8091&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ Swagger UI accessible at &lt;code&gt;http://localhost:8091/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; feeding Redis and &lt;code&gt;b3-matching-engine-api&lt;/code&gt; ready to process orders, the entire B3 side is operational.&lt;/p&gt;
&lt;p&gt;The next step is building the &lt;strong&gt;broker-order-api&lt;/strong&gt; — the orchestrator on the broker side that will manage the complete order lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Receive the buy/sell intent from the user&lt;/li&gt;
&lt;li&gt;Validate the ticker via REST against &lt;code&gt;broker-asset-api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save the order as &lt;code&gt;PENDING&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Publish the event to Kafka&lt;/li&gt;
&lt;li&gt;Send the order to the Matching Engine via RabbitMQ&lt;/li&gt;
&lt;li&gt;Consume the feedback and update the final status&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once that service is ready, we'll have the first complete end-to-end flow running in the ecosystem.&lt;/p&gt;
&lt;p&gt;Got any questions about the matching logic or RabbitMQ DLQ configuration? 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;#&quot;&gt;Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot&lt;/a&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-matching-engine&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17341/the-heart-of-b3-building-the-matching-engine-with-rabbitmq-redis-and-spring-boot</guid>
<pubDate>Sun, 10 May 2026 21:29:07 +0000</pubDate>
</item>
<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-1&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-1&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-1&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-1&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-1&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--1&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-1&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>
<item>
<title>Improving Maven's dependency:analyze... or not</title>
<link>https://coderlegion.com/1430/improving-mavens-dependency-analyze-or-not</link>
<description>&lt;p&gt;Recently, my good friend Richard Fichtner advised using the &lt;code&gt;mvn dependency:analyze&lt;/code&gt; command to get rid of declared but unused dependencies:&lt;/p&gt;
&lt;blockquote class=&quot;bluesky-embed&quot; data-bluesky-uri=&quot;at://did:plc:cc2k5egfzqpf3nbjrs5xox4r/app.bsky.feed.post/3lcxfnsc2h62m&quot; data-bluesky-cid=&quot;bafyreifttkfkj7y22l5ezheiy5jclf4aooekvw5rirlprupknwtk7fklxq&quot;&gt;&lt;p lang=&quot;&quot;&gt;There is another use case for

mvn dependency:analyze

It can show you the dependencies you use in your code but have not declared in your pom.xml. This works because you have a transitive dependency on your classpath. Either don't use the dependency or declare it.&lt;br&gt;&lt;br&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bsky.app/profile/did:plc:cc2k5egfzqpf3nbjrs5xox4r/post/3lcxfnsc2h62m?ref_src=embed&quot;&gt;[image or embed]&lt;/a&gt;&lt;/p&gt;&lt;div&gt;— Richard Fichtner ????☕ (&lt;/div&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bsky.app/profile/did:plc:cc2k5egfzqpf3nbjrs5xox4r?ref_src=embed&quot;&gt;@richard.fichtner.dev&lt;/a&gt;&lt;div&gt;) &lt;/div&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://bsky.app/profile/did:plc:cc2k5egfzqpf3nbjrs5xox4r/post/3lcxfnsc2h62m?ref_src=embed&quot;&gt;December 10, 2024 at 2:00 PM&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;While it was a great idea years ago, it's dangerous advice today. In this post, I'd like to explain what the plugin does and why you shouldn't use it but in the most straightforward projects.&lt;/p&gt;
&lt;h3 id=&quot;the-mvn-dependency-analyze-command&quot;&gt;The &lt;code&gt;mvn dependency:analyze&lt;/code&gt; command&lt;/h3&gt;
&lt;p&gt;Maven uses a plugin architecture; in the above command, the plugin is &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/apache/maven-dependency-plugin&quot;&gt;maven-dependency-plugin&lt;/a&gt;. A plugin hosts several related &lt;em&gt;goals&lt;/em&gt;. Here, it's &lt;code&gt;analyze&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&amp;gt;Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused and declared. This goal is intended to be used standalone, thus it always executes the &lt;code&gt;test-compile&lt;/code&gt; phase - use the&lt;code&gt; dependency:analyze-only&lt;/code&gt; goal instead when participating in the build lifecycle.&lt;br&gt;
&amp;gt;By default, &lt;code&gt;maven-dependency-analyzer&lt;/code&gt; is used to perform the analysis, with limitations due to the fact that it works at bytecode level, but any analyzer can be plugged in through &lt;code&gt;analyzer&lt;/code&gt; parameter.&lt;br&gt;
&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://maven.apache.org/plugins/maven-dependency-plugin/analyze-mojo.html&quot;&gt;dependency:analyze&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;maven-dependency-analyzer&lt;/code&gt; is a shared Maven component. Its description is quite descriptive:&lt;/p&gt;
&lt;p&gt;&amp;gt;Analyzes the dependencies of a project for undeclared or unused artifacts.&lt;br&gt;
&amp;gt;&lt;em&gt;Warning&lt;/em&gt;: Because analysis is done on the bytecode rather than the source, some cases are not detected including constants, annotations with source-only retention, and links in Javadoc. This can lead to incorrect results when these are the only uses of a dependency.&lt;br&gt;
&amp;gt;The main component is &lt;code&gt;ProjectDependencyAnalyzer&lt;/code&gt;, which uses &lt;code&gt;ClassAnalyzer&lt;/code&gt; and &lt;code&gt;DependencyAnalyzer&lt;/code&gt;.&lt;br&gt;
&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://maven.apache.org/shared/maven-dependency-analyzer/&quot;&gt;maven-dependency-analyzer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The warning clearly shows that it works at the &lt;em&gt;bytecode&lt;/em&gt; level. In particular, it explicitly mentions that it doesn't consider source-level annotations.&lt;/p&gt;
&lt;h3 id=&quot;spring-boot-starters&quot;&gt;Spring Boot starters&lt;/h3&gt;
&lt;p&gt;I described how to &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/designing-your-own-spring-boot-starter/1/&quot;&gt;design your own&lt;/a&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/designing-your-own-spring-boot-starter/2/&quot;&gt;Spring Boot starter&lt;/a&gt; a long time ago, and it didn't change a lot since then. If you're new to Spring Boot starters, here's a summary.&lt;/p&gt;
&lt;p&gt;SpringBoot relies on AutoConfiguration classes. AutoConfiguration classes are regular configuration classes, &lt;em&gt;i.e.&lt;/em&gt;, they contribute to the application classes. You can set specific activation criteria, such as the presence of a Spring property, but these are not specific to auto-configuration.&lt;/p&gt;
&lt;p&gt;Here's a very simplified flow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.frankel.ch/assets/generated/maven-dependency-analyze/spring-autoconfiguration-init.svg&quot; alt=&quot;Simplified sequence diagram of the auto-configuration in Spring Boot&quot;&gt;&lt;/p&gt;
&lt;p&gt;The JAR that automatically comes with Spring Boot is &lt;code&gt;org.springframework.boot:spring-boot-autoconfigure&lt;/code&gt;. You can check the content of its &lt;code&gt;META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As an example, here's the &lt;code&gt;RestClientAutoConfiguration&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@AutoConfiguration(after = { HttpClientAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) //1
@ConditionalOnClass(RestTemplate.class)                //2
@Conditional(NotReactiveWebApplicationCondition.class) //3
public class RestTemplateAutoConfiguration {

    // Class body
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Set the order of auto-configuration classes&lt;/li&gt;
&lt;li&gt;Activate if the &lt;code&gt;RestTemplate&lt;/code&gt; class is on the classpath&lt;/li&gt;
&lt;li&gt;Activate if we aren't in a reactive web app context&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that the class loader loads the &lt;code&gt;RestTemplateAutoConfiguration&lt;/code&gt; class just fine, &lt;em&gt;regardless of whether the &lt;code&gt;RestTemplate&lt;/code&gt; class is on the classpath or not&lt;/em&gt;! Spring leverages this mechanism to its fullest, as seen above. In effect, the resolution of classes configured in annotations is deferred until they are &lt;em&gt;explicitly&lt;/em&gt; accessed.&lt;/p&gt;
&lt;h3 id=&quot;bringing-the-maven-dependency-analyzer-into-the-modern-age&quot;&gt;Bringing the &lt;code&gt;maven-dependency-analyzer&lt;/code&gt; into the modern age&lt;/h3&gt;
&lt;p&gt;Committers designed the analyzer in 2007: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/apache/maven-dependency-analyzer/tree/b448d95daba17db67bc071eab9a1dd2457b77cab&quot;&gt;here's&lt;/a&gt; how it looked like then. Spring Boot started later, in 2010. For this reason, the analyzer didn't take deferred class loading in annotations. Note that this is still not the case; the project doesn't get a lot of love.&lt;/p&gt;
&lt;p&gt;When using the plugin on a Spring Boot project, you'll get a lot of false positives. I tried it with a simple Spring Boot project, using WebFlux and R2DBC on PostgreSQL.&lt;/p&gt;
&lt;p&gt;Here's a slight excerpt of the output when I run &lt;code&gt;mvn analyze:dependencies&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[WARNING] Unused declared dependencies found:
[WARNING]   org.springframework.boot:spring-boot-starter-data-r2dbc:jar:3.4.0:compile
[WARNING]   org.testcontainers:postgresql:jar:1.20.4:test
[WARNING]   org.testcontainers:r2dbc:jar:1.20.4:test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I remove any of these dependencies, tests don't run.&lt;/p&gt;
&lt;p&gt;What would be necessary to make the analyzer work with Spring Boot projects?&lt;br&gt;
Let's analyze the analyzer.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.frankel.ch/assets/generated/maven-dependency-analyze/analyzer-class-diagram.svg&quot; alt=&quot;Analyzer class diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;The plugin allows configuring another analyzer:&lt;/p&gt;
&lt;p&gt;&amp;gt;&lt;strong&gt;&lt;/strong&gt;&lt;br&gt;
&amp;gt;Specify the project dependency analyzer to use (plexus component role-hint). By default, maven-dependency-analyzer is used. To use this, you must declare a dependency for this plugin that contains the code for the analyzer. The analyzer must have a declared Plexus role name, and you specify the role name here.&lt;br&gt;
&amp;gt;&lt;em&gt; &lt;strong&gt;Type&lt;/strong&gt;: &lt;code&gt;java.lang.String&lt;/code&gt;&lt;br&gt;
&amp;gt;&lt;/em&gt; &lt;strong&gt;Since&lt;/strong&gt;: &lt;code&gt;2.2&lt;/code&gt;&lt;br&gt;
&amp;gt;&lt;em&gt; &lt;strong&gt;Required&lt;/strong&gt;: &lt;code&gt;No&lt;/code&gt;&lt;br&gt;
&amp;gt;&lt;/em&gt; &lt;strong&gt;User Property&lt;/strong&gt;: &lt;code&gt;analyzer&lt;/code&gt;&lt;br&gt;
&amp;gt;* &lt;strong&gt;Default&lt;/strong&gt;: &lt;code&gt;default&lt;/code&gt;&lt;br&gt;
&amp;gt;-- &lt;a rel=&quot;nofollow&quot; href=&quot;https://maven.apache.org/plugins/maven-dependency-plugin/analyze-mojo.html#analyzer&quot;&gt;dependency:analyze&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can create an overall analyzer that reuses the above but adds one specific to Spring Boot.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The current state of the Maven analyzer doesn't offer any benefit to modern Spring Boot projects. The existing code is open to configuration and even extension. However, we would need to embed a lot of Spring Boot logic. For Quarkus and Micronaut projects, we would require dedicated code as well.&lt;/p&gt;
&lt;p&gt;I don't know if it's worth the time and effort. If you think it is, I hope this blog post can serve as an early-stage analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;To go further:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://maven.apache.org/plugins/maven-dependency-plugin/analyze-mojo.html&quot;&gt;dependency:analyze&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://maven.apache.org/shared/maven-dependency-analyzer/&quot;&gt;Maven Dependency Analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/designing-your-own-spring-boot-starter/1/&quot;&gt;Designing your own Spring Boot starter – part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/designing-your-own-spring-boot-starter/2/&quot;&gt;Designing your own Spring Boot starter – part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.frankel.ch/maven-dependency-analyze/&quot;&gt;A Java Geek&lt;/a&gt; on March 9&lt;sup&gt;th&lt;/sup&gt;, 2025&lt;/em&gt;&lt;/p&gt;
</description>
<category>Java</category>
<guid isPermaLink="true">https://coderlegion.com/1430/improving-mavens-dependency-analyze-or-not</guid>
<pubDate>Mon, 10 Mar 2025 15:55:22 +0000</pubDate>
</item>
<item>
<title>Spring Data Neo4j: How to update an entity</title>
<link>https://coderlegion.com/1306/spring-data-neo4j-how-to-update-an-entity</link>
<description>&lt;p&gt;After working on a new &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphacademy.neo4j.com/courses/app-spring-data/&quot;&gt;online Spring Data Neo4j course&lt;/a&gt;, I learned a couple more things about updating an entity. The course required a different set of scenarios than outlined in my previous &lt;a rel=&quot;nofollow&quot; href=&quot;https://jmhreif.com/blog/2023/sdn-cypher-update-entity/&quot;&gt;SDN update blog post&lt;/a&gt;, so I wanted to cover those scenarios, as well.&lt;/p&gt;
&lt;h3 id=&quot;spring-save-method&quot;&gt;Spring save() method&lt;/h3&gt;
&lt;p&gt;First up is the out-of-the-box &lt;code&gt;save()&lt;/code&gt; method that is provided by Spring as a default. This method takes an input of the entity object you want to save to the database.&lt;/p&gt;
&lt;p&gt;Here is an example of what this looks like with a popular movie data set as the model.&lt;/p&gt;
&lt;p&gt;Movie domain class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Node
public class Movie {
     @Id
     private String movieId;
    
     private String title;
     private String plot;
     private String poster;
     private String url;
     private String released;
     private Long imdbVotes;
    
     @Relationship(value = &quot;ACTED_IN&quot;, direction = Relationship.Direction.INCOMING)
     private List&amp;lt;Role&amp;gt; actors;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Movie controller class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/movies&quot;)
public class MovieController {
    //inject repository + constructor

    @PostMapping(&quot;/save&quot;)
    Movie save(@RequestBody Movie movie) {
        return movieRepo.save(movie);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works well to save a new movie, as you can provide a subset of properties or all properties, as long as the &lt;code&gt;id&lt;/code&gt; is present, and anything you don't provide will be set to &lt;code&gt;null&lt;/code&gt;. However, the issue arises when you need to update an existing entity because it means any attributes you do not provide will be &lt;em&gt;overwritten&lt;/em&gt; to &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To better understand how this works, let's see it in action by saving a movie, and then trying to update it.&lt;/p&gt;
&lt;p&gt;Movie request object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;movieId&quot;: &quot;9876&quot;,
  &quot;title&quot;: &quot;MyMovie&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save Movie object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http &quot;:8080/movies/save&quot; @src/main/resources/movie.json
{
    &quot;actors&quot;: null,
    &quot;imdbId&quot;: null,
    &quot;imdbVotes&quot;: null,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: null,
    &quot;poster&quot;: null,
    &quot;title&quot;: &quot;MyMovie&quot;,
    &quot;url&quot;: null,
    &quot;year&quot;: null
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let's use the same method to try to update the movie with a &lt;code&gt;year&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;MovieUpdated object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;movieId&quot;: &quot;9876&quot;,
  &quot;year&quot;: 2018
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save updated Movie object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http &quot;:8080/movies/save&quot; @src/main/resources/movieUpdated.json
{
    &quot;actors&quot;: null,
    &quot;imdbId&quot;: null,
    &quot;imdbVotes&quot;: null,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: null,
    &quot;poster&quot;: null,
    &quot;title&quot;: null,
    &quot;url&quot;: null,
    &quot;year&quot;: 2018
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the output above, you can see that the &lt;code&gt;title&lt;/code&gt; property is &lt;code&gt;null&lt;/code&gt;, and the &lt;code&gt;year&lt;/code&gt; property is populated. This is because the title is not specified in the updated JSON object, so it is overwritten to &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This may not be too big of an effort if you have only a few attributes in your domain class, although I would still find it frustrating to include all properties for only updating one or two properties. In that case, I would need to resort to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://jmhreif.com/blog/2023/sdn-cypher-update-entity/&quot;&gt;POJO method outlined in my previous SDN update blog post&lt;/a&gt; where you pull the database entity, set each property, and then save the entity. The added business logic seems like an unnecessary maintenance headache.&lt;/p&gt;
&lt;p&gt;What if you wanted to preserve what was already there without providing all the properties defined in the domain class to the request? In these scenarios, there are a couple of other options at your disposal, though neither allow dynamic updates to random properties per request.&lt;/p&gt;
&lt;h3 id=&quot;patch-year&quot;&gt;Patch Year&lt;/h3&gt;
&lt;p&gt;The first option is that you don't have to set all properties if you use a PATCH method and only set the values you want to update. Here is an example.&lt;/p&gt;
&lt;p&gt;Movie controller class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/movies&quot;)
public class MovieController {
    //inject repository + constructor

    @PatchMapping(&quot;/patchYear&quot;)
    Movie patchYear(@RequestBody Movie moviePatch) {
        Movie existingMovie = movieRepository.findById(moviePatch.getMovieId()).get();
        existingMovie.setYear(moviePatch.getYear());

        return movieRepository.save(existingMovie);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PatchYear object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;movieId&quot;: &quot;9876&quot;,
  &quot;year&quot;: 2024
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Patch movie year:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http PATCH &quot;:8080/movies/patchYear&quot; @src/main/resources/moviePatch.json

{
    &quot;budget&quot;: null,
    &quot;countries&quot;: null,
    &quot;imdbId&quot;: null,
    &quot;imdbRating&quot;: null,
    &quot;imdbVotes&quot;: null,
    &quot;languages&quot;: null,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: null,
    &quot;poster&quot;: null,
    &quot;released&quot;: null,
    &quot;revenue&quot;: null,
    &quot;runtime&quot;: null,
    &quot;title&quot;: &quot;MyMovie&quot;,
    &quot;tmdbId&quot;: null,
    &quot;url&quot;: null,
    &quot;year&quot;: 2024
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows you to set specific values without overwriting other property values to &lt;code&gt;null&lt;/code&gt;. You also don't need to set all the values in the movie object programmatically. If you modified the initial &lt;code&gt;save()&lt;/code&gt; method to just include the &lt;code&gt;setYear()&lt;/code&gt; line, it would still overwrite other values. This approach prevents that, although you still have to call &lt;code&gt;setProperty()&lt;/code&gt; for each field you want to update.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; For this approach to work, your domain entity must be a class (not a record) because records are immutable, which means you cannot change (or set/update) fields on the entity instance. For immutable objects, you have to create a new instance of the object and copy property values to the new object before saving.&lt;/p&gt;
&lt;p&gt;You can avoid setting each property on an object and retain existing values with a couple of options covered next.&lt;/p&gt;
&lt;h3 id=&quot;custom-cypher&quot;&gt;Custom Cypher&lt;/h3&gt;
&lt;p&gt;One of the more flexible options is to &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-data/neo4j/reference/appendix/custom-queries.html&quot;&gt;use custom Cypher&lt;/a&gt;. For this, you would write a Cypher statement that sets the new values to the properties you want to update. You can even add/set properties that do not exist on the application's domain class. The negative is that you would need to make changes to the application (Cypher statement) if you wanted to update different properties, so it is not fully dynamic.&lt;/p&gt;
&lt;p&gt;The example below uses the same movie domain but adds a Cypher statement and method to the repository interface to update the &lt;code&gt;year&lt;/code&gt; property without overwriting the &lt;code&gt;title&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Repository interface:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interface MovieRepository extends Neo4jRepository&amp;lt;Movie, String&amp;gt; {
    @Query(&quot;MATCH (m:Movie {movieId: $movieId}) &quot; +
            &quot;SET m.year = toInteger($year) &quot; +
            &quot;RETURN m;&quot;)
    Movie updateYear(String movieId, Long year);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Movie controller class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/movies&quot;)
public class MovieController {
    //inject repository + constructor

    @PatchMapping(&quot;/updateYear&quot;)
    Movie patchYear(@RequestParam String movieId, @RequestParam Long year) {
        return movieRepository.updateYear(movieId, year);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, the following request calls the method and updates the movie's year property.&lt;/p&gt;
&lt;p&gt;Update movie year:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http PATCH &quot;:8080/movies/updateYear?movieId=9876&amp;amp;year=2018&quot;

{
    &quot;actors&quot;: [],
    &quot;imdbId&quot;: null,
    &quot;imdbVotes&quot;: null,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: null,
    &quot;poster&quot;: null,
    &quot;title&quot;: &quot;MyMovie&quot;,
    &quot;url&quot;: null,
    &quot;year&quot;: 2018
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It worked! The movie's title remained the same (not overwritten to &lt;code&gt;null&lt;/code&gt;), and a value was saved for the &lt;code&gt;year&lt;/code&gt; property.&lt;/p&gt;
&lt;p&gt;This ad hoc Cypher approach could work well when values or property updates occur somewhat frequently, as updating the Cypher statement makes updates flexible. You could also make the incoming property generic (&lt;code&gt;value&lt;/code&gt;) and pass in any value (or multiple values) and set whichever properties you'd like by changing the Cypher. While still not completely dynamic, this option is probably the most flexible and dynamic of the list.&lt;/p&gt;
&lt;p&gt;A custom Cypher approach might work well when you need to update certain properties, but if you have a subset of properties that operate together, another option is to create a projection of the domain class.&lt;/p&gt;
&lt;h3 id=&quot;projections&quot;&gt;Projections&lt;/h3&gt;
&lt;p&gt;To provide a consistent set of values for update and leave other properties as-is, &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-data/neo4j/reference/projections/sdn-projections.html&quot;&gt;projections&lt;/a&gt; are probably the nicest option I've found so far. This approach still requires setting consistent properties (like with custom Cypher), but avoids overwriting consistent values by creating a &quot;view&quot; of the larger entity, only working with those values and leaving other field values alone.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt; There are two types of projections - interface and DTO. Interface projections are immutable, which means you cannot update values, but have to create a new object and copy existing values over. DTO objects, then, are more straightforward when dealing with update operations. For this reason, the examples use DTO-based projections.&lt;/p&gt;
&lt;p&gt;There are two different ways to &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-data/neo4j/reference/projections/sdn-projections.html#projections.sdn.persistence&quot;&gt;save a projection&lt;/a&gt; - 1. send the projection (subset of properties) and save into the full domain entity, 2. send a full domain object but only save the projection fields. Really, the difference is the incoming request object, whether you have a smaller set or larger set and only want to save those values.&lt;/p&gt;
&lt;p&gt;Let's see how this operates.&lt;/p&gt;
&lt;h3 id=&quot;projection-as-movie&quot;&gt;Projection as Movie&lt;/h3&gt;
&lt;p&gt;The first example sends a projection object (subset of the full domain object's properties) and saves the trimmed object as the full movie entity. We have defined a projection that only includes the &lt;code&gt;movieId&lt;/code&gt; and &lt;code&gt;plot&lt;/code&gt; properties of a movie.&lt;/p&gt;
&lt;p&gt;MovieDTOProjection class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MovieDTOProjection {
    private String movieId;
    private String plot;

    public String getMovieId() { return movieId; }

    public String getPlot() {
        return plot;
    }

    public void setPlot(String plot) {
        this.plot = plot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MovieController class method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/movies&quot;)
public class MovieController {
    //inject repository + constructor

    //Only updates properties in projection
    @PatchMapping(&quot;/projectionAsMovie&quot;)
    MovieDTOProjection saveProjectionAsMovie(@RequestBody MovieDTOProjection movieDTO) {
        return neo4jTemplate.save(Movie.class).one(movieDTO);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ProjectionAsMovie object (request object):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;movieId&quot;: &quot;9876&quot;,
  &quot;plot&quot;: &quot;Here is the plot.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update Movie with a projection object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http PATCH &quot;:8080/movies/projectionAsMovie&quot; @src/main/resources/projectionAsMovie.json

{
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: &quot;Here is the plot.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full database entity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;plot&quot;: &quot;Here is the plot.&quot;,
    &quot;year&quot;: 2024,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;title&quot;: &quot;MyMovie&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The request successfully updated the entity with the new plot value (&quot;Here is the plot.&quot;) without overwriting the title or year properties on the existing entity! The method in the controller class takes the projection object input and saves it as a &lt;code&gt;Movie&lt;/code&gt; class entity.&lt;/p&gt;
&lt;p&gt;This approach is helpful when you only want to send certain properties over the wire and not the full object. You can set any fields you wish to update in the projection and only send those values in the request.&lt;/p&gt;
&lt;h3 id=&quot;movie-entity-as-projection&quot;&gt;Movie entity as projection&lt;/h3&gt;
&lt;p&gt;This type of projection only saves a defined subset of properties out of a potentially larger or variable object. The save will only update the fields included in the projection and ignore anything else that might be in the request object.&lt;/p&gt;
&lt;p&gt;The example below uses the same &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/sdn-update-entity-round2/blob/main/src/main/java/com/jmhreif/sdn_update_entity_round2/MovieDTOProjection.java&quot;&gt;&lt;code&gt;MovieDTOProjection&lt;/code&gt; class&lt;/a&gt; as the above example. Then, we only need a new method in the controller to save the projection.&lt;/p&gt;
&lt;p&gt;MovieController class method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/movies&quot;)
public class MovieController {
    //inject repository + constructor

    //Only updates properties in projection (ignores other values)
    @PatchMapping(&quot;/movieAsProjection&quot;)
    MovieDTOProjection saveMovieAsProjection(@RequestBody Movie movie) {
        return neo4jTemplate.saveAs(movie, MovieDTOProjection.class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MovieAsProjection object:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;movieId&quot;: &quot;9876&quot;,
  &quot;title&quot;: &quot;TestTitle&quot;,
  &quot;plot&quot;: &quot;Some plot cliche here.&quot;,
  &quot;year&quot;: 2025
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Send Movie object (only save projection values):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% http PATCH &quot;:8080/movies/movieAsProjection&quot; @src/main/resources/movieAsProjection.json

{
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;plot&quot;: &quot;Some plot cliche here.&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Full database entity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;plot&quot;: &quot;Some plot cliche here.&quot;,
    &quot;year&quot;: 2024,
    &quot;movieId&quot;: &quot;9876&quot;,
    &quot;title&quot;: &quot;MyMovie&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This also worked! The controller method accepts a &lt;code&gt;Movie&lt;/code&gt; request object as input and saves it as the projection entity, which only retains the subset of values defined in the projection and ignores the rest.&lt;/p&gt;
&lt;p&gt;This approach would be helpful if you have request objects with varying fields of a &lt;code&gt;Movie&lt;/code&gt; entity, but only want to update a certain subset each time. In terms of the incoming request object, this option seems to be the most flexible in allowing you to provide all kinds of fields as input, and it will only save the relevant ones defined in the projection.&lt;/p&gt;
&lt;h3 id=&quot;wrapping-up-&quot;&gt;Wrapping Up!&lt;/h3&gt;
&lt;p&gt;In this post, we delved a bit deeper into updating entities using Spring Data and Neo4j. A &lt;a rel=&quot;nofollow&quot; href=&quot;https://jmhreif.com/blog/2023/sdn-cypher-update-entity/&quot;&gt;previous blog post on the topic&lt;/a&gt; outlined some examples for that use case at the time (microservices), but I learned about a few more options after working on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphacademy.neo4j.com/courses/app-spring-data/&quot;&gt;GraphAcademy Spring Data Neo4j course&lt;/a&gt;. These options included custom Cypher statements and projections.&lt;/p&gt;
&lt;p&gt;There are still other pieces of the puzzle to explore and integrate, such as optimistic locking (with the &lt;code&gt;@Version&lt;/code&gt; field) and custom repository implementations for extending repositories (e.g. to combine &lt;code&gt;Neo4jRepository&amp;lt;&amp;gt;&lt;/code&gt;, &lt;code&gt;Neo4jTemplate&lt;/code&gt;, and/or &lt;code&gt;Neo4jClient&lt;/code&gt; levels of abstraction), but we will save those for a future blog post. Happy coding!&lt;/p&gt;
&lt;h3 id=&quot;resources&quot;&gt;Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/JMHReif/sdn-update-entity-round2&quot;&gt;sdn-update-entity-round2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Blog post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://jmhreif.com/blog/2023/sdn-cypher-update-entity/&quot;&gt;previous SDN update article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Graphacademy course (free, online, self-paced): &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphacademy.neo4j.com/courses/app-spring-data/&quot;&gt;Spring Data Neo4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-data/neo4j/reference/projections/sdn-projections.html#projections.sdn.persistence&quot;&gt;SDN projection persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.spring.io/spring-data/neo4j/reference/repositories/custom-implementations.html&quot;&gt;SDN custom repository implementations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/1306/spring-data-neo4j-how-to-update-an-entity</guid>
<pubDate>Thu, 27 Feb 2025 20:28:06 +0000</pubDate>
</item>
</channel>
</rss>