<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.trinitylogic.co.uk/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.trinitylogic.co.uk/" rel="alternate" type="text/html" hreflang="en-GB" /><updated>2026-04-27T15:39:15+00:00</updated><id>https://www.trinitylogic.co.uk/feed.xml</id><title type="html">Samuel Jackson</title><subtitle>Senior Java Back End Developer with 20+ years of experience — specialising in Betfair Exchange API, Betdaq, Smarkets, and Matchbook integrations, Spring Boot microservices, event-driven architecture, and AWS serverless systems. Available for Java contracting across betting, finance, energy, and government sectors. Based in Lancashire, UK.</subtitle><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><entry><title type="html">Architecture | Hexagonal vs Clean Architecture in Spring Boot</title><link href="https://www.trinitylogic.co.uk/blog/hexagonal-vs-clean-architecture" rel="alternate" type="text/html" title="Architecture | Hexagonal vs Clean Architecture in Spring Boot" /><published>2025-12-20T00:00:00+00:00</published><updated>2025-12-20T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/hexagonal-vs-clean-architecture</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/hexagonal-vs-clean-architecture"><![CDATA[<p>Once you move beyond simple CRUD services, architecture choices start to matter. Two patterns that frequently come up in the same conversation are Hexagonal Architecture and Clean Architecture. They share common goals, but they are not the same — and choosing between them (or blending them) has real consequences for Spring Boot systems.</p>

<p>I’ve applied both across long-lived, high-stakes systems — from real-time financial data pipelines to government benefit platforms — and here’s how I think about the choice.</p>

<h2 id="the-shared-goal">The Shared Goal</h2>

<p>Both architectures aim to:</p>
<ul>
  <li>Isolate business logic from infrastructure concerns</li>
  <li>Prevent framework lock-in</li>
  <li>Improve testability and long-term maintainability</li>
  <li>Support controlled evolution under changing requirements</li>
</ul>

<p>Where they differ is in structure and emphasis.</p>

<h2 id="hexagonal-architecture">Hexagonal Architecture</h2>

<p>Hexagonal Architecture (Ports and Adapters) focuses on <strong>interaction points</strong>. The domain sits at the centre, and everything outside it — HTTP, Kafka, persistence, third-party APIs — is an adapter connected through a port. The model is interaction-driven and maps naturally to event-driven systems.</p>

<p><strong>Strengths:</strong></p>
<ul>
  <li>Excellent fit for Kafka and messaging-heavy platforms</li>
  <li>Simple mental model that scales well to microservices</li>
  <li>Adapters are easy to swap — test doubles replace production implementations cleanly</li>
  <li>Minimal ceremony for small to medium codebases</li>
</ul>

<p><strong>Where I use it:</strong> At DWP Digital and Mosaic Smart Data, Hexagonal Architecture was the natural choice. Multiple entry points (Kafka events, HTTP, scheduled jobs) and the need for domain logic untouched by infrastructure made Ports and Adapters the right foundation.</p>

<h2 id="clean-architecture">Clean Architecture</h2>

<p>Clean Architecture introduces concentric dependency layers with strict inward-pointing rules. It emphasises use cases as first-class citizens alongside entities, and provides more prescriptive guidance on layer responsibilities.</p>

<p><strong>Strengths:</strong></p>
<ul>
  <li>Clear separation of concerns with explicit, enforced layer boundaries</li>
  <li>Strong guidance for large teams where consistency matters</li>
  <li>Use cases as explicit objects make complex domain flows easier to reason about</li>
  <li>Well-suited to large, domain-rich systems with multiple bounded contexts</li>
</ul>

<p><strong>Where I use it:</strong> When domain complexity grows and teams expand, I selectively introduce Clean Architecture concepts — particularly explicit use case objects — to bring additional structure where Hexagonal Architecture alone starts to feel ambiguous.</p>

<h2 id="which-i-default-to-and-why">Which I Default To and Why</h2>

<p>In Spring Boot systems, I start with Hexagonal Architecture. It gives flexibility without unnecessary ceremony, and the port/adapter model maps directly to the Spring component model. When systems grow large, or when teams expand to the point where boundary ambiguity causes friction, I layer in Clean Architecture concepts where they add genuine clarity — not as a rewrite, but as a targeted evolution.</p>

<h2 id="the-pragmatic-view">The Pragmatic View</h2>

<p>The cleanest systems I’ve worked on borrowed from both. Hexagonal Architecture as a foundation, with explicit use case objects from Clean Architecture where domain complexity demanded them. The goal is always the same: protect your domain from the churn of frameworks and infrastructure, and keep business logic testable without spinning up a database or a Kafka broker.</p>

<p>You don’t need to pick sides. Pick what solves your actual problem.</p>

<p class="text-center">
<a href="https://alistair.cockburn.us/hexagonal-architecture/" class="tl-ref-link" target="_blank" rel="noopener">Hexagonal Architecture</a>

<a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" class="tl-ref-link" target="_blank" rel="noopener">Clean Architecture</a>

</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Architecture" /><category term="Hexagonal Architecture" /><category term="Clean Architecture" /><category term="Spring Boot" /><summary type="html"><![CDATA[Hexagonal vs Clean Architecture in Java Spring Boot — a practical comparison from a senior developer who has applied both in production across event-driven, microservices, and government-scale systems.]]></summary></entry><entry><title type="html">Architecture | Object-Oriented vs Functional Programming (And Why the Argument Is Wrong)</title><link href="https://www.trinitylogic.co.uk/blog/oo-vs-fp" rel="alternate" type="text/html" title="Architecture | Object-Oriented vs Functional Programming (And Why the Argument Is Wrong)" /><published>2025-12-20T00:00:00+00:00</published><updated>2025-12-20T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/oo-vs-fp</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/oo-vs-fp"><![CDATA[<p>Every few years, the OO vs Functional Programming debate resurfaces, usually framed as a zero-sum argument. One side claims objects are just mutable state wrapped in ceremony. The other dismisses functional programming as academic, unreadable, and detached from real-world systems. I’ve worked with both styles for over two decades — across finance, energy, retail, betting, and government systems — and the longer I do this job, the clearer it becomes: this argument is based on a misunderstanding of what OO and FP actually are.</p>

<p>Let’s clear that up.</p>

<h2 id="object-orientation-is-not-about-state">Object Orientation Is Not About State</h2>

<p>One of the most persistent myths is that object-oriented programming is fundamentally about managing mutable state. It
isn’t.</p>

<p>Objects are not data structures. Objects may use data structures internally, but those details are deliberately hidden.
This is why fields are private. From the outside, you cannot see state at all—you can only see behaviour.</p>

<p>An object is best understood as a bag of functions, not a bag of data.</p>

<p>When objects are treated primarily as data holders—especially when exposed directly via getters and setters—that’s not
good OO, it’s a design smell. This confusion is why tools like ORMs muddy the waters. They don’t map relational data to
objects; they map relational data to data structures that merely look like objects.</p>

<p>Good OO keeps data hidden and exposes intention through behaviour.</p>

<h2 id="functional-programming-is-also-about-functions-operating-on-data">Functional Programming Is Also About Functions Operating on Data</h2>

<p>Functional Programming doesn’t escape this reality either. Every functional program ever written is composed of
functions that operate on data. So is every OO program.</p>

<p>It’s common to hear OO described as data and functions bound together, but that’s true of all programs. The real
difference is not that functions and data are bound—it’s how much discipline is imposed on changing them.</p>

<p>At that level, OO and FP are not opposites at all.</p>

<h2 id="the-real-differences">The Real Differences</h2>

<p>The meaningful differences between OO and FP come down to what each style restricts.</p>

<h3 id="functional-programming-imposes-discipline-on-assignment">Functional Programming Imposes Discipline on Assignment</h3>

<p>A true functional language does not allow assignment. You cannot change the value of a variable once it is defined.
Mutation is either impossible or heavily constrained behind explicit ceremony.</p>

<p>This has profound consequences:</p>

<ul>
  <li>State changes are rare and obvious</li>
  <li>Concurrency becomes dramatically safer</li>
  <li>Reasoning about behaviour becomes easier</li>
</ul>

<p>Most functional languages do allow mutation eventually—but only after you’ve jumped through deliberate hoops. And
because of that friction, most code simply doesn’t mutate state at all.</p>

<p>That’s not an accident. That’s the point.</p>

<h3 id="object-orientation-imposes-discipline-on-function-pointers">Object Orientation Imposes Discipline on Function Pointers</h3>

<p>OO’s real contribution is polymorphism.</p>

<p>Polymorphism is implemented with function pointers. In low-level languages, managing those pointers manually is painful
and error-prone. Object-oriented languages do this work for you. They initialise them, marshal them, and invoke them
safely.</p>

<p>In languages like Java, every method call is polymorphic. Every call is an indirect call through a function reference.</p>

<p>This is what OO really gives you: safe, ubiquitous polymorphism without manual pointer management.</p>

<h2 id="these-disciplines-are-not-mutually-exclusive">These Disciplines Are Not Mutually Exclusive</h2>

<p>FP restricts assignment. OO restricts how functions are referenced and invoked. These concerns are orthogonal.</p>

<p>There is nothing preventing a system from imposing both disciplines. You can write object-oriented functional code, and
when done well, it’s extremely powerful.</p>

<h2 id="why-polymorphism-actually-matters">Why Polymorphism Actually Matters</h2>

<p>Polymorphism inverts source-code dependencies.</p>

<p>At runtime, a caller still depends on the callee. But at compile time, both depend on an abstraction. This inversion
allows implementations to be swapped without touching core business logic.</p>

<p>This is the foundation of plugin architectures, hexagonal architecture, and dependency inversion.</p>

<h2 id="why-immutability-actually-matters">Why Immutability Actually Matters</h2>

<p>The benefit of restricting assignment is simple: you cannot corrupt shared state if you never mutate it.</p>

<p>Functional-style immutability dramatically reduces race conditions, concurrency bugs, and temporal coupling — critical
properties in modern multi-core systems.</p>

<h2 id="design-principles-still-apply">Design Principles Still Apply</h2>

<p>Choosing FP does not make design principles optional. Choosing OO does not make them automatic.</p>

<p>Good design is independent of paradigm.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>OO is good when you understand what it actually is. FP is good when you understand what it actually is. Combining them
thoughtfully is often better than treating either as dogma.</p>

<p>If you’re still arguing OO vs FP, you’re arguing the wrong thing.</p>

<p class="text-center">
<a href="https://en.wikipedia.org/wiki/Object-oriented_analysis_and_design" class="tl-ref-link" target="_blank" rel="noopener">Object-oriented analysis and design</a>

<a href="https://en.wikipedia.org/wiki/Functional_programming" class="tl-ref-link" target="_blank" rel="noopener">Functional programming</a>

</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Architecture" /><category term="Object-Oriented Programming" /><category term="Functional Programming" /><category term="Clean Code" /><category term="Software Design" /><summary type="html"><![CDATA[Object-Oriented vs Functional Programming in Java — a senior developer's take after 20 years building production systems. Polymorphism, immutability, and why combining both produces the best Java code.]]></summary></entry><entry><title type="html">Architecture | Hexagonal Architecture for Real-World Systems</title><link href="https://www.trinitylogic.co.uk/blog/hexagonal-architecture" rel="alternate" type="text/html" title="Architecture | Hexagonal Architecture for Real-World Systems" /><published>2025-12-15T00:00:00+00:00</published><updated>2025-12-15T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/hexagonal-architecture</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/hexagonal-architecture"><![CDATA[<p>Working on long-lived, high-stakes systems — from real-time financial data pipelines at Mosaic Smart Data to government benefit services at DWP Digital — I’ve learned that most architectural pain doesn’t come from business complexity, but from tight coupling. Early in my career, I built systems where domain logic leaked everywhere: into controllers, repositories, message handlers, and frameworks. They worked — until requirements changed.</p>

<p>That experience is why Hexagonal Architecture (also known as Ports and Adapters) has become my default approach for building systems that stay maintainable and continue to evolve without fear.</p>

<h2 id="what-is-hexagonal-architecture">What Is Hexagonal Architecture?</h2>

<p>Hexagonal Architecture is a design approach that places your domain logic at the centre of the system and treats everything else—databases, messaging systems, HTTP APIs, frameworks—as replaceable details.</p>

<p>At a high level:</p>

<ul>
  <li>The core domain contains business rules and use cases</li>
  <li>Ports define what the domain needs or exposes</li>
  <li>Adapters implement those ports using specific technologies (REST, Kafka, MongoDB, etc.)</li>
</ul>

<p>The key idea is simple but powerful:</p>

<blockquote>
  <p>Your business logic should not depend on infrastructure.<br />
Infrastructure should depend on your business logic.</p>
</blockquote>

<p>This inversion is what keeps systems flexible under real-world pressure.</p>

<h2 id="why-i-use-hexagonal-architecture-in-practice">Why I Use Hexagonal Architecture in Practice</h2>

<p>On modern platforms—especially event-driven ones—you rarely have a single interface. A service might be triggered by HTTP requests, Kafka events, scheduled jobs, or batch reprocessing. Without a clear boundary, logic gets duplicated or embedded in the wrong places. Hexagonal Architecture gives you one place where business decisions live, regardless of how the system is driven.</p>

<h2 id="ports-and-adapters-in-action">Ports and Adapters in Action</h2>

<p>Ports are interfaces owned by the domain:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ClaimRepository</span> <span class="o">{</span>
    <span class="nc">Optional</span><span class="o">&lt;</span><span class="nc">Claim</span><span class="o">&gt;</span> <span class="nf">findById</span><span class="o">(</span><span class="nc">ClaimId</span> <span class="n">id</span><span class="o">);</span>
    <span class="kt">void</span> <span class="nf">save</span><span class="o">(</span><span class="nc">Claim</span> <span class="n">claim</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Adapters translate infrastructure concerns into domain interactions, keeping frameworks out of the core.</p>

<h2 id="testing-and-maintainability">Testing and Maintainability</h2>

<p>Because the domain depends only on ports, business logic can be tested without Spring, Kafka, or databases. This leads to faster tests, clearer intent, and far greater confidence when making changes.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>Hexagonal Architecture isn’t about purity — it’s about protecting what matters most: your business logic. If you’re building systems that must evolve safely over time, it’s one of the most effective architectural patterns you can adopt.</p>

<p class="text-center">
<a href="https://alistair.cockburn.us/hexagonal-architecture/" class="tl-ref-link" target="_blank" rel="noopener">Hexagonal Architecture</a>

<a href="https://spring.io/projects/spring-boot" class="tl-ref-link" target="_blank" rel="noopener">Spring Boot</a>

</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Architecture" /><category term="Hexagonal Architecture" /><category term="Clean Code" /><category term="Spring Boot" /><category term="Microservices" /><summary type="html"><![CDATA[How I use Hexagonal Architecture (Ports and Adapters) in Java Spring Boot systems — from Betfair trading frameworks to DWP Digital's benefit platform. Practical patterns for keeping domain logic clean and independently testable.]]></summary></entry><entry><title type="html">Spring Boot | Adding Robustness with Spring Retry Annotations</title><link href="https://www.trinitylogic.co.uk/blog/robustness-with-spring-retry-annotations" rel="alternate" type="text/html" title="Spring Boot | Adding Robustness with Spring Retry Annotations" /><published>2025-10-07T00:00:00+00:00</published><updated>2025-10-07T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/robustness-with-spring-retry-annotations</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/robustness-with-spring-retry-annotations"><![CDATA[<p>When you’re integrating with external systems — APIs, databases, or third-party services — transient failures are
inevitable. A network hiccup, a brief rate-limit window, or a timeout is often enough to cause a failure that would
have succeeded a moment later. Rather than littering your code with manual retry loops, Spring Boot provides a clean,
declarative solution: the <a href="https://docs.spring.io/spring-retry/docs/current/api/org/springframework/retry/annotation/Retryable.html"><code class="language-plaintext highlighter-rouge">@Retryable</code></a> annotation.</p>

<p>I’ve used Spring Retry in real-time data pipelines at Mosaic Smart Data, where transient failures from external financial institution APIs were a fact of life. In this post I’ll walk through how I applied it, including exponential backoff and selective exception handling.</p>

<h2 id="contents">Contents</h2>

<ul>
  <li><a href="#why-retry">Why Retry?</a></li>
  <li><a href="#enabling-spring-retry">Enabling Spring Retry</a></li>
  <li><a href="#using-retryable-on-a-service-method">Using @Retryable on a Service Method</a></li>
  <li><a href="#configuring-backoff-and-exception-handling">Configuring Backoff and Exception Handling</a></li>
  <li><a href="#practical-example-in-a-base-provider-service">Practical Example in a Base Provider Service</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ul>

<hr />

<h2 id="why-retry">Why Retry?</h2>

<p>When a method calls an external API, that call can fail due to reasons beyond our control — temporary outages,
throttling, or network latency spikes. In such scenarios, retrying the operation after a delay is often enough to
recover gracefully.</p>

<p>Without a retry mechanism, these transient errors can lead to unnecessary failures, noisy logs, and degraded user
experience. Implementing retry logic manually can also clutter your code and lead to subtle bugs.</p>

<p>Spring Retry provides a <strong>declarative</strong>, <strong>configurable</strong>, and <strong>centralized</strong> way to handle this.</p>

<hr />

<h2 id="enabling-spring-retry">Enabling Spring Retry</h2>

<p>To use Spring Retry, you need to add the dependency and enable it:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- pom.xml --&gt;</span>
<span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.springframework.retry<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>spring-retry<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>

<span class="nt">&lt;dependency&gt;</span>
<span class="nt">&lt;groupId&gt;</span>org.springframework.boot<span class="nt">&lt;/groupId&gt;</span>
<span class="nt">&lt;artifactId&gt;</span>spring-boot-starter-aop<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>Then, enable retry support in your Spring Boot application:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@SpringBootApplication</span>
<span class="nd">@EnableRetry</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Application</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">SpringApplication</span><span class="o">.</span><span class="na">run</span><span class="o">(</span><span class="nc">Application</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">args</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="using-retryable-on-a-service-method">Using @Retryable on a Service Method</h2>

<p>The <code class="language-plaintext highlighter-rouge">@Retryable</code> annotation can be placed on any Spring-managed bean method to make it automatically retry on failure.</p>

<p>Here’s the basic structure:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Retryable</span><span class="o">(</span>
        <span class="n">maxAttempts</span> <span class="o">=</span> <span class="mi">5</span><span class="o">,</span>
        <span class="n">backoff</span> <span class="o">=</span> <span class="nd">@Backoff</span><span class="o">(</span><span class="n">delay</span> <span class="o">=</span> <span class="mi">1000</span><span class="o">,</span> <span class="n">multiplier</span> <span class="o">=</span> <span class="mf">2.0</span><span class="o">)</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="nc">ApiResponse</span> <span class="nf">callExternalApi</span><span class="o">(</span><span class="nc">String</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// potentially flaky operation</span>
<span class="o">}</span>

</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">maxAttempts</code> specifies how many times to try before giving up.</li>
  <li><code class="language-plaintext highlighter-rouge">@Backoff</code> controls the delay between attempts.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">delay</code> = 1000 means start with a 1-second delay.</li>
      <li><code class="language-plaintext highlighter-rouge">multiplier</code> = 2.0 means each subsequent retry doubles the delay (exponential backoff).</li>
    </ul>
  </li>
</ul>

<p>If the method keeps failing, the last thrown exception is propagated.</p>

<h2 id="configuring-backoff-and-exception-handling">Configuring Backoff and Exception Handling</h2>

<p>One of the strengths of Spring Retry is that its parameters can be <strong>externalized to configuration</strong>, allowing different
environments (e.g. dev vs prod) to tune retry behavior without code changes.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@Retryable</span><span class="o">(</span>
        <span class="n">maxAttemptsExpression</span> <span class="o">=</span> <span class="s">"${retry.max.attempts:5}"</span><span class="o">,</span>
        <span class="n">noRetryFor</span> <span class="o">=</span> <span class="o">{</span><span class="nc">ApiException</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">DailyLimitExceededException</span><span class="o">.</span><span class="na">class</span><span class="o">},</span>
        <span class="n">backoff</span> <span class="o">=</span> <span class="nd">@Backoff</span><span class="o">(</span>
                <span class="n">delayExpression</span> <span class="o">=</span> <span class="s">"${retry.backoff.delay:1000}"</span><span class="o">,</span>
                <span class="n">multiplierExpression</span> <span class="o">=</span> <span class="s">"${retry.backoff.multiplier:2.0}"</span>
        <span class="o">)</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="nc">ApiResponse</span> <span class="nf">callExternalApi</span><span class="o">(</span><span class="nc">String</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
    <span class="o">...</span>
<span class="o">}</span>

</code></pre></div></div>

<h3 id="key-points">Key points:</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">maxAttemptsExpression</code> lets you read values from application properties, with <code class="language-plaintext highlighter-rouge">5</code> as a fallback default.</li>
  <li><code class="language-plaintext highlighter-rouge">noRetryFor</code> specifies exceptions that should not trigger retries — for example, business logic errors like</li>
  <li><code class="language-plaintext highlighter-rouge">DailyLimitExceededException</code> should fail immediately.</li>
  <li><code class="language-plaintext highlighter-rouge">delayExpression</code> and <code class="language-plaintext highlighter-rouge">multiplierExpression</code> allow runtime configuration of backoff behavior.</li>
</ul>

<p>Sample <code class="language-plaintext highlighter-rouge">application.yml</code></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">retry</span><span class="pi">:</span>
  <span class="na">max</span><span class="pi">:</span>
    <span class="na">attempts</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">backoff</span><span class="pi">:</span>
    <span class="na">delay</span><span class="pi">:</span> <span class="m">1000</span>
    <span class="na">multiplier</span><span class="pi">:</span> <span class="m">2.0</span>

</code></pre></div></div>

<h2 id="practical-example-in-a-base-provider-service">Practical Example in a Base Provider Service</h2>

<p>In my application, I have an abstract base class called <code class="language-plaintext highlighter-rouge">ProviderService</code>, which provides common functionality for
multiple data provider integrations. One of its key methods, <code class="language-plaintext highlighter-rouge">lookup(String lookup)</code>, queries an external API.</p>

<p>Here’s how the retry mechanism was applied:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nd">@LogExecutionTime</span>
<span class="nd">@Retryable</span><span class="o">(</span>
        <span class="n">maxAttemptsExpression</span> <span class="o">=</span> <span class="s">"${retry.max.attempts:5}"</span><span class="o">,</span>
        <span class="n">noRetryFor</span> <span class="o">=</span> <span class="o">{</span><span class="nc">ApiException</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">DailyLimitExceededException</span><span class="o">.</span><span class="na">class</span><span class="o">},</span>
        <span class="n">backoff</span> <span class="o">=</span> <span class="nd">@Backoff</span><span class="o">(</span>
                <span class="n">delayExpression</span> <span class="o">=</span> <span class="s">"${retry.backoff.delay:1000}"</span><span class="o">,</span>
                <span class="n">multiplierExpression</span> <span class="o">=</span> <span class="s">"${retry.backoff.multiplier:2.0}"</span>
        <span class="o">)</span>
<span class="o">)</span>
<span class="kd">public</span> <span class="kd">abstract</span> <span class="no">E</span> <span class="nf">lookup</span><span class="o">(</span><span class="kd">final</span> <span class="nc">String</span> <span class="n">lookup</span><span class="o">)</span>
        <span class="kd">throws</span> <span class="nc">ApiException</span><span class="o">,</span> <span class="nc">DailyLimitExceededException</span><span class="o">;</span>

</code></pre></div></div>

<p>This allows <strong>every concrete provider</strong> (e.g. CRM, Zone, Prem) to inherit the retry behaviour automatically, without
repeating logic in each subclass.</p>

<p>If a lookup fails due to a transient error (e.g. timeout), Spring will automatically retry it up to 5 times with
exponential backoff.
If the failure is due to a business rule (e.g. daily limit exceeded), the method will fail fast without retries.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Spring Retry is a powerful tool for adding <strong>resilience</strong> and <strong>fault tolerance</strong> to your application with minimal code.
By using <code class="language-plaintext highlighter-rouge">@Retryable</code>:</p>

<ul>
  <li>Your service methods stay clean and focused on their core responsibility.</li>
  <li>Retry behaviour is externalised to configuration — no redeployment needed to tune it.</li>
  <li>Exceptions that should fail fast (like business rule violations) are excluded cleanly.</li>
  <li>Exponential backoff is a single annotation attribute.</li>
</ul>

<p>This small annotation can have a significant impact on the stability and reliability of your integration-heavy services.</p>

<p class="text-center">
<a href="https://docs.spring.io/spring-retry/docs/current/api/org/springframework/retry/annotation/Retryable.html" class="tl-ref-link" target="_blank" rel="noopener">Spring Retry Documentation</a>

<a href="https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-retry" class="tl-ref-link" target="_blank" rel="noopener">Spring Boot Retry Feature</a>

</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Spring Boot" /><category term="Retryable" /><category term="Backoff" /><category term="Resilience" /><category term="Java" /><category term="Clean Code" /><summary type="html"><![CDATA[How to use Spring Retry @Retryable and @Backoff in Spring Boot to handle transient failures cleanly — configurable retry logic with exponential backoff, selective exception handling, and real-world examples from Java integration services.]]></summary></entry><entry><title type="html">Java | Java Memory Management and GC Tuning for Low-Latency Systems</title><link href="https://www.trinitylogic.co.uk/blog/java-memory-management-gc-tuning-low-latency" rel="alternate" type="text/html" title="Java | Java Memory Management and GC Tuning for Low-Latency Systems" /><published>2025-10-03T00:00:00+00:00</published><updated>2025-10-03T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/java-memory-management-gc-tuning-low-latency</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/java-memory-management-gc-tuning-low-latency"><![CDATA[<p>Most Java applications never need to think about GC tuning. The defaults are reasonable, GC pauses are short, and the overhead is negligible. But for systems with strict latency requirements — a Betfair trading framework where a 500ms GC pause causes you to miss a pre-race steam, or a financial data pipeline where consistent sub-10ms processing is part of the SLA — GC behaviour matters and understanding it is the difference between a system that works and one that occasionally doesn’t.</p>

<h2 id="the-gc-pause-problem">The GC Pause Problem</h2>

<p>Every garbage collector must periodically pause application threads to collect unreachable objects. For most applications, a 50ms pause every 30 seconds is irrelevant. For a system that evaluates trading signals from a live streaming feed, a 50ms pause is a missed market event. For an in-play trading system, it’s a potentially unhedged position.</p>

<p>Understanding the trade-off: <strong>throughput vs pause time</strong>. G1GC optimises for throughput with bounded pauses. ZGC optimises for minimal pause time. Serial/Parallel GC maximise throughput with no pause-time guarantees. For low-latency systems, ZGC is almost always the right choice on Java 21.</p>

<h2 id="g1gc--the-default">G1GC — The Default</h2>

<p>G1GC is the default collector since Java 9. It divides the heap into regions and prioritises collection of regions with the most garbage (“garbage-first”). Key configuration:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-XX</span>:+UseG1GC                         <span class="c"># explicit (default on Java 9+)</span>
<span class="nt">-XX</span>:MaxGCPauseMillis<span class="o">=</span>100             <span class="c"># target max pause (not guaranteed)</span>
<span class="nt">-XX</span>:G1NewSizePercent<span class="o">=</span>20              <span class="c"># young generation minimum</span>
<span class="nt">-XX</span>:G1MaxNewSizePercent<span class="o">=</span>40           <span class="c"># young generation maximum</span>
<span class="nt">-XX</span>:G1HeapRegionSize<span class="o">=</span>8m              <span class="c"># region size (power of 2, 1m-32m)</span>
<span class="nt">-XX</span>:InitiatingHeapOccupancyPercent<span class="o">=</span>35 <span class="c"># start concurrent marking at 35% heap</span>
</code></pre></div></div>

<p>G1GC’s <code class="language-plaintext highlighter-rouge">MaxGCPauseMillis</code> is a target, not a hard limit. Under allocation pressure it may be breached. For my Betfair streaming data processing, G1GC worked well for pre-race analysis (latency requirements ~100ms) but was too unpredictable for in-play.</p>

<h2 id="zgc--pause-times-under-1ms">ZGC — Pause Times Under 1ms</h2>

<p>ZGC (available since Java 15, production-ready Java 21) is a concurrent collector designed for sub-millisecond pause times regardless of heap size. Most GC work happens concurrently with application threads:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-XX</span>:+UseZGC
<span class="nt">-XX</span>:SoftMaxHeapSize<span class="o">=</span>4g               <span class="c"># soft upper bound — ZGC may exceed in GC pressure</span>
<span class="nt">-Xmx6g</span>                               <span class="c"># hard upper bound</span>
<span class="nt">-XX</span>:ZCollectionInterval<span class="o">=</span>0            <span class="c"># let ZGC choose collection frequency</span>
<span class="nt">-XX</span>:ZUncommitDelay<span class="o">=</span>300               <span class="c"># return memory to OS after 300s of inactivity</span>
</code></pre></div></div>

<p>ZGC’s stop-the-world pauses are limited to root scanning — typically under 1ms, often under 500μs on modern hardware. For a Betfair in-play trading system, this is transformative. The trading loop continues running while GC collects concurrently.</p>

<p>The trade-off: ZGC uses more CPU for concurrent work. If your system is CPU-bound, ZGC’s background threads compete with application work. For IO-bound workloads (most trading systems), this is rarely a problem.</p>

<h2 id="monitoring-gc-behaviour">Monitoring GC Behaviour</h2>

<p>Don’t tune without measuring first. Enable GC logging:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-Xlog</span>:gc<span class="k">*</span>:file<span class="o">=</span>/var/log/app/gc.log:time,uptime,level,tags:filecount<span class="o">=</span>5,filesize<span class="o">=</span>20m
</code></pre></div></div>

<p>This writes structured GC logs you can analyse with tools like GCViewer or GCEasy. Look for:</p>
<ul>
  <li>Pause times (<code class="language-plaintext highlighter-rouge">GC(0) Pause Young (Normal) 150ms</code> — too high for low-latency)</li>
  <li>Frequency of full GC events (<code class="language-plaintext highlighter-rouge">GC(5) Pause Full (Ergonomics)</code> — should be rare)</li>
  <li>Heap occupancy trends (growing heap means memory leak or insufficient size)</li>
</ul>

<p>JDK Flight Recorder is even more powerful:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-XX</span>:+FlightRecorder
<span class="nt">-XX</span>:StartFlightRecording<span class="o">=</span><span class="nv">duration</span><span class="o">=</span>60s,filename<span class="o">=</span>/tmp/recording.jfr,settings<span class="o">=</span>profile
</code></pre></div></div>

<p>In JDK Mission Control, the GC events view shows pause distribution, allocation rates, and top allocation sites. The allocation profiler identifies which code paths allocate most — the first step in reducing GC pressure.</p>

<h2 id="reducing-allocation-pressure">Reducing Allocation Pressure</h2>

<p>The best GC optimisation is allocating less. Common patterns in high-throughput Java systems:</p>

<p><strong>Object pooling for frequently created/discarded objects:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MarketUpdatePool</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Queue</span><span class="o">&lt;</span><span class="nc">MarketUpdate</span><span class="o">&gt;</span> <span class="n">pool</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentLinkedQueue</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="nc">MarketUpdate</span> <span class="nf">acquire</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">MarketUpdate</span> <span class="n">update</span> <span class="o">=</span> <span class="n">pool</span><span class="o">.</span><span class="na">poll</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">update</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="n">update</span><span class="o">.</span><span class="na">reset</span><span class="o">()</span> <span class="o">:</span> <span class="k">new</span> <span class="nc">MarketUpdate</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">release</span><span class="o">(</span><span class="nc">MarketUpdate</span> <span class="n">update</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">pool</span><span class="o">.</span><span class="na">offer</span><span class="o">(</span><span class="n">update</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>Avoid boxing — use primitive collections:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Bad — boxes every long to Long, creates pressure</span>
<span class="nc">Map</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">,</span> <span class="nc">Double</span><span class="o">&gt;</span> <span class="n">prices</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>

<span class="c1">// Better — primitive long-to-double map (Eclipse Collections or Trove)</span>
<span class="nc">LongDoubleHashMap</span> <span class="n">prices</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LongDoubleHashMap</span><span class="o">();</span>
</code></pre></div></div>

<p><strong>Reuse buffers in the streaming data path:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Reuse ByteBuffer across JSON parses rather than allocating per message</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">parseBuffer</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="o">[</span><span class="mi">64</span> <span class="o">*</span> <span class="mi">1024</span><span class="o">];</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">JsonParser</span> <span class="n">parser</span> <span class="o">=</span> <span class="n">jsonFactory</span><span class="o">.</span><span class="na">createParser</span><span class="o">(</span><span class="n">parseBuffer</span><span class="o">);</span>
</code></pre></div></div>

<h2 id="jvm-flags-for-betfair-trading-systems">JVM Flags for Betfair Trading Systems</h2>

<p>The flags I use in production for a Betfair trading framework on a 16-core server with 32GB RAM:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Java 21, ZGC, 8GB heap for the trading service</span>
java <span class="se">\</span>
  <span class="nt">-XX</span>:+UseZGC <span class="se">\</span>
  <span class="nt">-Xms4g</span> <span class="nt">-Xmx8g</span> <span class="se">\</span>
  <span class="nt">-XX</span>:SoftMaxHeapSize<span class="o">=</span>6g <span class="se">\</span>
  <span class="nt">-XX</span>:+ZGenerational <span class="se">\ </span>        <span class="c"># generational ZGC (Java 21+) — better throughput</span>
  <span class="nt">-XX</span>:+AlwaysPreTouch <span class="se">\ </span>       <span class="c"># pre-touch pages at startup — avoids OS page faults at runtime</span>
  <span class="nt">-XX</span>:+DisableExplicitGC <span class="se">\ </span>    <span class="c"># ignore System.gc() calls from libraries</span>
  <span class="nt">-XX</span>:+HeapDumpOnOutOfMemoryError <span class="se">\</span>
  <span class="nt">-XX</span>:HeapDumpPath<span class="o">=</span>/var/dumps/ <span class="se">\</span>
  <span class="nt">-Xlog</span>:gc<span class="k">*</span>:file<span class="o">=</span>/var/log/gc.log:time,uptime:filecount<span class="o">=</span>5,filesize<span class="o">=</span>10m <span class="se">\</span>
  <span class="nt">-jar</span> trading-framework.jar
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-XX:+ZGenerational</code> is available in Java 21 and makes ZGC significantly more efficient by separating young and old generation collection — recommended for most applications.</p>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Start with ZGC on Java 21 for any latency-sensitive system.</strong> The performance characteristics are predictable and the configuration is simpler than G1GC tuning.</li>
  <li><strong>Monitor allocation rate, not just heap size.</strong> A system allocating 500MB/s with a 4GB heap will GC constantly. A system allocating 50MB/s with the same heap will GC rarely. <code class="language-plaintext highlighter-rouge">jcmd &lt;pid&gt; GC.run</code> + allocation profiling reveals the hottest allocation sites.</li>
  <li><strong>Don’t increase heap size as a first resort.</strong> More heap delays full GC but doesn’t fix the underlying allocation pattern. Fix the root cause; use heap sizing to tune the frequency.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-XX:+AlwaysPreTouch</code> is worth it for long-running services.</strong> Without it, the OS lazily allocates physical pages, causing page fault latency spikes early in the application lifecycle. Pre-touch eliminates this at the cost of longer startup.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="JVM" /><category term="GC" /><category term="Performance" /><category term="Low Latency" /><summary type="html"><![CDATA[JVM memory management and GC tuning for Java low-latency systems — G1GC vs ZGC, key JVM flags, allocation pressure reduction, and GC monitoring with JFR for Betfair trading and financial data pipelines.]]></summary></entry><entry><title type="html">Smarkets | Smarkets API Integration for Java Developers</title><link href="https://www.trinitylogic.co.uk/blog/smarkets-api-java-integration" rel="alternate" type="text/html" title="Smarkets | Smarkets API Integration for Java Developers" /><published>2025-09-26T00:00:00+00:00</published><updated>2025-09-26T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/smarkets-api-java-integration</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/smarkets-api-java-integration"><![CDATA[<p>Smarkets is the third major UK betting exchange, and it has a few characteristics that make it worth adding to a multi-exchange trading system. The commission structure is among the lowest in the market — 2% flat. The REST and streaming APIs are well-documented and genuinely modern. And for certain political events and US sports markets, Smarkets often has comparable or superior liquidity to Betfair. If you’ve built Betfair integration in Java, adding Smarkets is a few days’ work — and the incremental edge it provides can be meaningful.</p>

<h2 id="smarkets-api-overview">Smarkets API Overview</h2>

<p>Smarkets provides two API surfaces:</p>
<ul>
  <li><strong>REST API</strong> (<code class="language-plaintext highlighter-rouge">api.smarkets.com/v3/</code>) — market browsing, order management, account operations</li>
  <li><strong>Streaming API</strong> — WebSocket-based market data stream for live prices</li>
</ul>

<p>Authentication uses OAuth2 tokens. API access is available via the developer portal; you’ll need to apply for a trading account with API access.</p>

<h2 id="authentication">Authentication</h2>

<p>Smarkets uses a session token obtained via login:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SmarketsAuthService</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">AUTH_URL</span> <span class="o">=</span> <span class="s">"https://api.smarkets.com/v3/sessions/"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RestClient</span> <span class="n">restClient</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">login</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="nc">String</span> <span class="n">password</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">LoginRequest</span> <span class="n">loginRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LoginRequest</span><span class="o">(</span><span class="n">username</span><span class="o">,</span> <span class="n">password</span><span class="o">);</span>

        <span class="nc">LoginResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">post</span><span class="o">()</span>
            <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="no">AUTH_URL</span><span class="o">)</span>
            <span class="o">.</span><span class="na">contentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
            <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="n">loginRequest</span><span class="o">)</span>
            <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
            <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">LoginResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="na">token</span><span class="o">();</span> <span class="c1">// bearer token for subsequent requests</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Include the token in subsequent requests:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">HttpHeaders</span> <span class="n">headers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HttpHeaders</span><span class="o">();</span>
<span class="n">headers</span><span class="o">.</span><span class="na">setBearerAuth</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
<span class="n">headers</span><span class="o">.</span><span class="na">setContentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">);</span>
</code></pre></div></div>

<p>Sessions have limited TTLs; refresh tokens before they expire, or handle 401 responses with automatic re-authentication.</p>

<h2 id="browsing-events-and-markets">Browsing Events and Markets</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">SmarketsEvent</span><span class="o">&gt;</span> <span class="nf">getHorseRacingEvents</span><span class="o">()</span> <span class="o">{</span>
    <span class="c1">// List event types</span>
    <span class="nc">String</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
        <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"/v3/events/?type=race&amp;state=upcoming&amp;sort=start_datetime"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">,</span> <span class="s">"Bearer "</span> <span class="o">+</span> <span class="n">token</span><span class="o">)</span>
        <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
        <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="c1">// Parse events from JSON response</span>
    <span class="k">return</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">response</span><span class="o">,</span> <span class="nc">EventsResponse</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">getEvents</span><span class="o">();</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">SmarketsMarket</span><span class="o">&gt;</span> <span class="nf">getMarketsForEvent</span><span class="o">(</span><span class="nc">String</span> <span class="n">eventId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">MarketsResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
        <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"/v3/events/"</span> <span class="o">+</span> <span class="n">eventId</span> <span class="o">+</span> <span class="s">"/markets/"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">,</span> <span class="s">"Bearer "</span> <span class="o">+</span> <span class="n">token</span><span class="o">)</span>
        <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
        <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">MarketsResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="na">getMarkets</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The Smarkets API uses string IDs throughout. A market has a <code class="language-plaintext highlighter-rouge">contractId</code> per runner — the equivalent of Betfair’s <code class="language-plaintext highlighter-rouge">selectionId</code>.</p>

<h2 id="retrieving-prices">Retrieving Prices</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">Quote</span><span class="o">&gt;</span> <span class="nf">getPrices</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">QuotesResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">restClient</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
        <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"/v3/markets/"</span> <span class="o">+</span> <span class="n">marketId</span> <span class="o">+</span> <span class="s">"/quotes/"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">,</span> <span class="s">"Bearer "</span> <span class="o">+</span> <span class="n">token</span><span class="o">)</span>
        <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
        <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">QuotesResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="na">getQuotes</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Each <code class="language-plaintext highlighter-rouge">Quote</code> contains <code class="language-plaintext highlighter-rouge">bid</code> prices (equivalent to Betfair’s available-to-lay from the customer perspective) and <code class="language-plaintext highlighter-rouge">offer</code> prices (equivalent to available-to-back). The terminology maps to traditional financial markets — Smarkets was founded with a financial markets background and it shows.</p>

<h2 id="placing-orders">Placing Orders</h2>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">OrderResponse</span> <span class="nf">placeBackOrder</span><span class="o">(</span>
        <span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">contractId</span><span class="o">,</span>
        <span class="nc">String</span> <span class="n">price</span><span class="o">,</span> <span class="nc">String</span> <span class="n">quantity</span><span class="o">)</span> <span class="o">{</span>

    <span class="c1">// Smarkets uses fractional odds represented as rationals (e.g. "4/1")</span>
    <span class="c1">// or decimal expressed as numerator/denominator pairs</span>
    <span class="nc">PlaceOrderRequest</span> <span class="n">request</span> <span class="o">=</span> <span class="nc">PlaceOrderRequest</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
        <span class="o">.</span><span class="na">marketId</span><span class="o">(</span><span class="n">marketId</span><span class="o">)</span>
        <span class="o">.</span><span class="na">contractId</span><span class="o">(</span><span class="n">contractId</span><span class="o">)</span>
        <span class="o">.</span><span class="na">side</span><span class="o">(</span><span class="s">"buy"</span><span class="o">)</span>             <span class="c1">// "buy" = back, "sell" = lay (financial convention)</span>
        <span class="o">.</span><span class="na">price</span><span class="o">(</span><span class="n">price</span><span class="o">)</span>            <span class="c1">// e.g. "4.5" for decimal odds</span>
        <span class="o">.</span><span class="na">quantity</span><span class="o">(</span><span class="n">quantity</span><span class="o">)</span>      <span class="c1">// stake in pence as a string, e.g. "1000" = £10</span>
        <span class="o">.</span><span class="na">type</span><span class="o">(</span><span class="s">"limit"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">build</span><span class="o">();</span>

    <span class="k">return</span> <span class="n">restClient</span><span class="o">.</span><span class="na">post</span><span class="o">()</span>
        <span class="o">.</span><span class="na">uri</span><span class="o">(</span><span class="s">"/v3/orders/"</span><span class="o">)</span>
        <span class="o">.</span><span class="na">header</span><span class="o">(</span><span class="s">"Authorization"</span><span class="o">,</span> <span class="s">"Bearer "</span> <span class="o">+</span> <span class="n">token</span><span class="o">)</span>
        <span class="o">.</span><span class="na">contentType</span><span class="o">(</span><span class="nc">MediaType</span><span class="o">.</span><span class="na">APPLICATION_JSON</span><span class="o">)</span>
        <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="n">request</span><span class="o">)</span>
        <span class="o">.</span><span class="na">retrieve</span><span class="o">()</span>
        <span class="o">.</span><span class="na">body</span><span class="o">(</span><span class="nc">OrderResponse</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">buy</code>/<code class="language-plaintext highlighter-rouge">sell</code> terminology — consistent with financial markets, opposite to Betfair’s <code class="language-plaintext highlighter-rouge">BACK</code>/<code class="language-plaintext highlighter-rouge">LAY</code> convention. <code class="language-plaintext highlighter-rouge">buy</code> means you want the selection to win; <code class="language-plaintext highlighter-rouge">sell</code> means you want it to lose.</p>

<h2 id="websocket-streaming">WebSocket Streaming</h2>

<p>Smarkets provides real-time price updates via WebSocket:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SmarketsStreamClient</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">STREAM_URL</span> <span class="o">=</span> <span class="s">"wss://stream.smarkets.com/"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">WebSocketSession</span> <span class="n">session</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">connect</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">marketIds</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">WebSocketClient</span> <span class="n">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StandardWebSocketClient</span><span class="o">();</span>

        <span class="n">session</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="k">new</span> <span class="nc">TextWebSocketHandler</span><span class="o">()</span> <span class="o">{</span>
            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleTextMessage</span><span class="o">(</span><span class="nc">WebSocketSession</span> <span class="n">session</span><span class="o">,</span> <span class="nc">TextMessage</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">processMessage</span><span class="o">(</span><span class="n">message</span><span class="o">.</span><span class="na">getPayload</span><span class="o">());</span>
            <span class="o">}</span>

            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">afterConnectionClosed</span><span class="o">(</span><span class="nc">WebSocketSession</span> <span class="n">session</span><span class="o">,</span> <span class="nc">CloseStatus</span> <span class="n">status</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Smarkets stream closed: {}"</span><span class="o">,</span> <span class="n">status</span><span class="o">);</span>
                <span class="n">scheduleReconnect</span><span class="o">();</span>
            <span class="o">}</span>
        <span class="o">},</span> <span class="no">STREAM_URL</span><span class="o">).</span><span class="na">get</span><span class="o">();</span>

        <span class="c1">// Subscribe to markets</span>
        <span class="nc">SubscribeRequest</span> <span class="n">sub</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SubscribeRequest</span><span class="o">(</span><span class="n">marketIds</span><span class="o">);</span>
        <span class="n">session</span><span class="o">.</span><span class="na">sendMessage</span><span class="o">(</span><span class="k">new</span> <span class="nc">TextMessage</span><span class="o">(</span><span class="n">objectMapper</span><span class="o">.</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">sub</span><span class="o">)));</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">processMessage</span><span class="o">(</span><span class="nc">String</span> <span class="n">payload</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Parse market update and apply to local state cache</span>
        <span class="nc">SmarketsMarketUpdate</span> <span class="n">update</span> <span class="o">=</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">payload</span><span class="o">,</span> <span class="nc">SmarketsMarketUpdate</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="n">stateCache</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">update</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="multi-exchange-abstraction">Multi-Exchange Abstraction</h2>

<p>A common adapter interface normalises Smarkets and Betfair behind a shared model:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ExchangeAdapter</span> <span class="o">{</span>

    <span class="nc">String</span> <span class="nf">exchangeId</span><span class="o">();</span>

    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ExchangeMarket</span><span class="o">&gt;</span> <span class="nf">findMarkets</span><span class="o">(</span><span class="nc">ExchangeMarketFilter</span> <span class="n">filter</span><span class="o">);</span>

    <span class="nc">ExchangePriceView</span> <span class="nf">getPrices</span><span class="o">(</span><span class="nc">String</span> <span class="n">exchangeMarketId</span><span class="o">);</span>

    <span class="nc">ExchangeOrderResult</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">ExchangeOrder</span> <span class="n">order</span><span class="o">);</span>

    <span class="kt">void</span> <span class="nf">cancelOrder</span><span class="o">(</span><span class="nc">String</span> <span class="n">exchangeOrderId</span><span class="o">);</span>
<span class="o">}</span>

<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SmarketsAdapter</span> <span class="kd">implements</span> <span class="nc">ExchangeAdapter</span> <span class="o">{</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">exchangeId</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="s">"SMARKETS"</span><span class="o">;</span> <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">ExchangeOrderResult</span> <span class="nf">placeOrder</span><span class="o">(</span><span class="nc">ExchangeOrder</span> <span class="n">order</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Translate ExchangeOrder to Smarkets request</span>
        <span class="c1">// Handle buy/sell convention mapping</span>
        <span class="nc">String</span> <span class="n">side</span> <span class="o">=</span> <span class="n">order</span><span class="o">.</span><span class="na">getSide</span><span class="o">()</span> <span class="o">==</span> <span class="nc">Side</span><span class="o">.</span><span class="na">BACK</span> <span class="o">?</span> <span class="s">"buy"</span> <span class="o">:</span> <span class="s">"sell"</span><span class="o">;</span>
        <span class="nc">OrderResponse</span> <span class="n">response</span> <span class="o">=</span> <span class="n">smarketsClient</span><span class="o">.</span><span class="na">placeOrder</span><span class="o">(</span>
            <span class="n">order</span><span class="o">.</span><span class="na">getMarketId</span><span class="o">(),</span> <span class="n">order</span><span class="o">.</span><span class="na">getSelectionId</span><span class="o">(),</span>
            <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">order</span><span class="o">.</span><span class="na">getPrice</span><span class="o">()),</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">((</span><span class="kt">int</span><span class="o">)(</span><span class="n">order</span><span class="o">.</span><span class="na">getStake</span><span class="o">()</span> <span class="o">*</span> <span class="mi">100</span><span class="o">))</span>
        <span class="o">);</span>
        <span class="k">return</span> <span class="nc">ExchangeOrderResult</span><span class="o">.</span><span class="na">fromSmarkets</span><span class="o">(</span><span class="n">response</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The strategy layer works only against <code class="language-plaintext highlighter-rouge">ExchangeAdapter</code>. Arbitrage opportunities between Betfair and Smarkets become detectable when both adapters report prices for correlated markets.</p>

<h2 id="smarkets-vs-betfair--practical-notes">Smarkets vs Betfair — Practical Notes</h2>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Smarkets</th>
      <th>Betfair</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Commission</td>
      <td>2% flat</td>
      <td>2–5% tiered</td>
    </tr>
    <tr>
      <td>API style</td>
      <td>REST + WebSocket</td>
      <td>REST + TCP streaming</td>
    </tr>
    <tr>
      <td>Streaming</td>
      <td>WebSocket (standard)</td>
      <td>Custom TCP/ESA protocol</td>
    </tr>
    <tr>
      <td>Terminology</td>
      <td>Financial (buy/sell)</td>
      <td>Exchange (back/lay)</td>
    </tr>
    <tr>
      <td>Liquidity</td>
      <td>Lower overall, strong in politics/US sports</td>
      <td>Highest in UK horse racing</td>
    </tr>
    <tr>
      <td>Developer docs</td>
      <td>Good</td>
      <td>Excellent</td>
    </tr>
  </tbody>
</table>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Map terminology carefully.</strong> <code class="language-plaintext highlighter-rouge">buy</code> = back, <code class="language-plaintext highlighter-rouge">sell</code> = lay, <code class="language-plaintext highlighter-rouge">bid</code> = available-to-lay, <code class="language-plaintext highlighter-rouge">offer</code> = available-to-back — the financial-market naming is internally consistent but opposite to what Betfair developers expect.</li>
  <li><strong>Prices are decimal in the REST API.</strong> Don’t try to use fractional odds strings unless the API specifically requires them.</li>
  <li><strong>Check liquidity before deploying.</strong> Smarkets liquidity on individual horse racing markets is typically 10–20% of Betfair’s. Verify that your stake sizes are proportionate to available depth.</li>
  <li><strong>Smarkets has better API rate limits for streaming.</strong> The WebSocket stream has no message rate limits equivalent to Betfair’s ESA data charges. For heavily monitored markets, this can reduce costs compared to Betfair streaming.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="Smarkets" /><category term="Betting Exchange" /><category term="API" /><category term="Spring Boot" /><summary type="html"><![CDATA[Integrating with the Smarkets Exchange API in Java — REST and streaming authentication, market browsing, order placement, and building a multi-exchange abstraction alongside Betfair.]]></summary></entry><entry><title type="html">Spring Boot | Production Observability with Actuator, Micrometer, and Prometheus</title><link href="https://www.trinitylogic.co.uk/blog/spring-boot-observability-actuator-micrometer-prometheus" rel="alternate" type="text/html" title="Spring Boot | Production Observability with Actuator, Micrometer, and Prometheus" /><published>2025-09-19T00:00:00+00:00</published><updated>2025-09-19T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/spring-boot-observability-actuator-micrometer-prometheus</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/spring-boot-observability-actuator-micrometer-prometheus"><![CDATA[<p>Observability is the difference between knowing your system is behaving correctly and hoping it is. I’ve been on teams that discovered a critical processing failure from a customer complaint rather than from a dashboard — and I’ve been on teams where we caught and resolved a Kafka consumer lag spike before a single business transaction was affected. The difference was instrumentation. Spring Boot Actuator, Micrometer, and Prometheus give you the building blocks; the craft is knowing what to measure and how to make the dashboards actionable.</p>

<h2 id="spring-boot-actuator--health-endpoints">Spring Boot Actuator — Health Endpoints</h2>

<p>Actuator exposes operational endpoints over HTTP. The most important for production is <code class="language-plaintext highlighter-rouge">/actuator/health</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">management</span><span class="pi">:</span>
  <span class="na">endpoints</span><span class="pi">:</span>
    <span class="na">web</span><span class="pi">:</span>
      <span class="na">exposure</span><span class="pi">:</span>
        <span class="na">include</span><span class="pi">:</span> <span class="s">health, info, metrics, prometheus, loggers</span>
  <span class="na">endpoint</span><span class="pi">:</span>
    <span class="na">health</span><span class="pi">:</span>
      <span class="na">show-details</span><span class="pi">:</span> <span class="s">when-authorized</span>  <span class="c1"># don't expose internals publicly</span>
      <span class="na">show-components</span><span class="pi">:</span> <span class="s">when-authorized</span>
  <span class="na">health</span><span class="pi">:</span>
    <span class="na">livenessState</span><span class="pi">:</span>
      <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">readinessState</span><span class="pi">:</span>
      <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/actuator/health/liveness</code> tells Kubernetes whether the process is alive (should it restart?). <code class="language-plaintext highlighter-rouge">/actuator/health/readiness</code> tells it whether the service is ready to receive traffic.</p>

<p>Custom health indicators let you express the health of dependencies:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span><span class="o">(</span><span class="s">"betfairStream"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">BetfairStreamHealthIndicator</span> <span class="kd">implements</span> <span class="nc">HealthIndicator</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">BetfairStreamClient</span> <span class="n">streamClient</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Health</span> <span class="nf">health</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">streamClient</span><span class="o">.</span><span class="na">isConnected</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">Health</span><span class="o">.</span><span class="na">down</span><span class="o">()</span>
                <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"reason"</span><span class="o">,</span> <span class="s">"Streaming connection down"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"lastConnected"</span><span class="o">,</span> <span class="n">streamClient</span><span class="o">.</span><span class="na">getLastConnectedAt</span><span class="o">())</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nc">Duration</span> <span class="n">lastHeartbeat</span> <span class="o">=</span> <span class="n">streamClient</span><span class="o">.</span><span class="na">timeSinceLastHeartbeat</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">lastHeartbeat</span><span class="o">.</span><span class="na">toSeconds</span><span class="o">()</span> <span class="o">&gt;</span> <span class="mi">30</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="nc">Health</span><span class="o">.</span><span class="na">down</span><span class="o">()</span>
                <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"reason"</span><span class="o">,</span> <span class="s">"No heartbeat for "</span> <span class="o">+</span> <span class="n">lastHeartbeat</span><span class="o">.</span><span class="na">toSeconds</span><span class="o">()</span> <span class="o">+</span> <span class="s">"s"</span><span class="o">)</span>
                <span class="o">.</span><span class="na">build</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="nc">Health</span><span class="o">.</span><span class="na">up</span><span class="o">()</span>
            <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"subscriptions"</span><span class="o">,</span> <span class="n">streamClient</span><span class="o">.</span><span class="na">getSubscriptionCount</span><span class="o">())</span>
            <span class="o">.</span><span class="na">withDetail</span><span class="o">(</span><span class="s">"lastHeartbeat"</span><span class="o">,</span> <span class="n">lastHeartbeat</span><span class="o">.</span><span class="na">toMillis</span><span class="o">()</span> <span class="o">+</span> <span class="s">"ms ago"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">build</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="micrometer--the-metrics-api">Micrometer — The Metrics API</h2>

<p>Micrometer is Spring Boot’s metrics abstraction. You write metrics once against the Micrometer API; the backend (Prometheus, Datadog, CloudWatch, etc.) is a dependency you swap.</p>

<p>The four core types:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MarketMetrics</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Counter</span> <span class="n">betsPlaced</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Counter</span> <span class="n">betsFailed</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Timer</span> <span class="n">betPlacementLatency</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Gauge</span> <span class="n">openPositions</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">DistributionSummary</span> <span class="n">stakeDistribution</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">MarketMetrics</span><span class="o">(</span><span class="nc">MeterRegistry</span> <span class="n">registry</span><span class="o">,</span> <span class="nc">PositionTracker</span> <span class="n">tracker</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">betsPlaced</span> <span class="o">=</span> <span class="nc">Counter</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.bets.placed"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Total bets successfully placed"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"exchange"</span><span class="o">,</span> <span class="s">"betfair"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">betsFailed</span> <span class="o">=</span> <span class="nc">Counter</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.bets.failed"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Total bet placement failures"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"exchange"</span><span class="o">,</span> <span class="s">"betfair"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">betPlacementLatency</span> <span class="o">=</span> <span class="nc">Timer</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.bet.placement.latency"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Time from order decision to API response"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">publishPercentiles</span><span class="o">(</span><span class="mf">0.5</span><span class="o">,</span> <span class="mf">0.95</span><span class="o">,</span> <span class="mf">0.99</span><span class="o">)</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">openPositions</span> <span class="o">=</span> <span class="nc">Gauge</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.positions.open"</span><span class="o">,</span> <span class="n">tracker</span><span class="o">,</span>
                <span class="nl">PositionTracker:</span><span class="o">:</span><span class="n">getOpenPositionCount</span><span class="o">)</span>
            <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Number of currently open positions"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

        <span class="k">this</span><span class="o">.</span><span class="na">stakeDistribution</span> <span class="o">=</span> <span class="nc">DistributionSummary</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.stake.distribution"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">description</span><span class="o">(</span><span class="s">"Distribution of bet stakes in pence"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">baseUnit</span><span class="o">(</span><span class="s">"pence"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">publishPercentiles</span><span class="o">(</span><span class="mf">0.5</span><span class="o">,</span> <span class="mf">0.95</span><span class="o">,</span> <span class="mf">0.99</span><span class="o">)</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Use tags to slice metrics by meaningful dimensions:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Record bet placement with outcome tags</span>
<span class="nc">Timer</span><span class="o">.</span><span class="na">Sample</span> <span class="n">sample</span> <span class="o">=</span> <span class="nc">Timer</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="k">try</span> <span class="o">{</span>
    <span class="nc">PlaceExecutionReport</span> <span class="n">report</span> <span class="o">=</span> <span class="n">betfairClient</span><span class="o">.</span><span class="na">placeOrders</span><span class="o">(</span><span class="n">ssoid</span><span class="o">,</span> <span class="n">request</span><span class="o">);</span>
    <span class="n">betsPlaced</span><span class="o">.</span><span class="na">increment</span><span class="o">();</span>
    <span class="n">stakeDistribution</span><span class="o">.</span><span class="na">record</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">getTotalStakePence</span><span class="o">());</span>
    <span class="n">sample</span><span class="o">.</span><span class="na">stop</span><span class="o">(</span><span class="n">betPlacementLatency</span><span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"result"</span><span class="o">,</span> <span class="s">"success"</span><span class="o">).</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">));</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">betsFailed</span><span class="o">.</span><span class="na">increment</span><span class="o">();</span>
    <span class="n">sample</span><span class="o">.</span><span class="na">stop</span><span class="o">(</span><span class="n">betPlacementLatency</span><span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"result"</span><span class="o">,</span> <span class="s">"failure"</span><span class="o">).</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="exporting-to-prometheus">Exporting to Prometheus</h2>

<p>Add the Prometheus dependency:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>io.micrometer<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>micrometer-registry-prometheus<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<p>Expose the scrape endpoint:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">management</span><span class="pi">:</span>
  <span class="na">endpoints</span><span class="pi">:</span>
    <span class="na">web</span><span class="pi">:</span>
      <span class="na">exposure</span><span class="pi">:</span>
        <span class="na">include</span><span class="pi">:</span> <span class="s">prometheus</span>
  <span class="na">metrics</span><span class="pi">:</span>
    <span class="na">export</span><span class="pi">:</span>
      <span class="na">prometheus</span><span class="pi">:</span>
        <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Configure Prometheus to scrape:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># prometheus.yml</span>
<span class="na">scrape_configs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trading-service'</span>
    <span class="na">metrics_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">/actuator/prometheus'</span>
    <span class="na">scrape_interval</span><span class="pi">:</span> <span class="s">15s</span>
    <span class="na">static_configs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">targets</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">trading-service:8080'</span><span class="pi">]</span>
</code></pre></div></div>

<h2 id="business-level-metrics-in-grafana">Business-Level Metrics in Grafana</h2>

<p>Technical metrics (JVM heap, HTTP request rate, database pool size) are valuable but not sufficient. The dashboards I find most useful in production combine technical metrics with business metrics:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Kafka consumer lag — technical + business signal</span>
<span class="nc">Gauge</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"kafka.consumer.lag"</span><span class="o">,</span> <span class="n">consumerLag</span><span class="o">,</span> <span class="nl">ConsumerLagProvider:</span><span class="o">:</span><span class="n">getTotalLag</span><span class="o">)</span>
    <span class="o">.</span><span class="na">tags</span><span class="o">(</span><span class="s">"topic"</span><span class="o">,</span> <span class="s">"claim-events"</span><span class="o">,</span> <span class="s">"consumer-group"</span><span class="o">,</span> <span class="s">"claims-processor"</span><span class="o">)</span>
    <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

<span class="c1">// Daily P&amp;L — business metric</span>
<span class="nc">Gauge</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.daily.pnl.pence"</span><span class="o">,</span> <span class="n">pnlTracker</span><span class="o">,</span> <span class="nl">PnlTracker:</span><span class="o">:</span><span class="n">getDailyPnlPence</span><span class="o">)</span>
    <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>

<span class="c1">// Kill switch state — operational metric</span>
<span class="nc">Gauge</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"trading.kill.switch.active"</span><span class="o">,</span>
        <span class="n">riskController</span><span class="o">,</span> <span class="n">rc</span> <span class="o">-&gt;</span> <span class="n">rc</span><span class="o">.</span><span class="na">isKillSwitchActive</span><span class="o">()</span> <span class="o">?</span> <span class="mf">1.0</span> <span class="o">:</span> <span class="mf">0.0</span><span class="o">)</span>
    <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">registry</span><span class="o">);</span>
</code></pre></div></div>

<p>In Grafana, set alerts on:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">trading.kill.switch.active == 1</code> → page immediately</li>
  <li><code class="language-plaintext highlighter-rouge">kafka.consumer.lag &gt; 1000</code> for a sustained 5 minutes → investigate</li>
  <li><code class="language-plaintext highlighter-rouge">trading.bets.failed</code> rate &gt; 5% → investigate</li>
  <li><code class="language-plaintext highlighter-rouge">jvm.memory.used / jvm.memory.max &gt; 0.85</code> → memory pressure</li>
</ul>

<h2 id="distributed-tracing">Distributed Tracing</h2>

<p>For request flows that span multiple services, add distributed tracing:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>io.micrometer<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>micrometer-tracing-bridge-brave<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
<span class="nt">&lt;dependency&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>io.zipkin.reporter2<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>zipkin-reporter-brave<span class="nt">&lt;/artifactId&gt;</span>
<span class="nt">&lt;/dependency&gt;</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">management</span><span class="pi">:</span>
  <span class="na">tracing</span><span class="pi">:</span>
    <span class="na">sampling</span><span class="pi">:</span>
      <span class="na">probability</span><span class="pi">:</span> <span class="m">0.1</span>  <span class="c1"># sample 10% of requests</span>
</code></pre></div></div>

<p>Traces propagate automatically across HTTP calls and Kafka messages in Spring Boot 3.x. The <code class="language-plaintext highlighter-rouge">traceId</code> appears in structured log output, linking logs to the trace in Zipkin or Grafana Tempo.</p>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Instrument the things that change your decision about the system.</strong> Metrics exist to inform action. Before adding a metric, ask: “what would I do differently if this number changed?” If the answer is nothing, skip the metric.</li>
  <li><strong>Use <code class="language-plaintext highlighter-rouge">@Timed</code> for quick wins.</strong> Spring’s <code class="language-plaintext highlighter-rouge">@Timed</code> annotation on <code class="language-plaintext highlighter-rouge">@RequestMapping</code> methods and <code class="language-plaintext highlighter-rouge">@KafkaListener</code> handlers instruments latency automatically without boilerplate.</li>
  <li><strong>Keep Actuator endpoints off the public port.</strong> Use <code class="language-plaintext highlighter-rouge">management.server.port=8090</code> to expose Actuator on a separate port, accessible only within your VPC. Don’t expose internal health details to the internet.</li>
  <li><strong>Add runbook links to your Grafana alerts.</strong> When an alert fires, the responder should immediately know what to check and what actions are available. A link to a runbook in the alert annotation makes this automatic.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="Spring Boot" /><category term="Observability" /><category term="Prometheus" /><category term="Micrometer" /><category term="Actuator" /><summary type="html"><![CDATA[Building production-grade observability into Spring Boot — Actuator health endpoints, custom health indicators, Micrometer metrics, Prometheus export, and instrumenting business-level metrics.]]></summary></entry><entry><title type="html">Betfair | Pre-Race vs In-Play Trading — Technical Architecture</title><link href="https://www.trinitylogic.co.uk/blog/betfair-pre-race-vs-in-play-trading-architecture" rel="alternate" type="text/html" title="Betfair | Pre-Race vs In-Play Trading — Technical Architecture" /><published>2025-09-12T00:00:00+00:00</published><updated>2025-09-12T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/betfair-pre-race-vs-in-play-trading-architecture</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/betfair-pre-race-vs-in-play-trading-architecture"><![CDATA[<p>Pre-race and in-play trading on Betfair are fundamentally different problems. Pre-race trading operates over minutes, prices move predictably enough to apply analytical signals, and the risk of catastrophic loss on a single decision is manageable. In-play trading operates in seconds or fractions of seconds, prices can halve or double on a single event, and the consequences of a position left unhedged during a suspension cascade can be severe. Building a system that handles both modes cleanly — without either polluting the other — requires deliberate architectural choices.</p>

<h2 id="market-state-transitions">Market State Transitions</h2>

<p>A Betfair market moves through several states, visible in the Streaming API’s <code class="language-plaintext highlighter-rouge">MarketDefinition</code>:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="nc">MarketStatus</span> <span class="o">{</span>
    <span class="no">INACTIVE</span><span class="o">,</span>       <span class="c1">// market exists but not yet accepting bets</span>
    <span class="no">OPEN</span><span class="o">,</span>           <span class="c1">// pre-race, accepting bets, match-time clock running</span>
    <span class="no">SUSPENDED</span><span class="o">,</span>      <span class="c1">// temporarily suspended (VAR check, injury, etc.)</span>
    <span class="no">CLOSED</span>          <span class="c1">// market settled</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The transition that matters most for trading architecture is <code class="language-plaintext highlighter-rouge">OPEN → IN_PLAY</code>. This happens at the event start and is signalled in the streaming API via <code class="language-plaintext highlighter-rouge">MarketDefinition.inPlay = true</code>.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMarketDefinitionChange</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="nc">MarketDefinition</span> <span class="n">definition</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">MarketState</span> <span class="n">current</span> <span class="o">=</span> <span class="n">markets</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">current</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span>

    <span class="kt">boolean</span> <span class="n">wasInPlay</span> <span class="o">=</span> <span class="n">current</span><span class="o">.</span><span class="na">isInPlay</span><span class="o">();</span>
    <span class="kt">boolean</span> <span class="n">isNowInPlay</span> <span class="o">=</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">TRUE</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">definition</span><span class="o">.</span><span class="na">isInPlay</span><span class="o">());</span>

    <span class="k">if</span> <span class="o">(!</span><span class="n">wasInPlay</span> <span class="o">&amp;&amp;</span> <span class="n">isNowInPlay</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">onMarketGoesInPlay</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">definition</span><span class="o">.</span><span class="na">getStatus</span><span class="o">()</span> <span class="o">==</span> <span class="nc">MarketStatus</span><span class="o">.</span><span class="na">SUSPENDED</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">onMarketSuspended</span><span class="o">(</span><span class="n">marketId</span><span class="o">,</span> <span class="n">wasInPlay</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="n">current</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">definition</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">onMarketGoesInPlay</code> event is your trigger to switch from pre-race mode (signal-based, deliberate entry) to in-play mode (position management, hedging, emergency exits).</p>

<h2 id="pre-race-architecture">Pre-Race Architecture</h2>

<p>Pre-race trading has time on its side. Your analytical loop runs every 500ms–1s, evaluating WoM, LTP, and OFI signals. The latency requirements are lenient — a 50ms delay in signal processing rarely changes the outcome.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">PreRaceStrategyEngine</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">MarketSignalBuffer</span><span class="o">&gt;</span> <span class="n">signalBuffers</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>

    <span class="c1">// Called on each Streaming API delta</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMarketUpdate</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="nc">MarketChangeMessage</span> <span class="n">mcm</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">MarketSignalBuffer</span> <span class="n">buffer</span> <span class="o">=</span> <span class="n">signalBuffers</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">buffer</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span>

        <span class="n">buffer</span><span class="o">.</span><span class="na">addDelta</span><span class="o">(</span><span class="n">mcm</span><span class="o">);</span>

        <span class="c1">// Evaluate at most once per 500ms</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">buffer</span><span class="o">.</span><span class="na">shouldEvaluate</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">MarketSignals</span> <span class="n">signals</span> <span class="o">=</span> <span class="n">signalCalculator</span><span class="o">.</span><span class="na">calculate</span><span class="o">(</span><span class="n">buffer</span><span class="o">.</span><span class="na">getState</span><span class="o">());</span>
            <span class="n">strategies</span><span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="n">s</span> <span class="o">-&gt;</span> <span class="n">s</span><span class="o">.</span><span class="na">onSignalUpdate</span><span class="o">(</span><span class="n">context</span><span class="o">(</span><span class="n">marketId</span><span class="o">),</span> <span class="n">signals</span><span class="o">));</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMarketGoesInPlay</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Stop pre-race evaluation</span>
        <span class="n">signalBuffers</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
        <span class="c1">// Transfer to in-play engine</span>
        <span class="n">inPlayEngine</span><span class="o">.</span><span class="na">trackMarket</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">MarketSignalBuffer</code> rate-limits evaluation — no point running signal calculations on every Streaming API tick when the signals require 20-observation windows to be meaningful.</p>

<h2 id="in-play-architecture">In-Play Architecture</h2>

<p>In-play is a different beast. Prices move at sports event pace — a goal in football can move a selection from 2.5 to 1.2 in under a second. The in-play engine must respond in milliseconds, not seconds. In-play strategy is primarily about position management:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">InPlayPositionManager</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">InPlayPosition</span><span class="o">&gt;</span> <span class="n">openPositions</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConcurrentHashMap</span><span class="o">&lt;&gt;();</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">OrderManager</span> <span class="n">orderManager</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">RiskController</span> <span class="n">riskController</span><span class="o">;</span>

    <span class="c1">// Called on every Streaming API delta in-play</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onInPlayUpdate</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="nc">MarketChangeMessage</span> <span class="n">mcm</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">InPlayPosition</span> <span class="n">position</span> <span class="o">=</span> <span class="n">openPositions</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">position</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span>

        <span class="k">for</span> <span class="o">(</span><span class="nc">RunnerChange</span> <span class="n">rc</span> <span class="o">:</span> <span class="n">mcm</span><span class="o">.</span><span class="na">getMarketChanges</span><span class="o">().</span><span class="na">get</span><span class="o">(</span><span class="mi">0</span><span class="o">).</span><span class="na">getRunnerChanges</span><span class="o">())</span> <span class="o">{</span>
            <span class="kt">double</span> <span class="n">currentLtp</span> <span class="o">=</span> <span class="n">extractLtp</span><span class="o">(</span><span class="n">rc</span><span class="o">);</span>
            <span class="n">position</span><span class="o">.</span><span class="na">updateLtp</span><span class="o">(</span><span class="n">rc</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">currentLtp</span><span class="o">);</span>

            <span class="c1">// Check hedge conditions</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">shouldHedge</span><span class="o">(</span><span class="n">position</span><span class="o">,</span> <span class="n">rc</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">currentLtp</span><span class="o">))</span> <span class="o">{</span>
                <span class="n">hedgePosition</span><span class="o">(</span><span class="n">marketId</span><span class="o">,</span> <span class="n">position</span><span class="o">,</span> <span class="n">rc</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">currentLtp</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">shouldHedge</span><span class="o">(</span><span class="nc">InPlayPosition</span> <span class="n">position</span><span class="o">,</span> <span class="kt">long</span> <span class="n">selectionId</span><span class="o">,</span> <span class="kt">double</span> <span class="n">currentLtp</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">double</span> <span class="n">entryPrice</span> <span class="o">=</span> <span class="n">position</span><span class="o">.</span><span class="na">getEntryPrice</span><span class="o">(</span><span class="n">selectionId</span><span class="o">);</span>
        <span class="kt">double</span> <span class="n">currentPnl</span> <span class="o">=</span> <span class="n">position</span><span class="o">.</span><span class="na">calculatePnl</span><span class="o">(</span><span class="n">selectionId</span><span class="o">,</span> <span class="n">currentLtp</span><span class="o">);</span>

        <span class="c1">// Hedge if profit target hit</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">currentPnl</span> <span class="o">&gt;=</span> <span class="n">position</span><span class="o">.</span><span class="na">getProfitTarget</span><span class="o">())</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>

        <span class="c1">// Hedge if stop loss hit</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">currentPnl</span> <span class="o">&lt;=</span> <span class="n">position</span><span class="o">.</span><span class="na">getStopLoss</span><span class="o">())</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>

        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The critical difference: no signal buffering in-play. Every delta is evaluated immediately.</p>

<h2 id="api-rate-limits-in-play">API Rate Limits In-Play</h2>

<p>Betfair applies tighter API rate limits in-play. The REST API allows around 5 calls per second per application key for order operations in-play — roughly half the pre-race limit in some markets. This is why in-play order management must be batched: multiple position hedges in the same API call wherever possible:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">hedgeMultiplePositions</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">HedgeInstruction</span><span class="o">&gt;</span> <span class="n">hedges</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">List</span><span class="o">&lt;</span><span class="nc">PlaceInstruction</span><span class="o">&gt;</span> <span class="n">instructions</span> <span class="o">=</span> <span class="n">hedges</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
        <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">h</span> <span class="o">-&gt;</span> <span class="n">buildLayInstruction</span><span class="o">(</span><span class="n">h</span><span class="o">.</span><span class="na">selectionId</span><span class="o">(),</span> <span class="n">h</span><span class="o">.</span><span class="na">price</span><span class="o">(),</span> <span class="n">h</span><span class="o">.</span><span class="na">size</span><span class="o">()))</span>
        <span class="o">.</span><span class="na">toList</span><span class="o">();</span>

    <span class="c1">// One API call for all hedges</span>
    <span class="nc">PlaceExecutionReport</span> <span class="n">report</span> <span class="o">=</span> <span class="n">orderManager</span><span class="o">.</span><span class="na">placeOrders</span><span class="o">(</span><span class="n">marketId</span><span class="o">,</span> <span class="n">instructions</span><span class="o">);</span>
    <span class="n">processReport</span><span class="o">(</span><span class="n">report</span><span class="o">,</span> <span class="n">hedges</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="suspension-handling">Suspension Handling</h2>

<p>Suspension is particularly dangerous in-play. When a market suspends, open positions can’t be hedged, but liability from unmatched lays remains. Your architecture must:</p>

<ol>
  <li>Detect suspension immediately (streaming API <code class="language-plaintext highlighter-rouge">MarketDefinition.status = SUSPENDED</code>)</li>
  <li>Record all open position states</li>
  <li>Attempt to cancel unmatched portions (may fail if market is suspended)</li>
  <li>Wait for resumption</li>
  <li>Reassess positions at the new prices</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMarketSuspended</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">wasInPlay</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Market {} suspended (in-play: {})"</span><span class="o">,</span> <span class="n">marketId</span><span class="o">,</span> <span class="n">wasInPlay</span><span class="o">);</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">wasInPlay</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// In-play suspension — prices will change on resume</span>
        <span class="nc">InPlayPosition</span> <span class="n">position</span> <span class="o">=</span> <span class="n">openPositions</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">position</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">position</span><span class="o">.</span><span class="na">flagSuspended</span><span class="o">();</span>
            <span class="c1">// Attempt cancel but expect potential failure</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="n">orderManager</span><span class="o">.</span><span class="na">cancelAllOpenOrders</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Cancel on suspension failed — will retry on resume"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
                <span class="n">position</span><span class="o">.</span><span class="na">setPendingCancel</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>

<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMarketResumed</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">InPlayPosition</span> <span class="n">position</span> <span class="o">=</span> <span class="n">openPositions</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">position</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span>

    <span class="n">position</span><span class="o">.</span><span class="na">clearSuspended</span><span class="o">();</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">position</span><span class="o">.</span><span class="na">isPendingCancel</span><span class="o">())</span> <span class="o">{</span>
        <span class="c1">// Retry cancel now market is live again</span>
        <span class="n">orderManager</span><span class="o">.</span><span class="na">cancelAllOpenOrders</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="c1">// Reconcile — prices may have moved significantly during suspension</span>
    <span class="n">orderManager</span><span class="o">.</span><span class="na">reconcileOpenOrders</span><span class="o">(</span><span class="n">marketId</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Don’t carry pre-race positions into in-play unless you intend to.</strong> Use <code class="language-plaintext highlighter-rouge">PersistenceType.LAPSE</code> on pre-race orders — they’ll cancel at the off unless you explicitly convert them. An accidental in-play position at bad odds can ruin a profitable session.</li>
  <li><strong>In-play latency is network, not application, limited.</strong> Even with a well-optimised Java application, the Betfair API introduces 50–150ms round-trip latency from most UK locations. In-play execution should acknowledge this and only target markets where 150ms round-trip is acceptable.</li>
  <li><strong>Test suspension recovery in staging.</strong> Simulate a suspension by force-closing the streaming connection mid-position and verify your recovery logic handles it correctly. Suspension in a live race happens fast and the recovery window is short.</li>
  <li><strong>Log every in-play decision with microsecond timestamps.</strong> When reviewing a session, you need to know whether your hedge hit at the right price and in the right time window. Millisecond timestamps are often not granular enough for in-play analysis.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="Betfair" /><category term="Trading" /><category term="In-Play" /><category term="Architecture" /><summary type="html"><![CDATA[The technical and strategic differences between pre-race and in-play Betfair trading — market state transitions, streaming API indicators, latency requirements, and Java architecture for both modes.]]></summary></entry><entry><title type="html">AWS | AWS Lambda with Spring Boot — Cold Start Optimisation</title><link href="https://www.trinitylogic.co.uk/blog/aws-lambda-spring-boot-cold-start" rel="alternate" type="text/html" title="AWS | AWS Lambda with Spring Boot — Cold Start Optimisation" /><published>2025-09-05T00:00:00+00:00</published><updated>2025-09-05T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/aws-lambda-spring-boot-cold-start</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/aws-lambda-spring-boot-cold-start"><![CDATA[<p>AWS Lambda is a natural fit for event-driven architectures — and it’s where I’ve deployed several components of financial data ingestion pipelines and AWS-hosted services. The friction comes from Java’s cold start time. A standard Spring Boot application with its full context can take 8–15 seconds to initialise on a fresh Lambda container. For APIs where users experience that latency, it’s unacceptable. The good news is there are several techniques that bring cold starts to under 1 second, and in many cases the choice of approach is straightforward.</p>

<h2 id="why-java-cold-starts-are-slow">Why Java Cold Starts Are Slow</h2>

<p>Lambda cold start time breaks down into:</p>
<ol>
  <li>Container provisioning (AWS overhead, ~200ms — not your problem)</li>
  <li>JVM startup and class loading</li>
  <li>Spring ApplicationContext initialisation — component scanning, auto-configuration, bean creation</li>
  <li>Your application’s startup logic</li>
</ol>

<p>Step 3 is the main culprit. Spring Boot on the JVM needs to load and initialise hundreds of beans, run auto-configuration, and connect to external dependencies. On a warmed Lambda with an already-running JVM, this cost is zero. On a cold start, you pay it in full.</p>

<h2 id="baseline-measurement">Baseline Measurement</h2>

<p>Before optimising, measure. Add the AWS Lambda Powertools Java dependency for structured metrics:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ClaimHandler</span> <span class="kd">implements</span> <span class="nc">RequestHandler</span><span class="o">&lt;</span><span class="nc">APIGatewayProxyRequestEvent</span><span class="o">,</span> <span class="nc">APIGatewayProxyResponseEvent</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ClaimService</span> <span class="n">claimService</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">ClaimHandler</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// This constructor runs at cold start</span>
        <span class="kt">long</span> <span class="n">start</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
        <span class="k">this</span><span class="o">.</span><span class="na">claimService</span> <span class="o">=</span> <span class="nc">SpringLambda</span><span class="o">.</span><span class="na">getBean</span><span class="o">(</span><span class="nc">ClaimService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">initTime</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">-</span> <span class="n">start</span><span class="o">;</span>
        <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Cold start init time: {}ms"</span><span class="o">,</span> <span class="n">initTime</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">APIGatewayProxyResponseEvent</span> <span class="nf">handleRequest</span><span class="o">(</span>
            <span class="nc">APIGatewayProxyRequestEvent</span> <span class="n">event</span><span class="o">,</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// Warm invocations arrive here directly</span>
        <span class="k">return</span> <span class="n">claimService</span><span class="o">.</span><span class="na">handle</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Log and track <code class="language-plaintext highlighter-rouge">initTime</code>. Track in CloudWatch and alert when p99 cold start time exceeds your threshold.</p>

<h2 id="snapstart--fastest-path-to-improvement">SnapStart — Fastest Path to Improvement</h2>

<p>AWS Lambda SnapStart (available for Java 11+) snapshots the Lambda execution environment after initialisation and restores from the snapshot on subsequent cold starts. For Spring Boot applications, this means you pay the initialisation cost once — future cold starts restore from the snapshot in ~200ms.</p>

<p>Enable it in your <code class="language-plaintext highlighter-rouge">template.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">AWSTemplateFormatVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2010-09-09'</span>
<span class="na">Transform</span><span class="pi">:</span> <span class="s">AWS::Serverless-2016-10-31</span>

<span class="na">Resources</span><span class="pi">:</span>
  <span class="na">ClaimFunction</span><span class="pi">:</span>
    <span class="na">Type</span><span class="pi">:</span> <span class="s">AWS::Serverless::Function</span>
    <span class="na">Properties</span><span class="pi">:</span>
      <span class="na">Runtime</span><span class="pi">:</span> <span class="s">java21</span>
      <span class="na">SnapStart</span><span class="pi">:</span>
        <span class="na">ApplyOn</span><span class="pi">:</span> <span class="s">PublishedVersions</span>
      <span class="na">Handler</span><span class="pi">:</span> <span class="s">com.example.ClaimHandler</span>
      <span class="na">CodeUri</span><span class="pi">:</span> <span class="s">target/claim-service.jar</span>
      <span class="na">MemorySize</span><span class="pi">:</span> <span class="m">512</span>
      <span class="na">Timeout</span><span class="pi">:</span> <span class="m">30</span>
</code></pre></div></div>

<p>SnapStart requires that your initialisation code is idempotent — the snapshot is taken once and restored many times. Watch for:</p>
<ul>
  <li>Random values or UUIDs generated at init time (cached incorrectly across restorations)</li>
  <li>External connections opened at init time (restored connections may be stale)</li>
</ul>

<p>Implement <code class="language-plaintext highlighter-rouge">CRaC</code> hooks to handle pre-checkpoint and post-restore lifecycle:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">org.crac.Context</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.crac.Core</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.crac.Resource</span><span class="o">;</span>

<span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SnapStartLifecycle</span> <span class="kd">implements</span> <span class="nc">Resource</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">DataSource</span> <span class="n">dataSource</span><span class="o">;</span>

    <span class="nd">@PostConstruct</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">register</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Core</span><span class="o">.</span><span class="na">getGlobalContext</span><span class="o">().</span><span class="na">register</span><span class="o">(</span><span class="k">this</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">beforeCheckpoint</span><span class="o">(</span><span class="nc">Context</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Resource</span><span class="o">&gt;</span> <span class="n">ctx</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="c1">// Close connections before snapshot is taken</span>
        <span class="n">dataSource</span><span class="o">.</span><span class="na">getConnection</span><span class="o">().</span><span class="na">close</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">afterRestore</span><span class="o">(</span><span class="nc">Context</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">Resource</span><span class="o">&gt;</span> <span class="n">ctx</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="c1">// Re-establish connections after restore</span>
        <span class="n">dataSource</span><span class="o">.</span><span class="na">getConnection</span><span class="o">();</span> <span class="c1">// warm up connection pool</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="graalvm-native-image">GraalVM Native Image</h2>

<p>GraalVM compiles your Spring Boot application to a native executable — no JVM startup, AOT-compiled code. Cold starts of 50–200ms are achievable:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;plugin&gt;</span>
    <span class="nt">&lt;groupId&gt;</span>org.graalvm.buildtools<span class="nt">&lt;/groupId&gt;</span>
    <span class="nt">&lt;artifactId&gt;</span>native-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
    <span class="nt">&lt;configuration&gt;</span>
        <span class="nt">&lt;imageName&gt;</span>claim-function<span class="nt">&lt;/imageName&gt;</span>
        <span class="nt">&lt;buildArgs&gt;</span>
            <span class="nt">&lt;buildArg&gt;</span>--no-fallback<span class="nt">&lt;/buildArg&gt;</span>
            <span class="nt">&lt;buildArg&gt;</span>--enable-url-protocols=https<span class="nt">&lt;/buildArg&gt;</span>
        <span class="nt">&lt;/buildArgs&gt;</span>
    <span class="nt">&lt;/configuration&gt;</span>
<span class="nt">&lt;/plugin&gt;</span>
</code></pre></div></div>

<p>Spring Boot 3.x has first-class GraalVM support via <code class="language-plaintext highlighter-rouge">spring-boot-starter</code> + AOT processing. Most Spring features work — but dynamic features (reflection, proxies, runtime classpath scanning) require explicit hint configuration:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RegisterReflectionForBinding</span><span class="o">({</span><span class="nc">ClaimEvent</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">ClaimResponse</span><span class="o">.</span><span class="na">class</span><span class="o">})</span>
<span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LambdaConfig</span> <span class="o">{</span>
    <span class="c1">// beans for Lambda handler</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Native image compilation is slow (5–10 minutes on a typical build machine) and imposes constraints on dynamic features. For Lambda functions with stable, well-defined inputs and outputs, it’s the best option for cold start performance.</p>

<h2 id="tiered-compilation-flags">Tiered Compilation Flags</h2>

<p>Without native image, JVM flags reduce cold start time:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># In Lambda function environment variables</span>
<span class="nv">JAVA_TOOL_OPTIONS</span><span class="o">=</span><span class="s2">"-XX:+TieredCompilation -XX:TieredStopAtLevel=1"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TieredStopAtLevel=1</code> disables C2 (optimising) compilation and uses only the fast interpreter + C1 (baseline) compiler. Startup is faster; peak throughput is lower. For Lambda where invocations are short-lived, this is generally the right trade-off.</p>

<h2 id="memory-configuration">Memory Configuration</h2>

<p>Lambda CPU allocation scales with memory. A 512MB Lambda gets half a vCPU; 1024MB gets a full vCPU; 1769MB gets two vCPUs. For a Spring Boot application:</p>

<ul>
  <li>Cold start time at 256MB: ~6s</li>
  <li>Cold start time at 512MB: ~3s</li>
  <li>Cold start time at 1024MB: ~1.5s</li>
  <li>Cold start time at 2048MB: ~800ms</li>
</ul>

<p>Extra memory reduces cold start time faster than the marginal cost increase. For a Lambda behind an API Gateway, 1024–2048MB is often optimal.</p>

<h2 id="when-lambda-is-the-wrong-choice-for-java">When Lambda Is the Wrong Choice for Java</h2>

<p>Lambda is not appropriate for:</p>
<ul>
  <li><strong>Sustained high-throughput workloads.</strong> Lambda’s per-invocation billing and cold start overhead make it expensive and latency-variable compared to a sized ECS Fargate service.</li>
  <li><strong>Long-running Kafka consumers.</strong> Lambda has a maximum execution time of 15 minutes. A continuous consumer belongs on ECS or EKS.</li>
  <li><strong>Applications with tight p99 latency requirements.</strong> Cold starts on Lambda are non-deterministic. If your SLA requires consistent sub-100ms p99, use a persistently-running service.</li>
</ul>

<p>Lambda is ideal for: event-driven processing triggered by S3, SQS, SNS, EventBridge; infrequent API endpoints; scheduled batch jobs; and data transformation functions.</p>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Start with SnapStart.</strong> It’s the lowest-effort, highest-impact improvement for most Spring Boot Lambdas. Enable it, measure cold starts, and decide if further optimisation is needed.</li>
  <li><strong>Use Spring’s functional bean definition style.</strong> <code class="language-plaintext highlighter-rouge">BeanDefinitionCustomizerParameterizedCondition</code> and <code class="language-plaintext highlighter-rouge">@Bean</code>-based configuration start faster than classpath component scanning.</li>
  <li><strong>Measure warm invocations too.</strong> A Lambda with a 2-second cold start and a 100ms warm invocation has a very different cost/performance profile than one with a 200ms cold start and a 2-second warm invocation.</li>
  <li><strong>Set reserved concurrency on critical Lambdas.</strong> Reserved concurrency keeps containers warm and bounds cold start exposure for latency-sensitive paths.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="AWS" /><category term="Lambda" /><category term="Spring Boot" /><category term="Serverless" /><summary type="html"><![CDATA[Deploying Spring Boot to AWS Lambda and tackling cold start latency — GraalVM native image, SnapStart, tiered compilation, and knowing when Lambda is the wrong choice for Java workloads.]]></summary></entry><entry><title type="html">Java | Structured Concurrency in Java 21</title><link href="https://www.trinitylogic.co.uk/blog/java-21-structured-concurrency" rel="alternate" type="text/html" title="Java | Structured Concurrency in Java 21" /><published>2025-08-29T00:00:00+00:00</published><updated>2025-08-29T00:00:00+00:00</updated><id>https://www.trinitylogic.co.uk/blog/java-21-structured-concurrency</id><content type="html" xml:base="https://www.trinitylogic.co.uk/blog/java-21-structured-concurrency"><![CDATA[<p>Before structured concurrency, managing a group of concurrent tasks in Java was error-prone. If you forked three async operations with <code class="language-plaintext highlighter-rouge">CompletableFuture</code> and one failed, the other two kept running — consuming resources, potentially making external calls, and potentially producing results that would never be used. Structured concurrency, finalised in Java 21, gives you a disciplined model: tasks are scoped, they have clear lifetimes, and the scope doesn’t exit until all tasks have finished or been cancelled.</p>

<h2 id="the-core-idea">The Core Idea</h2>

<p>Structured concurrency enforces a simple invariant: <strong>a task’s lifetime is bounded by the scope that created it</strong>. The scope doesn’t complete until all forked tasks have completed. If you exit the scope with tasks still running, they’re cancelled. This eliminates the most common class of thread leak in concurrent Java.</p>

<p><code class="language-plaintext highlighter-rouge">StructuredTaskScope</code> is the central API:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span> <span class="o">(</span><span class="kt">var</span> <span class="n">scope</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StructuredTaskScope</span><span class="o">.</span><span class="na">ShutdownOnFailure</span><span class="o">())</span> <span class="o">{</span>
    <span class="kt">var</span> <span class="n">runnerTask</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">fetchRunners</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
    <span class="kt">var</span> <span class="n">priceTask</span>  <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">fetchPrices</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
    <span class="kt">var</span> <span class="n">formTask</span>   <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">fetchForm</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>

    <span class="n">scope</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>           <span class="c1">// wait for all tasks to complete</span>
    <span class="n">scope</span><span class="o">.</span><span class="na">throwIfFailed</span><span class="o">();</span>  <span class="c1">// propagate any exceptions</span>

    <span class="k">return</span> <span class="k">new</span> <span class="nf">MarketView</span><span class="o">(</span>
        <span class="n">runnerTask</span><span class="o">.</span><span class="na">get</span><span class="o">(),</span>
        <span class="n">priceTask</span><span class="o">.</span><span class="na">get</span><span class="o">(),</span>
        <span class="n">formTask</span><span class="o">.</span><span class="na">get</span><span class="o">()</span>
    <span class="o">);</span>
<span class="o">}</span> <span class="c1">// scope closes here — all tasks guaranteed done or cancelled</span>
</code></pre></div></div>

<p>Three fetches run concurrently. If any one throws, <code class="language-plaintext highlighter-rouge">scope.join()</code> returns, <code class="language-plaintext highlighter-rouge">throwIfFailed()</code> throws the exception, and the <code class="language-plaintext highlighter-rouge">try-with-resources</code> close cancels the remaining tasks. No dangling threads.</p>

<h2 id="shutdownonfailure--all-must-succeed">ShutdownOnFailure — All Must Succeed</h2>

<p><code class="language-plaintext highlighter-rouge">ShutdownOnFailure</code> shuts down the scope as soon as any task fails, then cancels remaining tasks:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">EnrichedMarket</span> <span class="nf">enrichMarket</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">(</span><span class="kt">var</span> <span class="n">scope</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StructuredTaskScope</span><span class="o">.</span><span class="na">ShutdownOnFailure</span><span class="o">())</span> <span class="o">{</span>
        <span class="nc">Subtask</span><span class="o">&lt;</span><span class="nc">RunnerData</span><span class="o">&gt;</span>  <span class="n">runners</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">runnerService</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
        <span class="nc">Subtask</span><span class="o">&lt;</span><span class="nc">FormData</span><span class="o">&gt;</span>    <span class="n">form</span>    <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">formService</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
        <span class="nc">Subtask</span><span class="o">&lt;</span><span class="nc">WeatherData</span><span class="o">&gt;</span> <span class="n">weather</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">weatherService</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>

        <span class="n">scope</span><span class="o">.</span><span class="na">join</span><span class="o">().</span><span class="na">throwIfFailed</span><span class="o">();</span> <span class="c1">// join and propagate first failure</span>

        <span class="k">return</span> <span class="nc">EnrichedMarket</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">runners</span><span class="o">.</span><span class="na">get</span><span class="o">(),</span> <span class="n">form</span><span class="o">.</span><span class="na">get</span><span class="o">(),</span> <span class="n">weather</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Subtask.get()</code> is safe to call after <code class="language-plaintext highlighter-rouge">throwIfFailed()</code> — all tasks either succeeded (state = <code class="language-plaintext highlighter-rouge">SUCCESS</code>) or were cancelled. Calling <code class="language-plaintext highlighter-rouge">get()</code> on a cancelled subtask throws <code class="language-plaintext highlighter-rouge">IllegalStateException</code> — which is why you check the scope result before calling <code class="language-plaintext highlighter-rouge">get()</code>.</p>

<p>If you need a timeout:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">scope</span><span class="o">.</span><span class="na">joinUntil</span><span class="o">(</span><span class="nc">Instant</span><span class="o">.</span><span class="na">now</span><span class="o">().</span><span class="na">plusSeconds</span><span class="o">(</span><span class="mi">5</span><span class="o">));</span> <span class="c1">// deadline, not duration</span>
<span class="n">scope</span><span class="o">.</span><span class="na">throwIfFailed</span><span class="o">();</span>
</code></pre></div></div>

<p>If the deadline passes before all tasks finish, the scope cancels remaining tasks and <code class="language-plaintext highlighter-rouge">throwIfFailed()</code> throws a <code class="language-plaintext highlighter-rouge">TimeoutException</code>.</p>

<h2 id="shutdownonsuccess--first-to-win">ShutdownOnSuccess — First to Win</h2>

<p><code class="language-plaintext highlighter-rouge">ShutdownOnSuccess</code> completes as soon as the first task succeeds, cancels the rest:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">MarketData</span> <span class="nf">fetchFromFastestSource</span><span class="o">(</span><span class="nc">String</span> <span class="n">marketId</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">(</span><span class="kt">var</span> <span class="n">scope</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StructuredTaskScope</span><span class="o">.</span><span class="na">ShutdownOnSuccess</span><span class="o">&lt;</span><span class="nc">MarketData</span><span class="o">&gt;())</span> <span class="o">{</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">primarySource</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">secondarySource</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>
        <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">tertiarySource</span><span class="o">.</span><span class="na">fetch</span><span class="o">(</span><span class="n">marketId</span><span class="o">));</span>

        <span class="n">scope</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">scope</span><span class="o">.</span><span class="na">result</span><span class="o">();</span> <span class="c1">// returns the first successful result</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>This is a redundant-fetch pattern: three providers are queried simultaneously, and the fastest successful response wins. The others are cancelled. It’s useful when you need low latency and have multiple data sources of varying reliability.</p>

<h2 id="preventing-thread-leaks">Preventing Thread Leaks</h2>

<p>The classic <code class="language-plaintext highlighter-rouge">CompletableFuture</code> leak pattern:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Old — if processA fails, processB keeps running and nobody notices</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">ResultA</span><span class="o">&gt;</span> <span class="n">a</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">processA</span><span class="o">());</span>
<span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">ResultB</span><span class="o">&gt;</span> <span class="n">b</span> <span class="o">=</span> <span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">processB</span><span class="o">());</span>

<span class="nc">ResultA</span> <span class="n">ra</span> <span class="o">=</span> <span class="n">a</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>  <span class="c1">// throws if a failed</span>
<span class="nc">ResultB</span> <span class="n">rb</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>  <span class="c1">// b is still running, but we've already thrown</span>
</code></pre></div></div>

<p>With structured concurrency:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// New — if processA fails, processB is cancelled before the scope exits</span>
<span class="k">try</span> <span class="o">(</span><span class="kt">var</span> <span class="n">scope</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StructuredTaskScope</span><span class="o">.</span><span class="na">ShutdownOnFailure</span><span class="o">())</span> <span class="o">{</span>
    <span class="kt">var</span> <span class="n">a</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">processA</span><span class="o">());</span>
    <span class="kt">var</span> <span class="n">b</span> <span class="o">=</span> <span class="n">scope</span><span class="o">.</span><span class="na">fork</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="n">processB</span><span class="o">());</span>

    <span class="n">scope</span><span class="o">.</span><span class="na">join</span><span class="o">().</span><span class="na">throwIfFailed</span><span class="o">();</span>
    <span class="k">return</span> <span class="nf">process</span><span class="o">(</span><span class="n">a</span><span class="o">.</span><span class="na">get</span><span class="o">(),</span> <span class="n">b</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="o">}</span>
<span class="c1">// b is guaranteed done or cancelled here</span>
</code></pre></div></div>

<p>The scope is the mechanism that provides the guarantee. As a <code class="language-plaintext highlighter-rouge">AutoCloseable</code>, it’s compatible with <code class="language-plaintext highlighter-rouge">try-with-resources</code>, and the close operation waits for tasks to terminate.</p>

<h2 id="custom-scope-policies">Custom Scope Policies</h2>

<p>For more complex fan-out/fan-in scenarios, extend <code class="language-plaintext highlighter-rouge">StructuredTaskScope</code> with a custom policy:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MajorityResultScope</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="kd">extends</span> <span class="nc">StructuredTaskScope</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">results</span> <span class="o">=</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">synchronizedList</span><span class="o">(</span><span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;());</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">required</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nf">MajorityResultScope</span><span class="o">(</span><span class="kt">int</span> <span class="n">required</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">required</span> <span class="o">=</span> <span class="n">required</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">handleComplete</span><span class="o">(</span><span class="nc">Subtask</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="no">T</span><span class="o">&gt;</span> <span class="n">subtask</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">subtask</span><span class="o">.</span><span class="na">state</span><span class="o">()</span> <span class="o">==</span> <span class="nc">Subtask</span><span class="o">.</span><span class="na">State</span><span class="o">.</span><span class="na">SUCCESS</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">results</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">subtask</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">results</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">&gt;=</span> <span class="n">required</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">shutdown</span><span class="o">();</span> <span class="c1">// enough results — cancel remaining</span>
            <span class="o">}</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="nf">results</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="nc">List</span><span class="o">.</span><span class="na">copyOf</span><span class="o">(</span><span class="n">results</span><span class="o">);</span> <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="structured-concurrency-vs-completablefuture">Structured Concurrency vs CompletableFuture</h2>

<p>Both have their place in Java 21:</p>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>Structured Concurrency</th>
      <th>CompletableFuture</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Code readability</td>
      <td>Sequential, clear</td>
      <td>Callback chains, dense</td>
    </tr>
    <tr>
      <td>Thread leak risk</td>
      <td>Impossible by design</td>
      <td>Requires careful handling</td>
    </tr>
    <tr>
      <td>Exception propagation</td>
      <td>Clean, first failure wins</td>
      <td>Wrapped in CompletionException</td>
    </tr>
    <tr>
      <td>Complex async pipelines</td>
      <td>Cumbersome</td>
      <td>Natural (thenCompose, etc.)</td>
    </tr>
    <tr>
      <td>Debugging</td>
      <td>Standard thread dumps</td>
      <td>Async stack traces are poor</td>
    </tr>
    <tr>
      <td>Java version</td>
      <td>Java 21+ (stable)</td>
      <td>Java 8+</td>
    </tr>
  </tbody>
</table>

<p>I use structured concurrency for fan-out patterns where I need all results (or the first result) and clean cancellation behaviour. I still use <code class="language-plaintext highlighter-rouge">CompletableFuture</code> for complex reactive pipelines where thenCompose chains read more naturally than nested scopes.</p>

<h2 id="protips">ProTips</h2>

<ul>
  <li><strong>Virtual threads are the natural partner.</strong> <code class="language-plaintext highlighter-rouge">StructuredTaskScope.fork()</code> creates virtual threads by default in Java 21. The combination gives you millions of lightweight concurrent tasks with disciplined scoping.</li>
  <li><strong>Avoid mixing structured concurrency with <code class="language-plaintext highlighter-rouge">CompletableFuture.allOf</code>.</strong> Wrapping <code class="language-plaintext highlighter-rouge">CompletableFuture</code> inside a structured scope defeats the purpose — the futures run independently of the scope’s lifecycle.</li>
  <li><strong>Propagate interruption correctly.</strong> Tasks forked in a scope receive an interrupt when the scope shuts down. Ensure your tasks check <code class="language-plaintext highlighter-rouge">Thread.currentThread().isInterrupted()</code> or use interruptible IO — otherwise they won’t cancel promptly.</li>
  <li><strong>Structured concurrency is not for long-running tasks.</strong> A scope that forks a Kafka consumer that runs indefinitely doesn’t fit the structured model. Use it for bounded, request-scoped operations.</li>
</ul>

<p>If you’re looking for a Java contractor who knows this space inside out, <a href="/hire/">get in touch</a>.</p>]]></content><author><name>Samuel Jackson</name><email>jacksosa76@gmail.com</email></author><category term="Java" /><category term="Java 21" /><category term="Structured Concurrency" /><category term="Concurrency" /><category term="Threading" /><summary type="html"><![CDATA[Structured concurrency in Java 21 — StructuredTaskScope, ShutdownOnFailure, ShutdownOnSuccess, how it prevents thread leaks, and where it produces cleaner code than CompletableFuture.]]></summary></entry></feed>