<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nicolas DUMINIL</title>
    <description>The latest articles on DEV Community by Nicolas DUMINIL (@nicolasduminil).</description>
    <link>https://dev.to/nicolasduminil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F236502%2F3edf7d0b-a964-4798-b4d6-3488a2e7ab73.jpeg</url>
      <title>DEV Community: Nicolas DUMINIL</title>
      <link>https://dev.to/nicolasduminil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nicolasduminil"/>
    <language>en</language>
    <item>
      <title>Spring AI or LangChain4j? One App, Three Stacks, One Test</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Wed, 11 Nov 2026 12:33:52 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/spring-ai-or-langchain4j-one-app-three-stacks-one-test-16bc</link>
      <guid>https://dev.to/nicolasduminil/spring-ai-or-langchain4j-one-app-three-stacks-one-test-16bc</guid>
      <description>&lt;p&gt;Java has quietly become a credible place to build AI applications. That raises a practical question for anyone starting out: which framework do you reach for, &lt;a href="https://docs.spring.io/spring-ai/reference/" rel="noopener noreferrer"&gt;Spring AI&lt;/a&gt; or &lt;a href="https://docs.langchain4j.dev/" rel="noopener noreferrer"&gt;LangChain4j&lt;/a&gt;, and does it matter whether you run&lt;br&gt;
it on Spring Boot or on Quarkus?&lt;/p&gt;

&lt;p&gt;The consistent way to answer is not a feature table. It is to build the same thing on each stack and read the difference. This article does exactly that for the smallest useful AI application: one HTTP endpoint that forwards a question to a model and returns its answer. We build it three times, in Spring AI, in LangChain4j on Spring Boot, and in LangChain4j on Quarkus, and then hold all three to a single shared test.&lt;/p&gt;
&lt;h2&gt;
  
  
  One interface, three implementations
&lt;/h2&gt;

&lt;p&gt;Everything starts from one framework-neutral contract, a plain Java interface that knows nothing about any AI library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;QuestionAnsweringService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Question&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Question&lt;/code&gt; and &lt;code&gt;Answer&lt;/code&gt; are simple records. Each stack provides only its own implementation of this interface, and nothing framework-specific leaks into the shared code. So the only thing that varies between the three is how each one talks to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring AI: a fluent ChatClient
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpringAiQuestionAnsweringService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;QuestionAnsweringService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringAiQuestionAnsweringService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;chatClientBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chatClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClientBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Question&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;answerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answerText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring AI hands you a &lt;code&gt;ChatClient.Builder&lt;/code&gt;, auto-configured from your properties, and a fluent API: &lt;code&gt;prompt()&lt;/code&gt;, &lt;code&gt;user()&lt;/code&gt;, &lt;code&gt;call()&lt;/code&gt;, &lt;code&gt;content()&lt;/code&gt;. The model, its base URL and its options are all configuration, not code.&lt;/p&gt;

&lt;h2&gt;
  
  
  LangChain4j: a plain ChatModel, and the same code on two runtimes
&lt;/h2&gt;

&lt;p&gt;On Spring Boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LangChain4jQuestionAnsweringService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;QuestionAnsweringService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LangChain4jQuestionAnsweringService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Question&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LangChain4j hands you a &lt;code&gt;ChatModel&lt;/code&gt; and a single method, &lt;code&gt;chat()&lt;/code&gt;. There is no builder to call: the model is injected ready to use.&lt;/p&gt;

&lt;p&gt;Now the Quarkus version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ApplicationScoped&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LangChain4jQuestionAnsweringService&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;QuestionAnsweringService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;LangChain4jQuestionAnsweringService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="nf"&gt;askQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Question&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely. The body is identical to the Spring Boot version. The only difference is the annotation: &lt;code&gt;@ApplicationScoped&lt;/code&gt; (CDI) instead of &lt;code&gt;@Service&lt;/code&gt; (Spring). Same library, same &lt;code&gt;ChatModel&lt;/code&gt;, same &lt;code&gt;chat()&lt;/code&gt; call. The runtime changes, the code does not. That is precisely why it is worth running LangChain4j on both: it isolates what the framework costs from what the runtime costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  One test to compare them all
&lt;/h2&gt;

&lt;p&gt;Three implementations are only comparable if they are held to the same standard. Each stack's integration test extends one abstract test that talks to the running application over HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbstractQuestionAnsweringIT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt; &lt;span class="nf"&gt;baseUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Test&lt;/span&gt;
  &lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"answers a factual question"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;answersAFactualQuestion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"What is the capital of France? Answer with the city name only."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/ask"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;blankOrNullString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;containsStringIgnoringCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"paris"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ... plus: rejects a blank question (400), rejects a malformed payload (400)&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;RequestSpecification&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;baseUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. First, the suite talks HTTP with REST Assured, never an in-process API, so the same test class can hold a Spring Boot application and a Quarkus application to the same standard: a subclass supplies only its base URI. Second, the assertion is semantic, not exact-string. It checks that the answer contains "paris", case-insensitively, because LLM output is non-deterministic even at temperature zero. Asserting on a literal completion would make the test lie.&lt;/p&gt;

&lt;p&gt;The tests run against a local model in an Ollama container, started with &lt;a href="https://testcontainers.com/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt;, so there is no API key, no per-run cost, and the whole comparison is reproducible in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison reveals
&lt;/h2&gt;

&lt;p&gt;The three stacks are not the same size and do not start at the same speed. Counting only non-blank, non-comment lines of production Java:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Spring AI&lt;/th&gt;
&lt;th&gt;LangChain4j / Boot&lt;/th&gt;
&lt;th&gt;LangChain4j / Quarkus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Production Java (LOC)&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files in &lt;code&gt;src/main/java&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resolved runtime jars&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup, JVM mode&lt;/td&gt;
&lt;td&gt;1.74 s&lt;/td&gt;
&lt;td&gt;2.69 s&lt;/td&gt;
&lt;td&gt;0.66 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The startup figures are medians of a handful of runs on a single machine, with the Ollama container already up so that only application boot is inside the window: they are indicative, not benchmarks. And the jar counts do not mean Quarkus is three times heavier than LangChain4j on Boot; it simply splits its runtime into many small artifacts. A fair comparison shows that Spring AI brings 106 jars while LangChain4j brings only 49 on the &lt;em&gt;same&lt;/em&gt; runtime.&lt;br&gt;
That's because Spring AI brings a JSON-schema generator, a tokenizer and Reactor along with it, none of which this use case touches.&lt;/p&gt;

&lt;p&gt;Even hedged this way, the shape is clear: Quarkus produces the smallest module and starts fastest, Spring AI is compact in code but carries the most dependencies of the two libraries on Spring Boot (106 jars to LangChain4j's 49), and LangChain4j on Boot sits close to Spring AI, the same three files with a few extra jars for its starter.&lt;/p&gt;

&lt;p&gt;Qualitatively, the differences are small but relevant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring AI leads with a fluent, builder-based &lt;code&gt;ChatClient&lt;/code&gt;, and configuration drives the model.&lt;/li&gt;
&lt;li&gt;LangChain4j leads with a single &lt;code&gt;ChatModel.chat()&lt;/code&gt; call, and the injected model is the whole surface.&lt;/li&gt;
&lt;li&gt;Between Spring Boot and Quarkus, the LangChain4j code is identical except for the dependency-injection annotation, which tells you the runtime is the variable, not the library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a verdict. The interesting output of a comparison like this is not a winner, it is the diff, and the diff is what tells you which framework fits the way your team already thinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;This is the smallest use case. The same method, build once per stack and grade with one shared test, carries all the way up: retrieval-augmented generation, conversational memory, tool calling, the Model Context Protocol, observability, security, and agents. Each one adds a capability and shows where the three stacks converge and where they diverge.&lt;/p&gt;

&lt;p&gt;If you want the full treatment, it is the subject of my book, &lt;em&gt;Java AI Frameworks&lt;/em&gt;, available on &lt;a href="https://leanpub.com/javaaiframeworks" rel="noopener noreferrer"&gt;Leanpub&lt;/a&gt; with a free sample of the first two chapters, and on Amazon:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.com/dp/B0HR1T5S4B" rel="noopener noreferrer"&gt;&lt;img src="/assets/images/books/java-ai-frameworks.jpg" alt="Java AI Frameworks"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>springboot</category>
      <category>quarkus</category>
      <category>langchain4j</category>
    </item>
    <item>
      <title>Using Anthropic evaluation to check Claude Code plugin efficiency</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Tue, 10 Nov 2026 12:38:04 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/using-anthropic-evaluation-to-check-claude-code-plugin-efficiency-51ne</link>
      <guid>https://dev.to/nicolasduminil/using-anthropic-evaluation-to-check-claude-code-plugin-efficiency-51ne</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;A Claude Code skill is a Markdown document that the assistant loads when a task matches its description. A collection of skills, agents and hooks can be packaged as a plugin and distributed through a marketplace. Since the summer of 2026, my development environment has&lt;br&gt;
relied on such a plugin for Java work, consisting in a set of: Spring Boot, Quarkus, Apache Camel and Jakarta EE agents and skills. This collection started as a small set of artifacts but, little by little, I extended it such that, after several months, it contained 43 files and 17,338 lines.&lt;/p&gt;

&lt;p&gt;This raised a precise question: were the stack conventions provided by this plugin really contributing anything of measurable to the final outcome or could the model,by its intrinsic general knowledge, infer the same result, with much fewer tokens ?&lt;/p&gt;

&lt;p&gt;This article describes how the question was answered thanks to the Anthropic evaluation tool, and what the measurements revealed about the skills and about the evaluation method itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  A criterion for useful skill content
&lt;/h2&gt;

&lt;p&gt;A skill contributes when it changes what the model produces. Four kinds of content satisfy that criterion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Personal or team conventions, which the model has no means to infer.&lt;/li&gt;
&lt;li&gt;Facts more recent than the model's training data, such as new versions and renamed artifacts.&lt;/li&gt;
&lt;li&gt;Non-obvious pitfalls and the decisions that avoid them.&lt;/li&gt;
&lt;li&gt;Corrections to guidance that is wrong elsewhere, including in upstream plugins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Textbook material, such as the definition of an Enterprise Integration Pattern or the meaning of an HTTP status code, belongs to what the model already knows. Trying to provide this same knowledge in skills consumes between 5,000 and 8,000 tokens each time a skill loads. Additionally, the given skill might be outdated. For example, my files still referenced Spring Boot 3.2.1 months after the move to &lt;br&gt;
Spring Boot 4.&lt;/p&gt;
&lt;h2&gt;
  
  
  Restructuring before measuring
&lt;/h2&gt;

&lt;p&gt;The first step was to collect all the loose folders in &lt;code&gt;~/.claude&lt;/code&gt;, defining agents, skills and hooks, to remove the duplicates and to consolidate them in stand-alone plugins. The result is a marketplace named &lt;code&gt;simplex-software&lt;/code&gt; that lists five plugins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Activation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring-boot-template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;quarkus-template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;java-extras&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;user scope, every session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;camel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jakarta-ee&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stack plugins take precedence on every conflict. &lt;code&gt;java-extras&lt;/code&gt; holds personal rules, cross stack decisions, operational notes and a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook that reminds the model, on every prompt, which skills apply to the detected stack. Activation per project&lt;br&gt;
keeps stacks material out of the sessions that don't use them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The measurement method
&lt;/h2&gt;

&lt;p&gt;Claude Code provides &lt;code&gt;claude plugin eval&lt;/code&gt;, an open source evaluation tool allowing you to test the pertinence of your skills, agents and plugins. For example, you might want to evaluate the efficiency of a given Claude Code plugin. In order to do that you need to define "test cases", that are execution scenarios that specify an input prompt, the required context or tools, and the logic or criteria for &lt;br&gt;
determining success.&lt;/p&gt;

&lt;p&gt;A case is a directory with a prompt and a set of graders. The harness runs each case with the plugin and without it, several times per arm, and reports the score difference. A case whose score difference stays near zero identifies a plugin which doesn't contribute much to the content, as the model is already able to produce the same result without it. Such a plugin has to be ablated because its skills add only tokens and nothing else.&lt;/p&gt;

&lt;p&gt;Instead of creating new files in the project, each case asked Claude to provide the requested code and configuration directly in its answer. Therefore, they don't require a prepared sample project, write permission, etc. The graders examined the text of the answer. Each case combined three kinds of graders:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Regular expressions for the specific facts the skill claims. These cost nothing.&lt;/li&gt;
&lt;li&gt;A judge model, Claude Haiku by default with three votes, for the overall correctness.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;tool_used&lt;/code&gt; grader that checks whether Claude invoked the skill through the &lt;code&gt;Skill&lt;/code&gt;tool. It shows whether the plugin arm actually loaded the skill. It is excluded from the score, because the arm without the plugin has no skill to invoke and would fail it in every run, which would inflate the difference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A case for the Camel plugin illustrates the format. The prompt file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;---
name: quarkus-route-test
tags: [camel, quarkus]
max_turns: 8
allowed_tools: [Skill, Read, Glob, Grep]
---

Quarkus 3 project using Camel Quarkus. A RouteBuilder defines route id &lt;span class="sb"&gt;`process-route`&lt;/span&gt; from &lt;span class="sb"&gt;`direct:process`&lt;/span&gt; to &lt;span class="sb"&gt;`kafka:processed-orders`&lt;/span&gt;. Write a test that verifies one message reaches the Kafka endpoint without a real Kafka broker being used for that endpoint. Include the Maven test dependencies.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its regular expression grader, &lt;code&gt;graders/test-artifact.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;---
type: regex
pattern: "camel-quarkus-junit(?!5)"
match: contains
---

Uses camel-quarkus-junit, the successor of the deprecated camel-quarkus-junit5.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its judge grader, &lt;code&gt;graders/judge.md&lt;/code&gt;, whose body is the criterion the judge model applies to the answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;---
type: llm
---

Pass only if ALL hold:
&lt;span class="k"&gt;- &lt;/span&gt;test dependency is org.apache.camel.quarkus:camel-quarkus-junit (not camel-quarkus-junit5)
&lt;span class="k"&gt;- &lt;/span&gt;test extends CamelQuarkusTestSupport, overrides isUseAdviceWith() to return true, advises route "process-route" (mockEndpoints / mockEndpointsAndSkip) and then starts the context
&lt;span class="k"&gt;- &lt;/span&gt;does not use @MockEndpoints (not available in Camel Quarkus)
&lt;span class="k"&gt;- &lt;/span&gt;no Thread.sleep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its skill indicator, &lt;code&gt;graders/skill-fired.md&lt;/code&gt;, which passes when Claude loads a skill whose name contains &lt;code&gt;camel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;---
type: tool_used
tool: Skill
input_match: "camel"
---

Indicator only: the plugin skill was loaded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command runs every case of a plugin, three runs per arm, with a cost ceiling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin &lt;span class="nb"&gt;eval&lt;/span&gt; . -j 3 --no-publish --max-cost-usd 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First results
&lt;/h2&gt;

&lt;p&gt;Ten cases covered the three own plugins. The table reports the mean score with the plugin, without it, and the difference. Two scores are corrected for grader defects described in the next section.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;With&lt;/th&gt;
&lt;th&gt;Without&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;camel&lt;/td&gt;
&lt;td&gt;Camel Quarkus route test&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;+0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;camel&lt;/td&gt;
&lt;td&gt;Spring Boot route unit test&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;+0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;camel&lt;/td&gt;
&lt;td&gt;Spring Boot Kafka integration test&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;+0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jakarta-ee&lt;/td&gt;
&lt;td&gt;integration test on WildFly&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;+0.56&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jakarta-ee&lt;/td&gt;
&lt;td&gt;pom setup&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jakarta-ee&lt;/td&gt;
&lt;td&gt;resource, service and injection&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;java-extras&lt;/td&gt;
&lt;td&gt;CircleCI with Testcontainers&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.33&lt;/td&gt;
&lt;td&gt;+0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;java-extras&lt;/td&gt;
&lt;td&gt;graceful shutdown on Kubernetes&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;+0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;java-extras&lt;/td&gt;
&lt;td&gt;Spring Boot to Quarkus migration&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.89&lt;/td&gt;
&lt;td&gt;+0.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;java-extras&lt;/td&gt;
&lt;td&gt;Reactor to Mutiny translation&lt;/td&gt;
&lt;td&gt;0.56&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;about 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cases with a large difference show that the plugin had a consistent contribution to the final result. In the case of Camel, for example, without the plugin the model estimated the Maven artifact version as &lt;code&gt;4.18.0&lt;/code&gt;, whereas the skill states 4.23.0, built on Spring Boot 4.1.1. It also selected the test artifacts &lt;br&gt;
&lt;code&gt;camel-test-spring-junit6&lt;/code&gt; instead of &lt;code&gt;camel-quarkus-junit&lt;/code&gt; with the skill. For CircleCI it chose a plain Docker executor, which offers no Docker daemon to Testcontainers. For graceful shutdown it omitted the arithmetic that relates the &lt;code&gt;preStop&lt;/code&gt; delay, the application shutdown timeout and &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cases with a difference near zero show the opposite pattern. The Reactor to Mutiny cheat sheet, the Jakarta EE pom and the CDI constructor injection rules describe knowledge the model already applies correctly. Reading the answers confirmed it: both arms produced the same code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trimming, and the regression the evaluation caught
&lt;/h2&gt;

&lt;p&gt;The trimming pass removed the content with a difference near zero and reduced the rest to facts, decisions and pitfalls. The Reactor to Mutiny section disappeared. The Spring Boot to Quarkus table kept only the rows where a mistake is likely, the Jakarta EE setup became a short list of facts and The Camel Kafka test skeleton became one sentence.&lt;/p&gt;

&lt;p&gt;The second run, performed after the trimming, confirmed that every case with a large difference kept a score of 1.00 with the plugin. Only the migration from Spring Boot to Quarkus case declined from 1.00 to 0.78 but part of the decline came from judge noise. However, a part was genuine: the shortened table row stated that the Quarkus &lt;code&gt;@Transactional&lt;/code&gt; annotation has no &lt;code&gt;readOnly&lt;/code&gt; attribute, which determined the model to mistakenly remove the read only behavior of the original method. Without the skill, the model preserved it through a read only Hibernate session.&lt;/p&gt;

&lt;p&gt;The corrected rule prescribes that the intended behavior should be achieved via a read only &lt;code&gt;Session&lt;/code&gt; with &lt;code&gt;FlushMode.MANUAL&lt;/code&gt;, or a &lt;code&gt;org.hibernate.readOnly&lt;/code&gt; query hint. A third run of that case, judged by Claude Sonnet with a criterion that checks the read only behavior, passed six runs out of six. The episode is the strongest argument for the evaluation: a detail that appeared harmless changed the generated code, and only the measurement detected it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final state and cost
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent and skill files&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of agent and skill content&lt;/td&gt;
&lt;td&gt;17,338&lt;/td&gt;
&lt;td&gt;638&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens added to every session&lt;/td&gt;
&lt;td&gt;about 2,100&lt;/td&gt;
&lt;td&gt;about 520, plus about 150 for each optional plugin enabled in the project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Evaluation cases&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;6, each with a measured difference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The complete exercise, including the exploratory run, both full runs and the verification run, cost about 20 US dollars in model usage. A full run of the remaining suite costs about 6 dollars and takes about five minutes with three parallel runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The useful content of a skill is narrow: versions, renamed artifacts, corrections and non-obvious conventions. Explanations of well known frameworks contribute nothing measurable.&lt;/li&gt;
&lt;li&gt;An evaluation with and without the plugin is inexpensive and decisive. It replaces a debate about relevance with a number per case.&lt;/li&gt;
&lt;li&gt;Graders require the same scrutiny as the skills and a small judge model is inconsistent on long answers.&lt;/li&gt;
&lt;li&gt;A reduction of content is a change like any other and deserves a regression test. The &lt;code&gt;readOnly&lt;/code&gt; episode demonstrates it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Each arm ran three times, which indicates a direction rather than a statistically established effect. The results describe one model at one date. Here, our Camel plugin pinned the 4.23 version which a later model may know without assistance. This is precisely why the suite should run again after each model change. The cases reflect one developer's projects, and the personal rules in &lt;code&gt;house-rules&lt;/code&gt; were deliberately left out of the evaluation, since the model has no means of guessing them.&lt;/p&gt;

</description>
      <category>claude</category>
    </item>
    <item>
      <title>Time-Based One-Time Passwords with HashiCorp Vault and Quarkus</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Fri, 30 Oct 2026 16:51:22 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/time-based-one-time-passwords-with-hashicorp-vault-and-quarkus-1193</link>
      <guid>https://dev.to/nicolasduminil/time-based-one-time-passwords-with-hashicorp-vault-and-quarkus-1193</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;This article examines how the Time-Based One-Time Password algorithm (TOTP) can be delegated to the TOTP secrets engine of HashiCorp Vault and consumed by a Quarkus application. It first recalls the theoretical foundations of TOTP and the purpose of Vault. It then describes the use cases of the reference implementation with a UML diagram, analyses the Vault configuration and its bootstrap script, comments on the Java code and its integration tests, and finally presents the deployment topology defined by Docker Compose. &lt;/p&gt;

&lt;h2&gt;
  
  
  1. What is TOTP and what problem does it solve?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 The weakness of the static password
&lt;/h3&gt;

&lt;p&gt;The static password is the oldest and most widely deployed authentication mechanism in computing. It is also among the most fragile. A password is a shared secret that never changes unless its owner decides otherwise, which in practice means rarely. Once it is captured by phishing, keylogging, database leakage or simple observation over the shoulder, it remains valid for an indefinite period, and the legitimate owner usually has no indication that it has been compromised. The problem is aggravated by the well documented human tendency to reuse the same password across many services, so that the compromise of a modest forum may lead to the&lt;br&gt;
compromise of a bank account. One could say that the static password is a key that its owner copies generously and then leaves under a great number of doormats.&lt;/p&gt;

&lt;p&gt;Strictly speaking, the defect does not reside in the password as such but in the fact that it is frequently the only proof of identity required. Whoever holds the password is, from the point of view of the system, the legitimate user, and a single theft is therefore sufficient to usurp an identity.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.2 Multi-factor authentication
&lt;/h3&gt;

&lt;p&gt;The classical remedy does not consist in abolishing the password, which remains convenient,inexpensive and universally understood, but in depriving it of its exclusive status. The user is required to present several proofs belonging to distinct categories, called authentication factors, which are commonly classified as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;something the user knows, such as a password or a PIN;&lt;/li&gt;
&lt;li&gt;something the user possesses, such as a smartphone or a hardware token;&lt;/li&gt;
&lt;li&gt;something the user is, such as a fingerprint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The strength of the approach lies in the independence of these categories. The techniques that capture a password, such as phishing or the leakage of a database, do not provide the attacker with a physical device, and the theft of a device does not reveal what its owner knows.&lt;/p&gt;

&lt;p&gt;The Time-Based One-Time Password algorithm (TOTP) is a widely adopted means of implementing the second category. At the moment of  enrollment, the server generates a random secret, called the&lt;br&gt;
seed, and transmits it once to the user, generally as a QR code scanned with an authenticator application installed on a smartphone. From then on, the application and the server each derive from this seed and from the current time a short numeric code, typically of six digits, which changes every thirty seconds. To authenticate, the user submits the code currently displayed together with the password, and the server accepts it only if it matches the code computed on its side. The possession of the device holding the seed thus becomes a necessary condition for authentication, so that the knowledge of the password alone is no longer sufficient. The mathematical details of this computation are the subject of section 1.3.&lt;/p&gt;

&lt;p&gt;When a password is combined with TOTP, an attacker must consequently succeed in two attacks of a different nature against the same person:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;obtaining the password, which is usually achieved remotely and on a large scale, by means of a fraudulent login page (phishing), a keylogger installed on the workstation, or the reuse of credentials published after the leakage of another service's database (credential stuffing);&lt;/li&gt;
&lt;li&gt;obtaining the seed or the device that holds it, which requires either the physical theft of an unlocked smartphone, the installation of malware on that particular smartphone, or the interception of the QR code during the brief moment of enrollment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first attack can be conducted against millions of victims at once from the other side of the world, whereas the second must generally be directed at one specific person and one specific device. Combining them is considerably more difficult than succeeding in either one, and this difference of scale is the essence of the protection. It also explains why TOTP is preferable to codes sent by SMS, whose second factor can be diverted remotely by persuading a mobile operator to transfer the victim's number to a new SIM card (SIM swapping). The password thus ceases to be the weak point of the system and becomes merely one of its two locks. Section 1.3 mentions the notable exception of real-time phishing, in which a single fraudulent page collects both the password and a fresh code and relays them immediately.&lt;/p&gt;

&lt;p&gt;It may be observed, not without some irony, that the code produced by the device is itself a kind of password. It differs from the static one, however, in every property that made the latter fragile, since it is generated rather than chosen, changes every thirty seconds, cannot be reused and is never memorized, let alone copied under a doormat.&lt;/p&gt;
&lt;h3&gt;
  
  
  1.3 The algorithm
&lt;/h3&gt;

&lt;p&gt;TOTP is specified by RFC 6238 and is a direct extension of the HMAC-Based One-Time Password algorithm (HOTP) defined in RFC 4226. HOTP computes a short numeric code from a shared secret &lt;code&gt;K&lt;/code&gt; and a counter &lt;code&gt;C&lt;/code&gt; according to the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;compute &lt;code&gt;HMAC-SHA1(K, C)&lt;/code&gt;, which yields a 20 byte value;&lt;/li&gt;
&lt;li&gt;apply the so-called dynamic truncation, which uses the four lowest bits of the last byte as an offset in order to extract 31 bits from the HMAC value;&lt;/li&gt;
&lt;li&gt;reduce the resulting integer modulo &lt;code&gt;10^d&lt;/code&gt;, where &lt;code&gt;d&lt;/code&gt; is the number of digits, usually 6.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The difficulty of HOTP lies in the synchronization of the counter between the client and the server. TOTP removes this difficulty in an elegant manner by replacing the counter with a value that both parties already share without any communication, namely the current time. The counter becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T = floor((CurrentUnixTime - T0) / X)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;T0&lt;/code&gt; is the origin of time (usually the Unix epoch) and &lt;code&gt;X&lt;/code&gt; is the time step, 30 seconds&lt;br&gt;
by default. The one-time password is then simply &lt;code&gt;HOTP(K, T)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The consequences of this construction are noteworthy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;each code is valid only during a short time window, so that an intercepted code rapidly loses any value for an attacker;&lt;/li&gt;
&lt;li&gt;the device that generates the codes needs no network connection, since it only needs the seed and a reasonably accurate clock;&lt;/li&gt;
&lt;li&gt;the seed is transmitted only once, at enrolment time, usually through a QR code encoding an &lt;code&gt;otpauth://&lt;/code&gt; URI;&lt;/li&gt;
&lt;li&gt;the server must tolerate a small clock drift, which is achieved by also accepting the codes of the adjacent time steps (the so-called skew).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;TOTP does not eliminate every threat. A real-time phishing proxy may still relay a fresh code within its validity window, and the seed itself must be protected with the utmost care on the server side. This last point is precisely the reason for introducing Vault into the picture.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What is HashiCorp Vault and what is it used for?
&lt;/h2&gt;

&lt;p&gt;HashiCorp Vault is a secrets management system. Its purpose is to store, generate, distribute and revoke sensitive material such as passwords, API keys, certificates and encryption keys, so that this material does not end up scattered across configuration files, environment variables and source code repositories. Every access to Vault is authenticated by a token or by an authentication method, is authorised by policies expressed in the HashiCorp Configuration Language (HCL), and may be recorded by an audit device.&lt;/p&gt;

&lt;p&gt;Vault organizes its functionalities in pluggable components called secrets engines, each of them mounted at a path of a virtual file system. Among the most frequently used engines are the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the key/value engine, which stores arbitrary static secrets;&lt;/li&gt;
&lt;li&gt;the database engine, which generates short-lived database credentials on demand;&lt;/li&gt;
&lt;li&gt;the PKI engine, which acts as a certification authority;&lt;/li&gt;
&lt;li&gt;the transit engine, which offers encryption as a service;&lt;/li&gt;
&lt;li&gt;the TOTP engine, which is the subject of this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The TOTP engine can play two roles. In the role of a generator, Vault creates the seed, returns a QR code that the user scans with an authenticator application, and later validates the codes submitted by the user. In the role of a provider, Vault receives an existing seed and generates codes on behalf of a client. The present project uses the first role. Its essential benefit is that the seed never leaves Vault after enrollment, and that the application never stores it,&lt;br&gt;
never sees it again and therefore cannot leak it. An application that knows nothing can reveal nothing, which is a principle of security that is far older than computing and has been practised with great success by generations of discreet butlers.&lt;/p&gt;

&lt;p&gt;A security-minded reader will notice an apparent paradox. The seed is protected by Vault, but Vault itself requires a secret to be opened. This is solved by the seal mechanism. At startup, the Vault storage is encrypted and Vault is said to be sealed. It must be unsealed by providing a number of key shares produced by Shamir's secret sharing algorithm before it can serve any request. Section 4 shows how this procedure is automated, in a deliberately simplified form,for local development.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Use cases
&lt;/h2&gt;

&lt;p&gt;The following diagram presents the actors of the system and the use cases in which they participate, together with their pre-conditions and post-conditions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ifz7f0n39u4ydlrhnrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ifz7f0n39u4ydlrhnrh.png" alt=" " width="647" height="1544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The PlantUML source of this diagram is available in &lt;code&gt;diagrams/use-case.puml&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The operator intervenes only once, by starting the stack, after which the bootstrap is fully automated. The end user performs the enrollment and then the authentication, whereas the authenticator application and Vault participate as secondary actors. It is worth noting that the authenticator application and Vault never communicate with each other. They are like two correspondents who agreed on a secret at their only meeting and who, from that day on, prove their acquaintance merely by consulting their watches.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Vault configuration
&lt;/h2&gt;

&lt;p&gt;The Vault configuration consists of three files located in the &lt;code&gt;vault/config&lt;/code&gt; directory, which is mounted read-only in the containers.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.1 The server configuration in &lt;code&gt;vault.hcl&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="s2"&gt;"file"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/vault/file"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;address&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0:8200"&lt;/span&gt;
  &lt;span class="nx"&gt;tls_disable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;api_addr&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://0.0.0.0:8200"&lt;/span&gt;
&lt;span class="nx"&gt;disable_mlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;ui&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;storage&lt;/code&gt; stanza selects the file storage back-end, which persists the encrypted data of Vault in the &lt;code&gt;/vault/file&lt;/code&gt; directory. This directory is backed by the Docker volume &lt;code&gt;vault-file&lt;/code&gt;, so that the registered TOTP keys survive the restarts of the container. This&lt;br&gt;
choice is deliberate. Vault also offers a development mode, as does the Quarkus Dev Services mechanism, but both are kept entirely in memory and forget everything when stopped. Such amnesia is acceptable in a test but rather inconvenient for a user who has just scanned a QR code and who would discover at the next restart that the application has no recollection of the enrollment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;listener&lt;/code&gt; stanza opens a TCP listener on all network interfaces on port 8200. The &lt;code&gt;tls_disable&lt;/code&gt; directive switches off TLS, which simplifies local development and which would be an unforgivable sin in any other context.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;api_addr&lt;/code&gt; parameter defines the address advertised by the server to its clients, in particular for request forwarding in a cluster. With a single node its role is modest.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;disable_mlock&lt;/code&gt; parameter prevents Vault from calling the &lt;code&gt;mlock&lt;/code&gt; system call, whose purpose is to forbid the operating system to swap memory pages containing secrets to disk. Since containers generally lack the &lt;code&gt;IPC_LOCK&lt;/code&gt; capability, this protection is disabled here. In a production deployment it should be kept enabled and the capability should be granted.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;ui = true&lt;/code&gt; activates the web interface, available at &lt;code&gt;http://localhost:8200/ui&lt;/code&gt;, which is convenient for inspecting the state of the server.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.2 The access policy in &lt;code&gt;totp-policy.hcl&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="s2"&gt;"totp/*"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"delete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"list"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Vault applies a deny by default model. A token can perform only the operations explicitly granted by the policies attached to it. The &lt;code&gt;totp-policy&lt;/code&gt; policy grants the application the capabilities &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt; on every path below &lt;code&gt;totp/&lt;/code&gt;, and nothing else. The application can therefore create keys (&lt;code&gt;totp/keys/&amp;lt;name&amp;gt;&lt;/code&gt;), generate codes and validate them (&lt;code&gt;totp/code/&amp;lt;name&amp;gt;&lt;/code&gt;), but it can neither read the key/value secrets of other&lt;br&gt;
teams nor alter the configuration of Vault. This is a direct application of the principle of least privilege. A more restrictive policy is conceivable, for example by removing &lt;code&gt;delete&lt;/code&gt; and &lt;code&gt;list&lt;/code&gt; if the application never needs them, and the reader is invited to apply such pruning in a real deployment.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.3 The bootstrap script &lt;code&gt;init.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The script is executed by the one-shot container &lt;code&gt;vault-init&lt;/code&gt;. Its main property is idempotence, in other words it can be executed any number of times and always leads to the same final state. It proceeds in six stages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waiting for the server.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;until &lt;/span&gt;vault status 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'Initialized|Sealed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; clause of Docker Compose only guarantees that the &lt;code&gt;vault&lt;/code&gt; container has been started, not that the Vault process is ready to answer. The script therefore polls the &lt;code&gt;vault status&lt;/code&gt; command until its output contains the expected fields. One may observe that the command returns a non-zero exit code when Vault is sealed, which is why the script examines the output rather than the exit code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialisation.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /init/init.json &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;vault operator init &lt;span class="nt"&gt;-key-shares&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-key-threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /init/init.json
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initialization creates the master encryption key, splits it into key shares and produces the initial root token. It is performed only once, when the file &lt;code&gt;/init/init.json&lt;/code&gt; does not exist or is empty. In production, the master key is typically divided into five shares with a threshold of three, entrusted to different persons who are expected never to travel on the same aircraft. Here, one share and a threshold of one are sufficient, and the result is stored in clear in the &lt;code&gt;vault-init&lt;/code&gt; volume. The script comment explicitly warns that this arrangement is acceptable only for local development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unsealing.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;UNSEAL_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.unseal_keys_b64[0]'&lt;/span&gt; /init/init.json&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ROOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.root_token'&lt;/span&gt; /init/init.json&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;vault status &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.sealed == true'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;vault operator unseal &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UNSEAL_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the file storage, Vault is sealed again after each restart. The script extracts the unseal key and the root token with &lt;code&gt;jq&lt;/code&gt;, which is installed on the fly by the container entry point, and unseals the server only if it is actually sealed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enabling the TOTP engine.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VAULT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; vault secrets list &lt;span class="nt"&gt;-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'has("totp/")'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;vault secrets &lt;span class="nb"&gt;enable &lt;/span&gt;totp
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authenticated with the root token, the script mounts the TOTP secrets engine at its default path &lt;code&gt;totp/&lt;/code&gt;, unless it is already mounted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Writing the policy.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault policy write totp-policy /vault/config/totp-policy.hcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command is naturally idempotent, since writing an identical policy twice leaves it unchanged, and it also guarantees that any modification of the HCL file is taken into account at the next start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating the application token.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; vault token lookup dev-app-token &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;vault token create &lt;span class="nt"&gt;-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dev-app-token"&lt;/span&gt; &lt;span class="nt"&gt;-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"totp-policy"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-orphan&lt;/span&gt; &lt;span class="nt"&gt;-no-default-policy&lt;/span&gt; &lt;span class="nt"&gt;-period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"8760h"&lt;/span&gt; &lt;span class="nt"&gt;-display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vault-auth"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This last stage deserves a closer look, since each option carries a precise meaning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;-id="dev-app-token"&lt;/code&gt; imposes a known token value, so that the application can be configured statically, which is convenient for development and strongly discouraged elsewhere;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-policy="totp-policy"&lt;/code&gt; attaches the least-privilege policy described above;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-no-default-policy&lt;/code&gt; removes the &lt;code&gt;default&lt;/code&gt; policy, which would otherwise grant a few additional capabilities, such as looking up the token itself;&lt;/li&gt;
&lt;li&gt; &lt;code&gt;-orphan&lt;/code&gt; detaches the token from its parent, the root token, so that a revocation of the latter does not revoke the application token in cascade;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-period="8760h"&lt;/code&gt; makes the token periodic, which means that it has no maximum lifetime and remains valid as long as it is renewed at least once a year;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-display-name="vault-auth"&lt;/code&gt; gives the token a readable name in the audit logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application thus receives a token that can do exactly what it needs and nothing more,&lt;br&gt;
which is the ideal relationship between a software component and its privileges, and which&lt;br&gt;
is, regrettably, far less frequent in practice than in textbooks.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The application code
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;vault-auth&lt;/code&gt; module is a Quarkus 3.37 application built with Quarkus REST and the Quarkiverse &lt;code&gt;quarkus-vault&lt;/code&gt; extension. Its configuration, in &lt;code&gt;application.properties&lt;/code&gt;, is minimal, as shown below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;quarkus.vault.devservices.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;
&lt;span class="py"&gt;quarkus.vault.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8200&lt;/span&gt;
&lt;span class="py"&gt;quarkus.vault.authentication.client-token&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;dev-app-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dev Services are disabled for the reason given in section 4.1. The URL and the token designate the Vault server started by Docker Compose and are overridden by the environment variables &lt;code&gt;QUARKUS_VAULT_URL&lt;/code&gt; and &lt;code&gt;QUARKUS_VAULT_AUTHENTICATION_CLIENT_TOKEN&lt;/code&gt; when the application runs in its own container.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 The service &lt;code&gt;TotpService&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ApplicationScoped&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TotpService&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;VaultTOTPSecretEngine&lt;/span&gt; &lt;span class="n"&gt;vaultTOTPSecretEngine&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;TotpService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VaultTOTPSecretEngine&lt;/span&gt; &lt;span class="n"&gt;vaultTOTPSecretEngine&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vaultTOTPSecretEngine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vaultTOTPSecretEngine&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;KeyDefinition&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTotpKeyDefinition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CreateKeyParameters&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateKeyParameters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"totp-test"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setExported&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vaultTOTPSecretEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;validateCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vaultTOTPSecretEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validateCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class is an application scoped CDI bean, which means that a single instance is shared by all requests. Its only dependency, &lt;code&gt;VaultTOTPSecretEngine&lt;/code&gt;, is supplied by the &lt;code&gt;quarkus-vault&lt;/code&gt; extension and is injected through the constructor, which makes the dependency explicit and the class easy to instantiate in isolation.&lt;/p&gt;

&lt;p&gt;The method &lt;code&gt;getTotpKeyDefinition()&lt;/code&gt; creates a TOTP key in Vault. The two-argument constructor of &lt;code&gt;CreateKeyParameters&lt;/code&gt; sets the issuer (&lt;code&gt;totp-test&lt;/code&gt;), which the authenticator application displays as the name of the service, and the account name, here the user name. This constructor also sets the &lt;code&gt;generate&lt;/code&gt; flag to &lt;code&gt;true&lt;/code&gt;, which places Vault in its generator role. Vault therefore creates the seed itself instead of expecting one from the caller. The call to &lt;code&gt;setExported(true)&lt;/code&gt; asks Vault to return the provisioning material, namely the &lt;code&gt;otpauth://&lt;/code&gt; URL and the QR code as a Base64 encoded PNG image. The key is stored under the path &lt;code&gt;totp/keys/&amp;lt;username&amp;gt;&lt;/code&gt;, so that each user owns a distinct key, and a new registration under the same name replaces the previous key.&lt;/p&gt;

&lt;p&gt;The period, the algorithm and the number of digits are deliberately left to their default values,namely 30 seconds, SHA1 and 6 digits. This apparent lack of ambition is the result of experience. During the development of the project, the period was initially set to one minute.&lt;br&gt;
The QR code correctly announced a period of 60 seconds, the authenticator application accepted it without any objection, and every single code was then rejected. The explanation is that Google Authenticator ignores the &lt;code&gt;period&lt;/code&gt;, &lt;code&gt;algorithm&lt;/code&gt; and &lt;code&gt;digits&lt;/code&gt; parameters of the URI and always uses the standard values. The application thus faithfully produced codes for a clock that ticked twice as often as the one of Vault. The comment in the source code preserves this lesson for posterity, and the author recommends it to any reader tempted by originality in this domain.&lt;/p&gt;

&lt;p&gt;The method &lt;code&gt;validateCode()&lt;/code&gt; delegates the verification to Vault, which recomputes the expected code for the current time step and its neighbors and compares it with the submitted one. Vault also records the codes already used and rejects a second submission of the same code within its validity window, which protects the application against replay attacks at no cost. The method returns a simple boolean, and the application never manipulates the seed.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.2 The resource &lt;code&gt;TotpApi&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/totp"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TotpApi&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@Inject&lt;/span&gt;
  &lt;span class="nc"&gt;TotpService&lt;/span&gt; &lt;span class="n"&gt;totpService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@GET&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt; &lt;span class="nf"&gt;testTotp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@HeaderParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-User"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                           &lt;span class="nd"&gt;@HeaderParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-TOTP-Code"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;totpService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validateCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Access granted"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
      &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UNAUTHORIZED&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Invalid TOTP code"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@GET&lt;/span&gt;
  &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/register/{username}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nd"&gt;@Produces&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TEXT_HTML&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;totpService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotpKeyDefinition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"""
        &amp;lt;!DOCTYPE html&amp;gt;
        &amp;lt;html&amp;gt;
            &amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Scan QR Code&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
            &amp;lt;body&amp;gt;
                &amp;lt;h2&amp;gt;Scan this QR code with Google Authenticator&amp;lt;/h2&amp;gt;
                &amp;lt;img src="data:image/png;base64,%s" alt="TOTP QR Code"/&amp;gt;
            &amp;lt;/body&amp;gt;
        &amp;lt;/html&amp;gt;
        """&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBarcode&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to generate TOTP key"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The class is a Jakarta REST resource bound to the path &lt;code&gt;/totp&lt;/code&gt; and exposes two endpoints.&lt;/p&gt;

&lt;p&gt;The endpoint &lt;code&gt;GET /totp/register/{username}&lt;/code&gt; performs the enrollment. It obtains the key definition from the service and, if it is present, inserts the Base64 encoded image returned by &lt;code&gt;getBarcode()&lt;/code&gt; into a small HTML page by means of a Java text block and the &lt;code&gt;formatted()&lt;/code&gt;&lt;br&gt;
method. The image is embedded as a &lt;code&gt;data:&lt;/code&gt; URI, so that the browser can display it without any additional request. If the key definition is absent, a short failure message is returned.&lt;/p&gt;

&lt;p&gt;The endpoint &lt;code&gt;GET /totp&lt;/code&gt; performs the authentication. The user name and the code are read from the custom headers &lt;code&gt;X-User&lt;/code&gt; and &lt;code&gt;X-TOTP-Code&lt;/code&gt;. A valid code produces the status 200 and the body &lt;code&gt;Access granted&lt;/code&gt;, whereas an invalid one produces the status 401 and the body&lt;br&gt;
&lt;code&gt;Invalid TOTP code&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reader should keep in mind that this resource is a demonstration and not an authentication service ready for production. The registration endpoint is not protected, so that anybody who knows a user name may replace the key of that user, and the absence of a header is not handled specifically. In a real system, enrollment would occur only after a first authentication, for example by password, and the successful validation of the code would lead to the issuance of a session or of a token, typically through the Quarkus OIDC or JWT extensions. These additions are left as an exercise, according to the venerable academic tradition which consists in calling "exercise" what the author did not have the time to write.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.3 The integration tests
&lt;/h3&gt;

&lt;p&gt;In accordance with the conventions of the project, the tests are integration tests executed against a real Vault server rather than unit tests based on mocks. Two classes are involved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;VaultTestResource&lt;/code&gt;&lt;/strong&gt; implements the &lt;code&gt;QuarkusTestResourceLifecycleManager&lt;/code&gt; interface, which allows Quarkus to start an external resource before the application and to stop it afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VaultContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="no"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withVaultToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ROOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withInitCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secrets enable totp"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"quarkus.vault.devservices.enabled"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"quarkus.vault.url"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHttpHostAddress&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"quarkus.vault.authentication.client-token"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ROOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method starts a Testcontainers &lt;code&gt;VaultContainer&lt;/code&gt; based on the image &lt;code&gt;hashicorp/vault:1.18&lt;/code&gt;, which is the same version as in Docker Compose. The method &lt;code&gt;asCompatibleSubstituteFor("vault")&lt;/code&gt;informs Testcontainers that the official HashiCorp image may be used in place of the historical &lt;code&gt;vault&lt;/code&gt; image it expects. The container runs in Vault development mode with the root token &lt;code&gt;root&lt;/code&gt;, and the init command mounts the TOTP engine. The returned map overrides the application configuration, so that the application under test connects to this ephemeral server on its randomly mapped port instead of the persistent one. Here, the ephemeral nature that was a defect&lt;br&gt;
in section 4.1 becomes a virtue, since each test run starts from a perfectly clean state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TotpApiTest&lt;/code&gt;&lt;/strong&gt; is annotated with &lt;code&gt;@QuarkusTest&lt;/code&gt;, which starts the complete application, and with &lt;code&gt;@WithTestResource(VaultTestResource.class)&lt;/code&gt;, which attaches the resource described above. It contains three test cases written with REST Assured:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;registerReturnsQrCode()&lt;/code&gt; is a positive test which calls &lt;code&gt;/totp/register/alice&lt;/code&gt; and verifies that the response has the status 200 and contains an embedded PNG image;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;validCodeGrantsAccess()&lt;/code&gt; is a positive test which registers &lt;code&gt;bob&lt;/code&gt;, then asks Vault itself to generate the current code by means of &lt;code&gt;generateCode("bob")&lt;/code&gt;, and verifies that submitting this code yields the status 200 and the message &lt;code&gt;Access granted&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;invalidCodeIsRejected()&lt;/code&gt; is a negative test which registers &lt;code&gt;carol&lt;/code&gt;, submits the code &lt;code&gt;000000&lt;/code&gt; and verifies the status 401 and the message &lt;code&gt;Invalid TOTP code&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second test is particularly instructive. Since no smartphone could reasonably be attached to the build server, the test injects &lt;code&gt;VaultTOTPSecretEngine&lt;/code&gt; and lets Vault play the role of the authenticator application. This is possible because the key was created in generator mode, in which Vault is able to compute the current code for its own keys. The third test relies on the fact that &lt;code&gt;000000&lt;/code&gt; is a valid code with a probability of one in a million for any given time step. Should this test ever fail, the author recommends not correcting it but rather buying a lottery ticket on the same day.&lt;/p&gt;

&lt;p&gt;The tests are executed by &lt;code&gt;mvn -pl vault-auth -am test&lt;/code&gt; on any workstation with a running Docker daemon, and by the CircleCI pipeline, which uses a &lt;code&gt;machine&lt;/code&gt; executor precisely so that&lt;br&gt;
Testcontainers has access to a Docker engine.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Deployment
&lt;/h2&gt;

&lt;p&gt;The file &lt;code&gt;docker-compose.yml&lt;/code&gt; defines three services, two named volumes and one bind mount. The following diagram represents this topology.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy4n1bxscrfplxn17on5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdy4n1bxscrfplxn17on5.png" alt=" " width="739" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The PlantUML source of this diagram is available in &lt;code&gt;diagrams/deployment.puml&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The service &lt;code&gt;vault&lt;/code&gt; runs the Vault server with the configuration of section 4.1. It publishes port 8200 on the host, stores its encrypted data in the volume &lt;code&gt;vault-file&lt;/code&gt; and is restarted automatically unless it is explicitly stopped.&lt;/p&gt;

&lt;p&gt;The service &lt;code&gt;vault-init&lt;/code&gt; uses the same image, runs as root in order to be able to install &lt;code&gt;jq&lt;/code&gt; and to write in its volume, executes the script &lt;code&gt;init.sh&lt;/code&gt; and then terminates. Its restart policy is &lt;code&gt;no&lt;/code&gt;, which is the expected behavior of a one-shot task. Its volume &lt;code&gt;vault-init&lt;/code&gt; keeps the file &lt;code&gt;init.json&lt;/code&gt; between executions, which is what makes the automatic unsealing possible after a restart.&lt;/p&gt;

&lt;p&gt;The service &lt;code&gt;vault-auth&lt;/code&gt; runs the Quarkus application, whose image is produced by the Quarkus container image extension during the Maven build. The &lt;code&gt;pull_policy: never&lt;/code&gt; clause indicates that the image must exist locally and must not be searched for in a registry. The application starts only once &lt;code&gt;vault-init&lt;/code&gt; has completed successfully, which guarantees that Vault is unsealed, that the TOTP engine is mounted and that the token &lt;code&gt;dev-app-token&lt;/code&gt; exists at the moment of the first request. Inside the Compose network, the application reaches Vault by its service name, &lt;code&gt;vault&lt;/code&gt;, through the environment variable &lt;code&gt;QUARKUS_VAULT_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The whole stack is started by the following commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn &lt;span class="nt"&gt;-pl&lt;/span&gt; vault-auth &lt;span class="nt"&gt;-am&lt;/span&gt; package &lt;span class="nt"&gt;-Dquarkus&lt;/span&gt;.container-image.build
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the user opens &lt;code&gt;http://localhost:8080/totp/register/&amp;lt;username&amp;gt;&lt;/code&gt;, scans the QR code and checks a code with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-User: &amp;lt;username&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-TOTP-Code: &amp;lt;code&amp;gt;"&lt;/span&gt; http://localhost:8080/totp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command &lt;code&gt;docker compose down -v&lt;/code&gt; removes the volumes and returns the system to its original state of blissful ignorance.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;This project shows that the implementation of a second authentication factor based on TOTP requires remarkably little application code once the cryptographic responsibilities are delegated to Vault. The Quarkus application contains two short classes and never stores a seed. Vault generates the keys, produces the QR codes, validates the codes and prevents their reuse, while the policy and the periodic orphan token restrict the application to the strict minimum. The integration tests, executed against a real Vault container, give a high degree of confidence in this collaboration without resorting to mocks.&lt;/p&gt;

&lt;p&gt;The simplifications adopted for local development, namely the single unseal share stored in clear, the disabled TLS, the fixed token value and the unprotected registration endpoint, must of course be removed before any production use. With this reservation, the approach offers a sound basis on which an authentication service may be built, and the author hopes that the reader, having reached this conclusion without falling asleep, will consider this a valid proof of presence, comparable in rigor to a six digit code.&lt;/p&gt;

</description>
      <category>quarkus</category>
      <category>java</category>
      <category>vault</category>
    </item>
    <item>
      <title>Distributed Transactions in Enterprise Applications: From X/Open to MicroProfile LRA</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:25:11 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/distributed-transactions-in-enterprise-applications-from-xopen-to-microprofile-lra-4mj5</link>
      <guid>https://dev.to/nicolasduminil/distributed-transactions-in-enterprise-applications-from-xopen-to-microprofile-lra-4mj5</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;The problem of making several independent systems agree on a single outcome is&lt;br&gt;
older than most of the technology used to solve it. Historically, one of the &lt;br&gt;
first protocols able to provide this synchronization was 2PC (&lt;em&gt;two-phase commit&lt;/em&gt;)&lt;br&gt;
that the application servers era made an ordinary feature of enterprise development.&lt;br&gt;
Later lightweight frameworks, service-oriented and microservices architectures&lt;br&gt;
have made the same guarantee considerably harder to obtain.&lt;br&gt;&lt;br&gt;
This article follows that problem from its formalization in the X/Open Distributed&lt;br&gt;
Transaction Processing to the present situation. It then examines two working&lt;br&gt;
implementations of an identical use case, one using XA and two-phase commit, the&lt;br&gt;
other using the saga pattern, as specified by MicroProfile LRA, and compares them&lt;br&gt;
on evidence rather than on preference. The accompanying source code is available&lt;br&gt;
in the &lt;a href="https://github.com/nicolasduminil/quarkus-distributed-tx" rel="noopener noreferrer"&gt;&lt;code&gt;quarkus-distributed-tx&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
repository, where both implementations expose the same REST contract and are&lt;br&gt;
exercised by the same test suite.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Origins: X/Open, and the Formalization of 2PC
&lt;/h2&gt;

&lt;p&gt;The 2PC protocol is older than the standard that made it famous. Its&lt;br&gt;
essential shape appears in the database literature of the late 1970s, notably in&lt;br&gt;
the work of Jim Gray on the recovery and consistency properties of database&lt;br&gt;
operating systems. The problem it addresses is easy to state and difficult to&lt;br&gt;
solve: several parties must all either perform or decline an action. And they&lt;br&gt;
must do that despite the possibility that any of them, including the party &lt;br&gt;
conducting the negotiation, may fail at any moment.&lt;/p&gt;

&lt;p&gt;As its name states, the protocol proceeds in two phases. In the first, the&lt;br&gt;
coordinator asks every participant whether it is able to commit. A participant&lt;br&gt;
that answers affirmatively cannot retract itself. It must have&lt;br&gt;
written enough durable state to guarantee that it can still commit even after &lt;br&gt;
a crash. In the second phase, the coordinator informs the participants of the &lt;br&gt;
collective decision, which is affirmative only if every vote was affirmative.&lt;/p&gt;

&lt;p&gt;In early '90s, the X/Open consortium decided to contribute by formalizing a reference &lt;br&gt;
model named DTP (&lt;em&gt;Distributed Transaction Processing&lt;/em&gt;). This model identified&lt;br&gt;
three roles namely the application program, the transaction manager and the &lt;br&gt;
resource manager. The following two interfaces have been defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the TX (&lt;em&gt;Transaction Demarcation&lt;/em&gt;) interface governs how an application demarcates a transaction;&lt;/li&gt;
&lt;li&gt;the XA (&lt;em&gt;eXtended Architecture&lt;/em&gt;) interface defines how the transaction manager drives the resource manager through the phases of the commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the XA specifications existed, a transaction manager written by one&lt;br&gt;
vendor could coordinate a database written by another, and the industry acquired&lt;br&gt;
a genuinely portable mechanism for distributed atomicity.&lt;/p&gt;

&lt;p&gt;Let's note a property of the XA specifications that will become important&lt;br&gt;
later. Between the moment a participant votes to commit and the moment it learns&lt;br&gt;
the collective decision, it is &lt;em&gt;blocked&lt;/em&gt;. It holds its locks, it cannot proceed,&lt;br&gt;
and it cannot unilaterally decide. If the coordinator fails during this interval,&lt;br&gt;
the participant waits. Skeen and others demonstrated in the early '80s that this&lt;br&gt;
blocking behavior is not an artifact of a careless design but a logical consequence&lt;br&gt;
of the quorum-based model which tolerates partitions only by blocking whenever no&lt;br&gt;
quorum can be assembled. In 1985, Fischer, Lynch and Paterson published the article&lt;br&gt;
&lt;em&gt;Impossibility of Distributed Consensus with One Faulty Process&lt;/em&gt; which establishes&lt;br&gt;
that deterministic consensus cannot be guaranteed in an asynchronous system in &lt;br&gt;
which even one process may fail.&lt;/p&gt;

&lt;p&gt;Because of this vulnerability 2PC was considered in the beginning a flawed protocol&lt;br&gt;
that needed improvements. It took some time and experience to understand that, &lt;br&gt;
on the opposite, it's an exact trade-off between perfect atomicity and availability.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Several Decades of Service
&lt;/h2&gt;

&lt;p&gt;It would be reasonable to expect that a protocol with a documented blocking&lt;br&gt;
failure mode and a known vulnerability would have been superseded. It has not &lt;br&gt;
been. 2PC remains the most widely deployed mechanism for distributed atomicity &lt;br&gt;
in enterprise systems, and it is likely to remain so for some time.&lt;/p&gt;

&lt;p&gt;The explanation is that the failure mode is rare and the guarantee is absolute.&lt;br&gt;
Within a single data center, coordinating two databases and a message broker&lt;br&gt;
across a reliable local network, with a transaction manager whose recovery log is&lt;br&gt;
on durable storage, the vulnerable interval is measured in milliseconds and the&lt;br&gt;
recovery procedure is automatic. Financial institutions, telecommunications&lt;br&gt;
operators and government systems adopted the protocol because the alternative was&lt;br&gt;
to write compensation logic by hand for every business operation. Additionally,&lt;br&gt;
software architects and developers are more confident in a protocol than in a &lt;br&gt;
promise that reconciliation will probably catch any discrepancies.&lt;/p&gt;

&lt;p&gt;Another important point to notice is that 2PC places complexity in the &lt;br&gt;
infrastructure field, written once, by specialists, and consumed through a single&lt;br&gt;
annotation. Every other alternative examined in this article moves that work into&lt;br&gt;
application code, where it must be written again for each operation, by&lt;br&gt;
developers whose principal expertise is the business domain rather than&lt;br&gt;
distributed systems. This is a real cost and it is frequently underestimated.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. OTS, JTS and JTA
&lt;/h2&gt;

&lt;p&gt;The X/Open model assumed a particular shape: an application, a transaction&lt;br&gt;
manager and some resource managers, generally within reach of one another. What&lt;br&gt;
it did not address is the case that the OO (&lt;em&gt;object-oriented&lt;/em&gt;) paradigm made &lt;br&gt;
central the concept of transactions spanning objects that reside in different &lt;br&gt;
processes, on different machines, supplied by different vendors, and invoked &lt;br&gt;
remotely as though they were local.&lt;/p&gt;

&lt;p&gt;The OMG (&lt;em&gt;Object Management Group&lt;/em&gt;), the consortium behind CORBA, answered this in the&lt;br&gt;
mid-1990s with the OTS (&lt;em&gt;Object Transaction Service&lt;/em&gt;), adopted as one of the&lt;br&gt;
CORBAservices. OTS described the roles of a distributed transaction in CORBA's&lt;br&gt;
IDL (&lt;em&gt;Interface Definition Language&lt;/em&gt;). The protocol underneath had not changed,&lt;br&gt;
the same resources, participants and quorum, in Gray's sense, were at work and &lt;br&gt;
no new algorithms have been proposed. OTS only specified how the 2PC participants&lt;br&gt;
might be distributed objects. And it settled the question that X/Open had left&lt;br&gt;
thin, namely how two transaction managers, from different vendors, should&lt;br&gt;
interoperate.&lt;/p&gt;

&lt;p&gt;OTS didn't displace XA but complemented it. The boundary to the resources, like &lt;br&gt;
databases or message brokers, remained what it had been, OTS typically only fronted&lt;br&gt;
resource managers driven over XA. Anything that OTS added sat above that &lt;br&gt;
boundary, between the managers themselves.&lt;/p&gt;

&lt;p&gt;The Jakarta EE platform, formerly Java EE and even formerly J2EE, absorbed this&lt;br&gt;
model with considerable thoroughness. The JTA (&lt;em&gt;Java Transaction API&lt;/em&gt;) specifications&lt;br&gt;
defined the interface through which applications and application servers demarcate &lt;br&gt;
transactions. JTS (&lt;em&gt;Java Transaction Service&lt;/em&gt;) brought OTS into Java. It required&lt;br&gt;
a transaction manager to implement that specification in its Java-mapped form, &lt;br&gt;
to propagate transaction context between managers as OTS prescribed, and to &lt;br&gt;
present JTA to the layer above. JTS was therefore not an alternative design but&lt;br&gt;
OTS in Java, with a JTA façade.&lt;/p&gt;

&lt;p&gt;The distinction between JTA and JTS is worth mentioning as the two are habitually&lt;br&gt;
confused, usually as "JTA is the interface and JTS the implementation". That is&lt;br&gt;
not quite it, since both are specifications. JTA is what an application programmer&lt;br&gt;
sees: &lt;code&gt;UserTransaction&lt;/code&gt;, &lt;code&gt;TransactionManager&lt;/code&gt;, and the interfaces by which resources&lt;br&gt;
are enlisted. JTS specifies what provides the guarantee underneath, including the&lt;br&gt;
propagation of transaction context between processes and the interoperability of&lt;br&gt;
transaction managers from different vendors.&lt;/p&gt;

&lt;p&gt;For the application developer, the effect was remarkable and is easily forgotten&lt;br&gt;
now. A method annotated for container-managed transactions would, upon invoking a&lt;br&gt;
remote component, carry its transaction context to that component, enlist the&lt;br&gt;
remote resources, and commit or roll back the entire assembly atomically. The&lt;br&gt;
programmer wrote business logic. The container arranged consensus. This was one&lt;br&gt;
of the genuinely impressive achievements of the enterprise Java period which &lt;br&gt;
worked reliably across products from competing vendors and it is&lt;br&gt;
somewhat unfair that it is now remembered as an old labyrinthine system.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. CORBA and IIOP as the Transport of Context
&lt;/h2&gt;

&lt;p&gt;The transaction context propagation just described required a mechanism, and the&lt;br&gt;
mechanism was CORBA. Transaction context traveled between application servers &lt;br&gt;
as part of the IIOP (&lt;em&gt;Internet Inter-ORB Protocol&lt;/em&gt;), in the form of a service &lt;br&gt;
context attached to the remote invocation. OTS defined how that context was&lt;br&gt;
represented and how transaction managers should interoperate. IIOP carried it.&lt;/p&gt;

&lt;p&gt;This arrangement was possible because &lt;strong&gt;the transport understood transactions&lt;/strong&gt;.&lt;br&gt;
A remote call was not merely a message but an invocation within a transactional&lt;br&gt;
context, and the infrastructure at both ends recognized it as such. Enlistment &lt;br&gt;
of the remote resource was automatic because the protocol carried the information&lt;br&gt;
required to perform it.&lt;/p&gt;

&lt;p&gt;The costs were substantial. CORBA was complex, its interoperability was achieved&lt;br&gt;
by considerable effort and its programming model demanded interface definitions&lt;br&gt;
and generated stubs. As the industry moved towards HTTP and simpler representations,&lt;br&gt;
these costs became decisive. But it is important to record that the price of the&lt;br&gt;
simplicity was HTTP lack of capacity to carry transactional semantics.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The Decline of Heavyweight Servers and the Rise of Lighter Frameworks
&lt;/h2&gt;

&lt;p&gt;The reasons for the decline of the traditional application server are well&lt;br&gt;
documented and only tangentially concerned with transactions. Deployment units&lt;br&gt;
grew large and start-up times grew long at exactly the moment when elastic&lt;br&gt;
infrastructure began to reward the opposite. Container images preferred a single&lt;br&gt;
process to a shared runtime hosting many applications. The operational model of&lt;br&gt;
the application server, in which several applications share a managed container&lt;br&gt;
administered by a specialist team, was displaced by one in which each service&lt;br&gt;
carries its own runtime and is administered by the team that wrote it.&lt;/p&gt;

&lt;p&gt;Spring Boot, released in 2014, demonstrated that an embedded server and&lt;br&gt;
convention-based configuration could deliver most of the productivity of the&lt;br&gt;
enterprise stack with a fraction of its ceremony. Quarkus, which followed in 2019,&lt;br&gt;
went further by moving substantial work from run time to build time, producing&lt;br&gt;
start-up characteristics that make Java a plausible choice for serverless and&lt;br&gt;
elastically scaled deployments.&lt;/p&gt;

&lt;p&gt;Jakarta EE itself, transferred to the Eclipse Foundation in 2017 and subsequently&lt;br&gt;
renamed, continues to evolve, and both Spring Boot and Quarkus rely on&lt;br&gt;
substantial parts of its specifications. The change is therefore less a&lt;br&gt;
repudiation of the enterprise Java model than a redistribution of it. The&lt;br&gt;
specifications survived, the monolithic container that implemented them did not.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. What the Lighter Frameworks Lost
&lt;/h2&gt;

&lt;p&gt;Two changes occurred simultaneously. The first is architectural. The microservice&lt;br&gt;
architecture style holds that each service should own its data, and that no other&lt;br&gt;
service should reach into that data directly. This principle is incompatible with&lt;br&gt;
2PC across services which requires the coordinator to be connected to&lt;br&gt;
the resource managers. A coordinator that must not touch another service's&lt;br&gt;
resources cannot enlist them.&lt;/p&gt;

&lt;p&gt;The second is a matter of transport. HTTP does not propagate transaction context.&lt;br&gt;
It doesn't provide an equivalent of the IIOP service context. A REST call is an&lt;br&gt;
independent interaction whose relationship to the caller's transaction is not &lt;br&gt;
represented anywhere, unless the application arranges to represent it.&lt;/p&gt;

&lt;p&gt;The consequence, which is regularly misunderstood in practice, is that&lt;br&gt;
annotating a method as transactional and then invoking a remote service from&lt;br&gt;
within it produces no distributed transaction of any kind. &lt;br&gt;
If the local transaction rolls back, the remote effect stands. If&lt;br&gt;
the local transaction commits, nothing establishes that the remote one did, and&lt;br&gt;
a remote call that returned no answer may have applied its change or may not,&lt;br&gt;
leaving the caller no basis on which to decide. The divergence does not even&lt;br&gt;
require the remote service to fail. The arrangement has the&lt;br&gt;
appearance of a distributed transaction and none of the substance, which is the&lt;br&gt;
least desirable of the available outcomes, since it fails while presenting the&lt;br&gt;
appearance of success.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. The Saga Pattern, and the Ease of Implementing It Incorrectly
&lt;/h2&gt;

&lt;p&gt;The saga was described by Garcia-Molina and Salem in 1987, in a paper concerned&lt;br&gt;
with long-lived transactions within a single database. Their observation was that&lt;br&gt;
a transaction holding locks for a long period is destructive to concurrency, and&lt;br&gt;
that certain long transactions may be decomposed into a sequence of shorter ones,&lt;br&gt;
each of which commits independently, with a compensating transaction defined for&lt;br&gt;
each step. If the sequence cannot be completed, the compensations for the&lt;br&gt;
completed steps are executed in reverse order.&lt;/p&gt;

&lt;p&gt;The saga doesn't provide isolation. This is not a defect of a particular&lt;br&gt;
implementation but by definition. Intermediate states are visible to other&lt;br&gt;
observers, because each step has genuinely committed. In exchange, no participant&lt;br&gt;
holds a lock while waiting for a distant coordinator, and each service retains&lt;br&gt;
authority over its own data.&lt;/p&gt;

&lt;p&gt;The naive implementation, which is very common, executes each step and defines its&lt;br&gt;
compensation as the arithmetic inverse. Let's consider the example of a money transfer&lt;br&gt;
service, like the one presented in the associated project. A debit is compensated&lt;br&gt;
by a credit of the same amount. This is wrong in two distinct ways.&lt;/p&gt;

&lt;p&gt;The first defect is the visible intermediate state. Between the debit and its&lt;br&gt;
compensation, the balance is genuinely reduced. Any concurrent observer, including&lt;br&gt;
another saga evaluating whether sufficient funds exist, will read that reduced&lt;br&gt;
value and may act upon information that is about to be invalidated.&lt;/p&gt;

&lt;p&gt;The second defect is that &lt;strong&gt;the compensating action can itself fail&lt;/strong&gt;. A credit is&lt;br&gt;
an operation like any other, subject to validation, to constraints, and to the&lt;br&gt;
account having been closed in the interval. A saga whose compensation fails is in&lt;br&gt;
a worse condition than one that never began, since it has performed part of an&lt;br&gt;
operation and it cannot undo it. The system requires human intervention, and the&lt;br&gt;
humans require an explanation.&lt;/p&gt;

&lt;p&gt;The remedy, illustrated in Section 10, is to arrange matters so that no&lt;br&gt;
compensating write is required. If the first step reserves rather than acts, then&lt;br&gt;
compensation releases the reservation, and a release cannot fail for business&lt;br&gt;
reasons because it removes an obligation rather than creating one.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. MicroProfile LRA
&lt;/h2&gt;

&lt;p&gt;MicroProfile LRA, where LRA denotes &lt;em&gt;Long Running Action&lt;/em&gt;, specifies a saga&lt;br&gt;
coordination protocol for services communicating over HTTP. It is best understood&lt;br&gt;
as an answer to the two negative effects identified in Section 6: it reintroduces a context&lt;br&gt;
that travels with the call, and it reintroduces a coordinator that drives&lt;br&gt;
participants to a common outcome. It does not, and cannot, reintroduce atomicity.&lt;/p&gt;

&lt;p&gt;The programming model is declarative. A method annotated &lt;code&gt;@LRA&lt;/code&gt; participates in a&lt;br&gt;
long-running action, beginning one if required. Context propagates in the&lt;br&gt;
&lt;code&gt;Long-Running-Action&lt;/code&gt; HTTP header, which the implementation attaches to outgoing&lt;br&gt;
calls and interprets on incoming ones. A participant declares its obligations&lt;br&gt;
through further annotations: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Compensate&lt;/code&gt;, which is invoked if the action is cancelled;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Complete&lt;/code&gt;, invoked if it is closed successfully;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Status&lt;/code&gt;, by which the coordinator may enquire what a participant did; &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Forget&lt;/code&gt;, by which a participant may discard state it no longer needs to retain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two properties of this protocol are commonly overlooked. The first is that participants&lt;br&gt;
must be &lt;strong&gt;idempotent&lt;/strong&gt;. The coordinator retries. A participant that applies its&lt;br&gt;
effect a second time corrupts its own data. This is the requirement that appears&lt;br&gt;
in every specification document and is absent from a remarkable proportion of &lt;br&gt;
first implementations.&lt;/p&gt;

&lt;p&gt;The second is that the coordinator is a &lt;strong&gt;separate process that calls the&lt;br&gt;
participants back&lt;/strong&gt;. As opposed to JTA where the transaction manager is embedded&lt;br&gt;
in the application, under LRA it is not and the participant must reach the coordinator&lt;br&gt;
to begin, join and end an action. The coordinator must reach every participant&lt;br&gt;
to invoke its callbacks. It is entirely possible to construct a deployment in &lt;br&gt;
which the first direction works as expected while the second does not, and the resulting &lt;br&gt;
behavior is discussed in Section 10.&lt;/p&gt;


&lt;h2&gt;
  
  
  9. The XA Implementation in Detail
&lt;/h2&gt;

&lt;p&gt;The use case is deliberately modest: transfer an amount from one account to&lt;br&gt;
another and record the transfer. Two databases are involved, one holding accounts&lt;br&gt;
and one holding the transfer log, because a single database would reduce the&lt;br&gt;
problem to a local transaction and demonstrate nothing.&lt;/p&gt;

&lt;p&gt;One component in this arrangement is essential: the Narayana transaction manager.&lt;br&gt;
Its lineage is running back to the Arjuna research at the University of Newcastle&lt;br&gt;
and provided two decades of good service as the transaction manager of JBoss and&lt;br&gt;
WildFly. Its Quarkus extension, used here, brings a JTA implementation embedded&lt;br&gt;
in the application's own process. It owns the object store, decides the outcome&lt;br&gt;
and executes the 2PC protocol on the application's behalf. The same project supplies&lt;br&gt;
the LRA coordinator that section 10 relies upon, so both implementations in this&lt;br&gt;
article rely on the same body of code.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Each datasource is declared as an XA one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;quarkus.datasource.accounts.jdbc.transactions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xa&lt;/span&gt;
&lt;span class="py"&gt;quarkus.datasource.xfers.jdbc.transactions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flag is the entire mechanism. The connection pool supplies &lt;code&gt;XAConnection&lt;/code&gt;&lt;br&gt;
instances rather than ordinary ones, and each pool enlists an &lt;code&gt;XAResource&lt;/code&gt; in the&lt;br&gt;
active transaction upon first use.&lt;/p&gt;
&lt;h3&gt;
  
  
  The transactional method
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TransferDto&lt;/span&gt; &lt;span class="nf"&gt;processTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransferRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSourceAccountId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSourceAccountId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDestinationAccountId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDestinationAccountId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBalance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InsufficientFundsException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="nc"&gt;Transfer&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRandomId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrency&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;TransferStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPLETED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;transferRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toDto&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is no reference to two-phase commit in this code, and that is the&lt;br&gt;
observation worth recording. The annotation causes a JTA transaction to begin. The&lt;br&gt;
two repositories address two different datasources. Each datasource enlists itself.&lt;br&gt;
When the method returns, Narayana performs the protocol.&lt;/p&gt;
&lt;h3&gt;
  
  
  The protocol as executed
&lt;/h3&gt;

&lt;p&gt;Upon commit the transaction manager calls &lt;code&gt;prepare&lt;/code&gt; on both branches. Each&lt;br&gt;
PostgreSQL instance executes &lt;code&gt;PREPARE TRANSACTION&lt;/code&gt;, writing the changes durably in&lt;br&gt;
a state from which they can still either be committed or rolled back. If both &lt;br&gt;
branches vote affirmatively, the transaction manager writes its decision to &lt;br&gt;
its object store. This is the point at which the outcome becomes irrevocable&lt;br&gt;
and then the transaction manager calls &lt;code&gt;commit&lt;/code&gt; on both branches, each of which&lt;br&gt;
executes &lt;code&gt;COMMIT PREPARED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two consequences of operational importance follow. PostgreSQL refuses prepared&lt;br&gt;
transactions unless &lt;code&gt;max_prepared_transactions&lt;/code&gt; is configured with a positive&lt;br&gt;
value, its default being zero, so an otherwise correct application fails at the&lt;br&gt;
first commit. And the transaction manager's object store must be durable, since it&lt;br&gt;
holds the only record of a decision made but not yet applied. Recovery must be&lt;br&gt;
enabled so that transactions left in doubt by a failure are resolved when the&lt;br&gt;
process returns.&lt;/p&gt;
&lt;h3&gt;
  
  
  The architectural cost
&lt;/h3&gt;

&lt;p&gt;The transfer service interacts directly with the &lt;code&gt;accounts&lt;/code&gt; database. It does not&lt;br&gt;
ask anything of the account service, it holds the resource manager instead. This&lt;br&gt;
means that it owns a connection to the &lt;code&gt;accounts&lt;/code&gt; database and enlists it in its&lt;br&gt;
own transaction. The transfer service opens that connection, modifies the account&lt;br&gt;
rows itself through its own persistence layer, and the &lt;code&gt;XAResource&lt;/code&gt; behind that&lt;br&gt;
connection becomes a branch of the transaction that the transfer service began.&lt;br&gt;
The account service plays no part. Its code does not run and it is not informed.&lt;br&gt;
The commit decision belongs to the transfer service's transaction manager.&lt;/p&gt;

&lt;p&gt;This is not an oversight but a requirement. &lt;code&gt;prepare&lt;/code&gt; and &lt;code&gt;commit&lt;/code&gt; are operations&lt;br&gt;
on an &lt;code&gt;XAResource&lt;/code&gt;, and a service boundary exposes no equivalent of them. HTTP&lt;br&gt;
offers no way to ask a remote service to make a change durable while leaving its&lt;br&gt;
outcome open. A coordinator that cannot reach the resource manager cannot enlist&lt;br&gt;
it. The consequence is that the two services need to share a database, and the&lt;br&gt;
independence that the service boundary was intended to establish is not present.&lt;br&gt;
The repository's &lt;code&gt;docker-compose.yaml&lt;/code&gt; states this fact in one line: &lt;code&gt;transfer&lt;/code&gt; is&lt;br&gt;
given its own database, as well as the accounts datasource URL, whereas &lt;br&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; is given, in addition to its own database URL, the one of the &lt;br&gt;
account service.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. The LRA Implementation in Detail
&lt;/h2&gt;

&lt;p&gt;The saga implementation exposes an identical REST contract and reaches balances&lt;br&gt;
only through the account service's HTTP interface. It has no dependency on the&lt;br&gt;
accounts persistence layer, and that absence is the entire architectural point.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reserve, then confirm
&lt;/h3&gt;

&lt;p&gt;In the LRA implementation and as opposed to the 2PC one, the account service does&lt;br&gt;
not move money when asked to reserve a debit. It records a &lt;code&gt;Reservation&lt;/code&gt; in state&lt;br&gt;
&lt;code&gt;PENDING&lt;/code&gt; and leaves the balance unchanged. Available funds are computed as the&lt;br&gt;
balance minus the sum of pending debits, so another possible concurrent saga &lt;br&gt;
cannot reserve the same money. Upon &lt;code&gt;@Complete&lt;/code&gt; the reservations are applied; &lt;br&gt;
upon &lt;code&gt;@Compensate&lt;/code&gt; they are released.&lt;/p&gt;

&lt;p&gt;Accordingly, &lt;strong&gt;because the balance never moved, compensation performs no update&lt;br&gt;
and, therefore, no persistence operation can fail during compensation.&lt;/strong&gt; Hence,&lt;br&gt;
the second issue identified in Section 7 is eliminated by construction rather &lt;br&gt;
than by coding.&lt;/p&gt;
&lt;h3&gt;
  
  
  Idempotency, enforced twice
&lt;/h3&gt;

&lt;p&gt;Reservations are identified, in business terms, by a triple consisting in the action identifier,&lt;br&gt;
the account and the reservation type. Notice that the triple is not the entity's primary key, &lt;br&gt;
which is a generated value, so nothing in the object model prevents a second row carrying the&lt;br&gt;
same triple. And in order to avoid that, it is declared as a unique constraint in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RESERVATIONS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;uniqueConstraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@UniqueConstraint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"UK_RESERVATION_LRA"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;columnNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"LRA_ID"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ACCOUNT_ID"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"RESERVATION_TYPE"&lt;/span&gt;&lt;span class="o"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint is needed because the account service, when asked to reserve a&lt;br&gt;
debit, looks for an existing reservation before writing one, which is a&lt;br&gt;
check-then-act sequence and, therefore, not atomic. Two deliveries of the same&lt;br&gt;
reserve request can both find nothing and both insert. The constraint avoids that&lt;br&gt;
and guarantees that the database refuses the 2nd reserve.&lt;/p&gt;

&lt;p&gt;Retries could also cause the effect described above. The coordinator waits for&lt;br&gt;
each callback, but a call that times out or whose reply is lost leaves the&lt;br&gt;
action unresolved, so it keeps calling again and again. A settlement instruction&lt;br&gt;
therefore might arrive more than once for a reservation that is already recorded. The&lt;br&gt;
database constraint cannot help here, since no row is being inserted. Then the guard&lt;br&gt;
sits one level down, in the service class shown below. As you can see, the two &lt;br&gt;
callbacks annotated with &lt;code&gt;@Compensate&lt;/code&gt; and &lt;code&gt;@Complete&lt;/code&gt; annotations obtain their&lt;br&gt;
reservations from a single helper, the &lt;code&gt;pendingFor(...)&lt;/code&gt; method, that selects&lt;br&gt;
only the &lt;code&gt;PENDING&lt;/code&gt; reservations, so that the ones already confirmed or already&lt;br&gt;
released are passed over rather than settled again. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@ApplicationScoped
public class AccountLraService
{
  ...
  @Transactional
  public ParticipantStatus complete(String lraId)
  {
    pendingFor(lraId).forEach(this::confirm);
    return ParticipantStatus.Completed;
  }
  ...
  @Transactional
  public ParticipantStatus compensate(String lraId)
  {
    pendingFor(lraId).forEach(reservation -&amp;gt; reservation.setState(ReservationState.RELEASED));
    return ParticipantStatus.Compensated;
  }
  ...
  private Stream&amp;lt;Reservation&amp;gt; pendingFor(String lraId)
  {
    return reservations.findByLra(lraId).stream()
     .filter(reservation -&amp;gt; reservation.getState() == ReservationState.PENDING);
  }
  ...
}  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  11. The Project
&lt;/h2&gt;

&lt;p&gt;The project that illustrates this article is a Quarkus workshop, available at&lt;br&gt;
&lt;a href="https://github.com/nicolasduminil/quarkus-distributed-tx" rel="noopener noreferrer"&gt;&lt;code&gt;quarkus-distributed-tx&lt;/code&gt;&lt;/a&gt;,&lt;br&gt;
showing a simple money transfer use case in the two ways that we discussed along&lt;br&gt;
the article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XA / 2PC&lt;/strong&gt; (&lt;code&gt;transfer&lt;/code&gt;, port 8080): a genuine 2PC across two XA
datasources, coordinated by Narayana.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saga / MicroProfile LRA&lt;/strong&gt; (&lt;code&gt;transfer-lra&lt;/code&gt;, port 8090 and &lt;code&gt;account&lt;/code&gt;, port 8081): a compensation-based
consistency example across autonomous services, coordinated by a Narayana LRA coordinator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both expose the same &lt;code&gt;/xfers&lt;/code&gt; REST contract and are driven by the same shared&lt;br&gt;
test suite (&lt;code&gt;transfer-tck&lt;/code&gt;), so any difference you observe is a difference in&lt;br&gt;
transaction strategy and nothing else.&lt;/p&gt;
&lt;h3&gt;
  
  
  Modules
&lt;/h3&gt;

&lt;p&gt;The following modules are part of the workshop project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;common&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared library: &lt;code&gt;Common&lt;/code&gt; utility, the transfer DTOs (&lt;code&gt;TransferDto&lt;/code&gt;, &lt;code&gt;TransferRequest&lt;/code&gt;, &lt;code&gt;TransferResponse&lt;/code&gt;, &lt;code&gt;TransferStatus&lt;/code&gt;), the application exception hierarchy and the single &lt;code&gt;ApplicationExceptionMapper&lt;/code&gt; that maps them to HTTP status codes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account-repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared persistence layer for accounts: the &lt;code&gt;Account&lt;/code&gt; entity, &lt;code&gt;AccountDto&lt;/code&gt;, the &lt;code&gt;AccountRepository&lt;/code&gt; interface and its two implementations. Depended on by &lt;code&gt;account&lt;/code&gt; and by &lt;code&gt;transfer&lt;/code&gt; — and pointedly &lt;strong&gt;not&lt;/strong&gt; by &lt;code&gt;transfer-lra&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-tck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The REST contract both transfer implementations must satisfy, as an abstract test class. Test tooling is at compile scope so consumers depend on it with &lt;code&gt;&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account microservice (port 8081). Owns the &lt;code&gt;accounts&lt;/code&gt; schema. CRUD + debit/credit, &lt;strong&gt;plus&lt;/strong&gt; the LRA participant endpoints under &lt;code&gt;/lra/accounts&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XA coordinator (port 8080). Two XA datasources (&lt;code&gt;accounts&lt;/code&gt;, &lt;code&gt;xfers&lt;/code&gt;). Transfer CRUD plus the 2PC transfer operation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-lra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saga orchestrator (port 8090). One plain datasource for its own transfer log; reaches balances only through the account service's API.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each repository has two implementations: an in-memory one (&lt;code&gt;DefaultAccountRepository&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;DefaultTransferRepository&lt;/code&gt;) and a JPA/Panache one (&lt;code&gt;AccountRepositoryXA&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;TransferRepositoryXA&lt;/code&gt;). Both are &lt;code&gt;@Alternative&lt;/code&gt;, so neither is enabled by default;&lt;br&gt;
each application picks the XA pair explicitly through&lt;br&gt;
&lt;code&gt;quarkus.arc.selected-alternatives&lt;/code&gt;. Only the XA implementations participate in 2PC —&lt;br&gt;
the in-memory ones are there to run the services without a database.&lt;/p&gt;

&lt;p&gt;All four modules carry the Jandex plugin so their &lt;code&gt;@Entity&lt;/code&gt;, &lt;code&gt;@Provider&lt;/code&gt; and CDI bean&lt;br&gt;
classes are indexed when consumed as external dependencies.&lt;/p&gt;
&lt;h3&gt;
  
  
  Endpoints
&lt;/h3&gt;

&lt;p&gt;Three services expose an HTTP API, each with a Swagger UI at &lt;code&gt;/q/swagger-ui&lt;/code&gt;. The&lt;br&gt;
rows marked as callbacks are invoked by the LRA coordinator, not by clients.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create an account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}/debit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debit, amount in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}/credit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credit, amount in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/debit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reserve a debit, &lt;code&gt;@LRA(MANDATORY)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/credit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reserve a credit, &lt;code&gt;@LRA(MANDATORY)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/availability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance, held and available amounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Complete&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/compensate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Compensate&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Status&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/forget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Forget&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transfer, in one 2PC transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the transfer log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transfer, as a saga, &lt;code&gt;@LRA(REQUIRED)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the transfer log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Complete&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/compensate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Compensate&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Status&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/forget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Forget&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things are worth reading off the table. The two transfer services present the&lt;br&gt;
same &lt;code&gt;POST /xfers&lt;/code&gt; contract, taking the same request body of&lt;br&gt;
&lt;code&gt;{sourceAccountId, destinationAccountId, amount, currency}&lt;/code&gt;, which is what allows&lt;br&gt;
a single test class to exercise both. And the account service's participant API&lt;br&gt;
sits under its own path prefix instead of altering the existing endpoints, which&lt;br&gt;
is why the saga work left the original account tests untouched.&lt;/p&gt;
&lt;h3&gt;
  
  
  Build &amp;amp; test
&lt;/h3&gt;

&lt;p&gt;Integration tests use Quarkus Dev Services (Testcontainers), so a running Docker&lt;br&gt;
daemon is required. No manual database setup is needed. Each service seeds its own&lt;br&gt;
data in dev/test profiles, on the behalf of the &lt;code&gt;import.sql&lt;/code&gt; file for &lt;code&gt;account&lt;/code&gt; and of the&lt;br&gt;
&lt;code&gt;import-accounts.sql&lt;/code&gt; for &lt;code&gt;transfer&lt;/code&gt;. In the &lt;code&gt;prod&lt;/code&gt; profile the account service&lt;br&gt;
owns the &lt;code&gt;accounts&lt;/code&gt; schema and no generation or seeding happens.&lt;/p&gt;

&lt;p&gt;The following command runs the integration tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn clean verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the list of the integration tests provided with the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AbstractMoneyTransferContractIT&lt;/code&gt; (transfer-tck): this is an abstract class serving as a superclass for all the integration tests. It defines the shared scenarios: happy
path, insufficient funds, unknown account, listing
and deletion. Assertions are on deltas, and the balance/count probes are
abstract, because the XA service can read the databases directly while the
saga service can only get them by calling the API over HTTP. Failure paths assert the response body
as well as the status. Both stacks must return the business error rather than
transaction plumbing, which a status-only assertion would not catch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MoneyTransferIT&lt;/code&gt; (transfer): the contract against XA, with repository-based probes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MoneyTransferSagaIT&lt;/code&gt; (transfer-lra): the contract against LRA, with HTTP probes,
plus saga-only assertions about the &lt;code&gt;COMPENSATED&lt;/code&gt; audit row and released reservations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AccountResourceIT&lt;/code&gt; (account): account CRUD, debit/credit, 404 on unknown account,
and rejection of overdrafts and invalid amounts. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Run the full stack
&lt;/h3&gt;

&lt;p&gt;In order to run the full stack, production like, proceed as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the container images (container-image.build=true is enabled)&lt;/span&gt;
mvn clean package
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose.yaml&lt;/code&gt; starts the whole comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accounts-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;Accounts, &lt;code&gt;max_prepared_transactions=100&lt;/code&gt; for XA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xfers-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5433&lt;/td&gt;
&lt;td&gt;XA transfer log, &lt;code&gt;max_prepared_transactions=100&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xfers-lra-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5434&lt;/td&gt;
&lt;td&gt;Saga transfer log (no XA needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lra-coordinator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;quay.io/jbosstm/lra-coordinator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8081&lt;/td&gt;
&lt;td&gt;CRUD + LRA participant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;td&gt;XA/2PC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-lra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8090&lt;/td&gt;
&lt;td&gt;Saga/LRA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both transfer services run against the same accounts, so you can drive the same&lt;br&gt;
transfer through each and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"sourceAccountId":"ACC1","destinationAccountId":"ACC2","amount":10.00,"currency":"EUR"}'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8080/xfers  &lt;span class="c"&gt;# XA&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8090/xfers  &lt;span class="c"&gt;# Saga&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8081/lra/accounts/ACC1/availability                                &lt;span class="c"&gt;# reservations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;QUARKUS_LRA_BASE_URI&lt;/code&gt; on the two participants: it is the address the&lt;br&gt;
coordinator uses to call them back, and without it a participant advertises a URL&lt;br&gt;
that is only meaningful from its own side of the network.&lt;/p&gt;

&lt;p&gt;A Swagger interface is also available with each API, as explained at §11 Endpoints.&lt;br&gt;
Additionally, the smoke test script &lt;code&gt;test-all.sh&lt;/code&gt; is a very convenient way to make&lt;br&gt;
sure to cover all the test case possible combinations. Here below is an example&lt;br&gt;
of how to run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;./test-all.sh

&lt;span class="o"&gt;===&lt;/span&gt; Account API - http://localhost:8081/accounts &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC90, 1000.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC91, 0.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC92, 10.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS GET /accounts/ACC90 -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 1000.00
PASS GET /accounts &lt;span class="o"&gt;(&lt;/span&gt;list&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS PUT /accounts/ACC90 &lt;span class="o"&gt;(&lt;/span&gt;rename owner&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /accounts/ACC90 &lt;span class="o"&gt;(&lt;/span&gt;confirm rename&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS customerName updated
PASS POST /accounts/ACC90/credit &lt;span class="o"&gt;(&lt;/span&gt;50.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 1050.00
PASS POST /accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;200.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00

&lt;span class="o"&gt;===&lt;/span&gt; Account API - negative cases &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST credit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;.00&lt;span class="o"&gt;)&lt;/span&gt; rejected -&amp;gt; 400
PASS POST debit &lt;span class="o"&gt;(&lt;/span&gt;0.00&lt;span class="o"&gt;)&lt;/span&gt; rejected -&amp;gt; 400
PASS POST debit &lt;span class="o"&gt;(&lt;/span&gt;999999.00&lt;span class="o"&gt;)&lt;/span&gt; insufficient funds -&amp;gt; 400
PASS GET /accounts/NOSUCH -&amp;gt; 404
PASS POST credit on unknown account -&amp;gt; 404
PASS POST credit without Content-Type -&amp;gt; 415
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00
PASS DELETE /accounts/ACC92 -&amp;gt; 204
PASS GET deleted account -&amp;gt; 404
PASS DELETE already-deleted account -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - http://localhost:8081/lra/accounts &lt;span class="o"&gt;===&lt;/span&gt;
PASS GET /lra/accounts/ACC90/availability -&amp;gt; 200
PASS availability exposes balance/held/available
PASS POST /lra/accounts/ACC90/debit without LRA context -&amp;gt; 412
PASS POST /lra/accounts/ACC90/credit without LRA context -&amp;gt; 412
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00

&lt;span class="o"&gt;===&lt;/span&gt; XA Transfer API &lt;span class="o"&gt;(&lt;/span&gt;2PC&lt;span class="o"&gt;)&lt;/span&gt; - http://localhost:8080/xfers &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers &lt;span class="o"&gt;(&lt;/span&gt;ACC90 -&amp;gt; ACC91, 100.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS transferId returned: raizmjf
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 750.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 100.00
PASS GET /xfers/raizmjf -&amp;gt; 200
PASS GET /xfers &lt;span class="o"&gt;(&lt;/span&gt;list&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS PUT /xfers/raizmjf &lt;span class="o"&gt;(&lt;/span&gt;currency -&amp;gt; USD&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200

&lt;span class="o"&gt;===&lt;/span&gt; XA Transfer API - negative cases &lt;span class="o"&gt;(&lt;/span&gt;nothing must move&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers insufficient funds -&amp;gt; 400
PASS POST /xfers negative amount -&amp;gt; 400
PASS POST /xfers unknown &lt;span class="nb"&gt;source&lt;/span&gt; -&amp;gt; 404
PASS POST /xfers unknown destination -&amp;gt; 404
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 750.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 100.00
PASS GET /xfers/NOSUCH -&amp;gt; 404
PASS DELETE /xfers/raizmjf -&amp;gt; 204
PASS GET deleted transfer -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Saga Transfer API &lt;span class="o"&gt;(&lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; - http://localhost:8090/xfers &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers &lt;span class="o"&gt;(&lt;/span&gt;ACC90 -&amp;gt; ACC91, 25.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS transferId returned: etmlebm
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 125.00
PASS GET /xfers/etmlebm -&amp;gt; 200
PASS GET /xfers &lt;span class="o"&gt;(&lt;/span&gt;list, includes compensated&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS @Complete flipped the transfer to COMPLETED

&lt;span class="o"&gt;===&lt;/span&gt; Saga Transfer API - cancellation and compensation &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers insufficient funds &lt;span class="o"&gt;(&lt;/span&gt;saga cancels&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 400
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 125.00
PASS @Compensate logged the cancelled transfer as COMPENSATED
PASS POST /xfers unknown destination &lt;span class="o"&gt;(&lt;/span&gt;saga cancels&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 404
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS GET availability after settled sagas -&amp;gt; 200
PASS held &lt;span class="o"&gt;==&lt;/span&gt; 0 &lt;span class="o"&gt;(&lt;/span&gt;no reservation left dangling&lt;span class="o"&gt;)&lt;/span&gt;
PASS PUT /xfers/etmlebm &lt;span class="o"&gt;(&lt;/span&gt;currency -&amp;gt; USD&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /xfers/NOSUCH -&amp;gt; 404
PASS DELETE /xfers/etmlebm -&amp;gt; 204
PASS GET deleted transfer -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Saga participant callbacks - @Status and @Forget &lt;span class="o"&gt;(&lt;/span&gt;transfer-lra&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1ed
PASS POST /xfers with an externally started LRA -&amp;gt; 412
PASS GET /xfers/lra/status &lt;span class="o"&gt;(&lt;/span&gt;@Status&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS @Status reports Compensated &lt;span class="k"&gt;for &lt;/span&gt;the cancelled LRA
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 725.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS DELETE /xfers/lra/forget &lt;span class="o"&gt;(&lt;/span&gt;@Forget&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /xfers/lra/status after @Forget -&amp;gt; 404
PASS @Forget kept the transfer, only dropped its LRA &lt;span class="nb"&gt;id&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - driving a saga by hand &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1f2
PASS POST /lra/accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;40.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 725.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 40.00 &lt;span class="o"&gt;(&lt;/span&gt;now 40.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS POST /lra/accounts/ACC91/credit &lt;span class="o"&gt;(&lt;/span&gt;40.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 125.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS coordinator closed the LRA -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by &lt;span class="nt"&gt;-40&lt;/span&gt;.00 &lt;span class="o"&gt;(&lt;/span&gt;now 685.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; moved by 40.00 &lt;span class="o"&gt;(&lt;/span&gt;now 165.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 0&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - cancelling a saga by hand &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1f7
PASS POST /lra/accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;15.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 15.00 &lt;span class="o"&gt;(&lt;/span&gt;now 15.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS coordinator cancelled the LRA -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 685.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 0&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Summary &lt;span class="o"&gt;===&lt;/span&gt;
87 passed, 0 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  12. Comparison
&lt;/h2&gt;

&lt;p&gt;The attached GitHub repository contains a multi-module Maven project with two&lt;br&gt;
implementations. The&lt;br&gt;
first uses JTA and XA to obtain a two-phase commit, and is contained entirely in&lt;br&gt;
the &lt;code&gt;transfer&lt;/code&gt; module. The second uses MicroProfile LRA, and is necessarily&lt;br&gt;
spread across two modules: &lt;code&gt;transfer-lra&lt;/code&gt; orchestrates the saga, while &lt;code&gt;account&lt;/code&gt;&lt;br&gt;
takes part in it. That difference in shape is not an accident of packaging. A&lt;br&gt;
JTA coordinator that enlists resource managers directly can live in a single &lt;br&gt;
module, the &lt;code&gt;transfer&lt;/code&gt; one, whereas a saga cannot exist unless its participants&lt;br&gt;
are reachable and callable, hence the necessity for it to live in two modules,&lt;br&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; and &lt;code&gt;account&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both implementations satisfy the same test contract. The following differences&lt;br&gt;
are therefore differences of mechanism and of consequence, not of capability.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;XA and two-phase commit&lt;/th&gt;
&lt;th&gt;Saga and LRA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coordinator&lt;/td&gt;
&lt;td&gt;Embedded in the application process&lt;/td&gt;
&lt;td&gt;Separate process, must call participants back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data ownership&lt;/td&gt;
&lt;td&gt;Coordinator holds the resource managers, so services share a database&lt;/td&gt;
&lt;td&gt;Each service retains its own data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;Complete; no intermediate state is observable&lt;/td&gt;
&lt;td&gt;Absent by design; a reservation is observable while in flight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;td&gt;Rollback leaves no trace whatsoever&lt;/td&gt;
&lt;td&gt;Leaves a record that the operation was attempted and undone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locks&lt;/td&gt;
&lt;td&gt;Held between prepare and commit&lt;/td&gt;
&lt;td&gt;None held across the action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Participants block if the coordinator fails while in doubt&lt;/td&gt;
&lt;td&gt;Participants remain available; completion may occur later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location of difficulty&lt;/td&gt;
&lt;td&gt;Infrastructure, written once&lt;/td&gt;
&lt;td&gt;Application code, written for each operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure of the undo&lt;/td&gt;
&lt;td&gt;Not applicable; nothing was committed&lt;/td&gt;
&lt;td&gt;Possible, unless reservations are used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two entries deserve comment.&lt;/p&gt;

&lt;p&gt;The row concerning failure is frequently presented as an advantage of 2PC on the&lt;br&gt;
grounds that the rollbacks are clean. It may equally be regarded as a loss of &lt;br&gt;
information. A cancelled saga records that a transfer was attempted and&lt;br&gt;
subsequently undone, which is an audit trail that a rollback cannot produce&lt;br&gt;
because the rows were never committed. Whether this constitutes an advantage&lt;br&gt;
depends entirely on whether the business is required to account for attempts as&lt;br&gt;
well as for outcomes, and this is a question for the domain rather than for the&lt;br&gt;
architecture.&lt;/p&gt;

&lt;p&gt;The row concerning the location of difficulty is, in my opinion, the&lt;br&gt;
one most often omitted from comparisons. 2PC is difficult once, in&lt;br&gt;
infrastructure maintained by specialists. Sagas are difficult repeatedly, in&lt;br&gt;
application code and this argument is important.&lt;/p&gt;

&lt;p&gt;The correct conclusion is not that one approach supersedes the other. It is that&lt;br&gt;
the choice is determined by whether the resource managers can be co-located under&lt;br&gt;
a single coordinator. If they can, 2PC provides a stronger guarantee&lt;br&gt;
for less application code. If they cannot, because the services are genuinely&lt;br&gt;
independent or the resources do not support XA, then a saga is not the inferior&lt;br&gt;
option but the only option, and the engineering effort should be directed at&lt;br&gt;
implementing it correctly rather than at regretting the atomicity that was never&lt;br&gt;
available.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. The Present and the Future
&lt;/h2&gt;

&lt;p&gt;Two-phase commit is not disappearing. It remains appropriate wherever resources&lt;br&gt;
can be co-located, and a significant proportion of enterprise data continues to&lt;br&gt;
reside in exactly such arrangements. Reports of its obsolescence generally&lt;br&gt;
originate from contexts in which it was never applicable.&lt;/p&gt;

&lt;p&gt;Sagas are becoming ordinary. What is changing is the level of abstraction at which&lt;br&gt;
they are expressed. Handwritten orchestration is giving way to specified&lt;br&gt;
protocols such as LRA, to workflow engines that record the state of long-running&lt;br&gt;
processes durably, and to durable execution platforms in which the compensation&lt;br&gt;
logic is expressed as ordinary code. The direction of travel is consistent: the difficulty is being moved&lt;br&gt;
back out of application code and into infrastructure, which is where the industry&lt;br&gt;
placed it in the first place, some thirty years ago, and where it appears to&lt;br&gt;
belong.&lt;/p&gt;

&lt;p&gt;Two developments merit attention. The first is the growing use of the outbox&lt;br&gt;
pattern with event streaming, which achieves atomicity between a database change&lt;br&gt;
and a message by writing both to the same database and publishing asynchronously.&lt;br&gt;
This is neither 2PC nor a saga in the classical sense, and for many integration&lt;br&gt;
problems it is a better answer than either. The second is the increasing&lt;br&gt;
availability of formal verification tools for distributed protocols, which offers&lt;br&gt;
some prospect that compensation logic might eventually be checked rather than&lt;br&gt;
merely reviewed.&lt;/p&gt;

&lt;p&gt;The underlying tension, however, is not a matter of technology and will not be&lt;br&gt;
resolved by these developments. Atomicity across independent parties&lt;br&gt;
requires either that they transfer autonomy to a coordinator or that they accept&lt;br&gt;
observable intermediate states. The FLP (&lt;em&gt;Fischer, Lynch and Paterson&lt;/em&gt;) result and its relatives indicate that no&lt;br&gt;
implementation will escape this choice. What improves is the quality of the tools&lt;br&gt;
with which each side of the choice may be pursued, and the clarity with which the&lt;br&gt;
choice is presented to the practitioners.&lt;/p&gt;

&lt;p&gt;The most costly systems are not those that chose two-phase commit, nor those that&lt;br&gt;
chose sagas, but those that believed they had chosen the former while in fact&lt;br&gt;
operating the latter, without compensations.&lt;/p&gt;




&lt;h2&gt;
  
  
  References and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gray, J. &lt;em&gt;Notes on Data Base Operating Systems&lt;/em&gt;, 1978.&lt;/li&gt;
&lt;li&gt;Fischer, M., Lynch, N., Paterson, M. &lt;em&gt;Impossibility of Distributed Consensus
with One Faulty Process&lt;/em&gt;, Journal of the ACM, 1985.&lt;/li&gt;
&lt;li&gt;Garcia-Molina, H., Salem, K. &lt;em&gt;Sagas&lt;/em&gt;, ACM SIGMOD, 1987.&lt;/li&gt;
&lt;li&gt;X/Open. &lt;em&gt;Distributed Transaction Processing: The XA Specification&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Object Management Group. &lt;em&gt;Object Transaction Service&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Eclipse MicroProfile. &lt;em&gt;MicroProfile LRA Specification&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The accompanying source code, &lt;a href="https://github.com/nicolasduminil/quarkus-distributed-tx" rel="noopener noreferrer"&gt;&lt;code&gt;quarkus-distributed-tx&lt;/code&gt;&lt;/a&gt;,
containing both implementations, the shared contract test suite and a Docker
Compose deployment that runs the two stacks side by side.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Distributed Transactions in Enterprise Applications: From X/Open to MicroProfile LRA</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Mon, 19 Oct 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/distributed-transactions-in-enterprise-applications-from-xopen-to-microprofile-lra-28i9</link>
      <guid>https://dev.to/nicolasduminil/distributed-transactions-in-enterprise-applications-from-xopen-to-microprofile-lra-28i9</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;The problem of making several independent systems agree on a single outcome is older than most of the technology used to solve it. Historically, one of the &lt;br&gt;
first protocol able to provide this synchronization was 2PC (&lt;em&gt;two-phase commit&lt;/em&gt;)&lt;br&gt;
that the application servers era made an ordinary feature of enterprise development.&lt;br&gt;
Later lightweight frameworks, service-oriented and microservices architectures&lt;br&gt;
have made the same guarantee considerably harder to obtain.&lt;br&gt;&lt;br&gt;
This article follows that problem from its formalization in the X/Open Distributed&lt;br&gt;
Transaction Processing to the present situation. It then examines two working&lt;br&gt;
implementations of an identical use case, one using XA and two-phase commit, the&lt;br&gt;
other using the saga pattern, as specified by MicroProfile LRA, and compares them&lt;br&gt;
on evidence rather than on preference. The accompanying source code is available&lt;br&gt;
in the &lt;a href="https://github.com/nicolasduminil/quarkus-distributed-tx.git" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, where both implementations expose the&lt;br&gt;
same REST contract and are exercised by the same test suite.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Origins: X/Open, and the Formalization of 2PC
&lt;/h2&gt;

&lt;p&gt;The 2PC protocol is older than the standard that made it famous. Its&lt;br&gt;
essential shape appears in the database literature of the late 1970s, notably in&lt;br&gt;
the work of Jim Gray on the recovery and consistency properties of database&lt;br&gt;
operating systems. The problem it addresses is easy to state and difficult to&lt;br&gt;
solve: several parties must all either perform or decline an action. And they&lt;br&gt;
must do that despite the possibility that any of them, including the party &lt;br&gt;
conducting the negotiation, may fail at any moment.&lt;/p&gt;

&lt;p&gt;As its name states, the protocol proceeds in two phases. In the first, the&lt;br&gt;
coordinator asks every participant whether it is able to commit. A participant&lt;br&gt;
that answers affirmatively cannot retract itself. It must have&lt;br&gt;
written enough durable state to guarantee that it can still commit even after &lt;br&gt;
a crash. In the second phase, the coordinator informs the participants of the &lt;br&gt;
collective decision, which is affirmative only if every vote was affirmative.&lt;/p&gt;

&lt;p&gt;In early '90s, the X/Open consortium decided to contribute by formalizing a reference &lt;br&gt;
model named DTP (&lt;em&gt;Distributed Transaction Processing&lt;/em&gt;). This model identified&lt;br&gt;
three roles namely the application program, the transaction manager and the &lt;br&gt;
resource manager. The following two interfaces have been defined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the TX (&lt;em&gt;Transaction Demarcation&lt;/em&gt;) interface governs how an application demarcates a transaction;&lt;/li&gt;
&lt;li&gt;the XA (*eXtended Architecture) interface defines how the transaction manager drives the resource manager through the phases of the commit;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the XA specifications existed, a transaction manager written by one&lt;br&gt;
vendor could coordinate a database written by another, and the industry acquired&lt;br&gt;
a genuinely portable mechanism for distributed atomicity.&lt;/p&gt;

&lt;p&gt;Let's note a property of the XA specifications that will become important&lt;br&gt;
later. Between the moment a participant votes to commit and the moment it learns&lt;br&gt;
the collective decision, it is &lt;em&gt;blocked&lt;/em&gt;. It holds its locks, it cannot proceed,&lt;br&gt;
and it cannot unilaterally decide. If the coordinator fails during this interval,&lt;br&gt;
the participant waits. Skeen and others demonstrated in the early '80s that this&lt;br&gt;
blocking behavior is not an artifact of a careless design but a logical consequence&lt;br&gt;
of the quorum-based model which tolerates partitions only by blocking whenever no&lt;br&gt;
quorum can be assembled. In 1985, Fischer, Lynch and Paterson published the article&lt;br&gt;
&lt;em&gt;Impossibility of Distributed Consensus with One Faulty Process&lt;/em&gt; which establishes&lt;br&gt;
that deterministic consensus cannot be guaranteed in an asynchronous system in &lt;br&gt;
which even one process may fail.&lt;/p&gt;

&lt;p&gt;Because of this vulnerability 2PC was considered in the beginning a flawed protocol&lt;br&gt;
that needed improvements. It took some time and experience to understand that, &lt;br&gt;
on the opposite, it's an exact trade-off between perfect atomicity and availability.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Several Decades of Service
&lt;/h2&gt;

&lt;p&gt;It would be reasonable to expect that a protocol with a documented blocking&lt;br&gt;
failure mode and a known vulnerability would have been superseded. It has not &lt;br&gt;
been. 2PC remains the most widely deployed mechanism for distributed atomicity &lt;br&gt;
in enterprise systems, and it is likely to remain so for some time.&lt;/p&gt;

&lt;p&gt;The explanation is that the failure mode is rare and the guarantee is absolute.&lt;br&gt;
Within a single data center, coordinating two databases and a message broker&lt;br&gt;
across a reliable local network, with a transaction manager whose recovery log is&lt;br&gt;
on durable storage, the vulnerable interval is measured in milliseconds and the&lt;br&gt;
recovery procedure is automatic. Financial institutions, telecommunications&lt;br&gt;
operators and government systems adopted the protocol because the alternative was&lt;br&gt;
to write compensation logic by hand for every business operation. Additionally,&lt;br&gt;
software architects and developers are more confident in a protocol than in a &lt;br&gt;
promise that reconciliation will probably catch any discrepancies.&lt;/p&gt;

&lt;p&gt;Another important point to notice is that 2PC places complexity in the &lt;br&gt;
infrastructure field, written once, by specialists, and consumed through a single&lt;br&gt;
annotation. Every other alternative examined in this article moves that work into&lt;br&gt;
application code, where it must be written again for each operation, by&lt;br&gt;
developers whose principal expertise is the business domain rather than&lt;br&gt;
distributed systems. This is a real cost and it is frequently underestimated.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. OTS, JTS and JTA
&lt;/h2&gt;

&lt;p&gt;The X/Open model assumed a particular shape: an application, a transaction&lt;br&gt;
manager and some resource managers, generally within reach of one another. What&lt;br&gt;
it did not address is the case that the OO (&lt;em&gt;object-oriented&lt;/em&gt;) paradigm made &lt;br&gt;
central the co,cept of transactions spanning objects that reside in different &lt;br&gt;
processes, on different machines, supplied by different vendors, and invoked &lt;br&gt;
remotely as though they were local.&lt;/p&gt;

&lt;p&gt;The OMG (&lt;em&gt;Object Management Group&lt;/em&gt;), the consortium behind CORBA, answered this in the&lt;br&gt;
mid-1990s with the OTS (&lt;em&gt;Object Transaction Service&lt;/em&gt;), adopted as one of the&lt;br&gt;
CORBAservices. OTS described the roles of a distributed transaction in CORBA's&lt;br&gt;
IDL (&lt;em&gt;Interface Definition Language&lt;/em&gt;). The protocol underneath had not changed,&lt;br&gt;
the same resources, participants and quorum, in Gray's sense, were at work and &lt;br&gt;
no new algorithms has been proposed. OTS only specified how the 2PC participants&lt;br&gt;
might be distributed objects. And it settled the question that X/Open had left&lt;br&gt;
thin, namely how two transaction managers, from different vendors, should&lt;br&gt;
interoperate.&lt;/p&gt;

&lt;p&gt;OTS didn't displace XA but complemented it. The boundary to the resources, like &lt;br&gt;
databases or message brokers, remained what it had been, OTS typically only fronted&lt;br&gt;
resource managers driven over XA. Anything that OTS added sat above that &lt;br&gt;
boundary, between the managers themselves.&lt;/p&gt;

&lt;p&gt;The Jakarta EE platform, formerly Java EE and even formerly J2EE, absorbed this&lt;br&gt;
model with considerable thoroughness. The JTA (&lt;em&gt;Java Transaction API&lt;/em&gt;) specifications&lt;br&gt;
defined the interface through which applications and application servers demarcate &lt;br&gt;
transactions. JTS (&lt;em&gt;Java Transaction Service&lt;/em&gt;) brought OTS into Java. It required&lt;br&gt;
a transaction manager to implement that specification in its Java-mapped form, &lt;br&gt;
to propagate transaction context between managers as OTS prescribed, and to &lt;br&gt;
present JTA to the layer above. JTS was therefore not an alternative design but&lt;br&gt;
OTS in Java, with a JTA façade.&lt;/p&gt;

&lt;p&gt;The distinction between JTA and JTS is worth mentioning as the two are habitually&lt;br&gt;
confused, usually as "JTA is the interface and JTS the implementation". That is&lt;br&gt;
not quite it, since both are specifications. JTA is what an application programmer&lt;br&gt;
sees: &lt;code&gt;UserTransaction&lt;/code&gt;, &lt;code&gt;TransactionManager&lt;/code&gt;, and the interfaces by which resources&lt;br&gt;
are enlisted. JTS specifies what provides the guarantee underneath, including the&lt;br&gt;
propagation of transaction context between processes and the interoperability of&lt;br&gt;
transaction managers from different vendors.&lt;/p&gt;

&lt;p&gt;For the application developer, the effect was remarkable and is easily forgotten&lt;br&gt;
now. A method annotated for container-managed transactions would, upon invoking a&lt;br&gt;
remote component, carry its transaction context to that component, enlist the&lt;br&gt;
remote resources, and commit or roll back the entire assembly atomically. The&lt;br&gt;
programmer wrote business logic. The container arranged consensus. This was one&lt;br&gt;
of the genuinely impressive achievements of the enterprise Java period which &lt;br&gt;
worked reliably across products from competing vendors and it is&lt;br&gt;
somewhat unfair that it is now remembered as an old labyrinthine system.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. CORBA and IIOP as the Transport of Context
&lt;/h2&gt;

&lt;p&gt;The transaction context propagation just described required a mechanism, and the&lt;br&gt;
mechanism was CORBA. Transaction context traveled between application servers &lt;br&gt;
as part of the IIOP (&lt;em&gt;Internet Inter-ORB Protocol&lt;/em&gt;), in the form of a service &lt;br&gt;
context attached to the remote invocation. OTS defined how that context was&lt;br&gt;
represented and how transaction managers should interoperate. IIOP carried it.&lt;/p&gt;

&lt;p&gt;This arrangement was possible because &lt;strong&gt;the transport understood transactions&lt;/strong&gt;.&lt;br&gt;
A remote call was not merely a message but an invocation within a transactional&lt;br&gt;
context, and the infrastructure at both ends recognized it as such. Enlistment &lt;br&gt;
of the remote resource was automatic because the protocol carried the information&lt;br&gt;
required to perform it.&lt;/p&gt;

&lt;p&gt;The costs were substantial. CORBA was complex, its interoperability was achieved&lt;br&gt;
by considerable effort and its programming model demanded interface definitions&lt;br&gt;
and generated stubs. As the industry moved towards HTTP and simpler representations,&lt;br&gt;
these costs became decisive. But it is important to record that the price of the&lt;br&gt;
simplicity was HTTP lack of capacity to carry transactional semantics.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. The Decline of Heavyweight Servers and the Rise of Lighter Frameworks
&lt;/h2&gt;

&lt;p&gt;The reasons for the decline of the traditional application server are well&lt;br&gt;
documented and only tangentially concerned with transactions. Deployment units&lt;br&gt;
grew large and start-up times grew long at exactly the moment when elastic&lt;br&gt;
infrastructure began to reward the opposite. Container images preferred a single&lt;br&gt;
process to a shared runtime hosting many applications. The operational model of&lt;br&gt;
the application server, in which several applications share a managed container&lt;br&gt;
administered by a specialist team, was displaced by one in which each service&lt;br&gt;
carries its own runtime and is administered by the team that wrote it.&lt;/p&gt;

&lt;p&gt;Spring Boot, released in 2014, demonstrated that an embedded server and&lt;br&gt;
convention-based configuration could deliver most of the productivity of the&lt;br&gt;
enterprise stack with a fraction of its ceremony. Quarkus, which followed in 2019,&lt;br&gt;
went further by moving substantial work from run time to build time, producing&lt;br&gt;
start-up characteristics that make Java a plausible choice for serverless and&lt;br&gt;
elastically scaled deployments.&lt;/p&gt;

&lt;p&gt;Jakarta EE itself, transferred to the Eclipse Foundation in 2017 and subsequently&lt;br&gt;
renamed, continues to evolve, and both Spring Boot and Quarkus rely on&lt;br&gt;
substantial parts of its specifications. The change is therefore less a&lt;br&gt;
repudiation of the enterprise Java model than a redistribution of it. The&lt;br&gt;
specifications survived, the monolithic container that implemented them did not.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. What the Lighter Frameworks Lost
&lt;/h2&gt;

&lt;p&gt;Two changes occurred simultaneously. The first is architectural. The microservice&lt;br&gt;
architecture style holds that each service should own its data, and that no other&lt;br&gt;
service should reach into that data directly. This principle is incompatible with&lt;br&gt;
2PC across services which requires the coordinator to be connected to&lt;br&gt;
the resource managers. A coordinator that must not touch another service's&lt;br&gt;
resources cannot enlist them.&lt;/p&gt;

&lt;p&gt;The second is a matter of transport. HTTP does not propagate transaction context.&lt;br&gt;
It doesn't provide an equivalent of the IIOP service context. A REST call is an&lt;br&gt;
independent interaction whose relationship to the caller's transaction is not &lt;br&gt;
represented anywhere, unless the application arranges to represent it.&lt;/p&gt;

&lt;p&gt;The consequence, which is regularly misunderstood in practice, is that&lt;br&gt;
annotating a method as transactional and then invoking a remote service from&lt;br&gt;
within it produces no distributed transaction of any kind. &lt;br&gt;
If the local transaction rolls back, the remote effect stands. If&lt;br&gt;
the local transaction commits, nothing establishes that the remote one did, and&lt;br&gt;
a remote call that returned no answer may have applied its change or may not,&lt;br&gt;
leaving the caller no basis on which to decide. The divergence does not even&lt;br&gt;
require the remote service to fail. The arrangement has the&lt;br&gt;
appearance of a distributed transaction and none of the substance, which is the&lt;br&gt;
least desirable of the available outcomes, since it fails while presenting the&lt;br&gt;
appearance of success.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. The Saga Pattern, and the Ease of Implementing It Incorrectly
&lt;/h2&gt;

&lt;p&gt;The saga was described by Garcia-Molina and Salem in 1987, in a paper concerned&lt;br&gt;
with long-lived transactions within a single database. Their observation was that&lt;br&gt;
a transaction holding locks for a long period is destructive to concurrency, and&lt;br&gt;
that certain long transactions may be decomposed into a sequence of shorter ones,&lt;br&gt;
each of which commits independently, with a compensating transaction defined for&lt;br&gt;
each step. If the sequence cannot be completed, the compensations for the&lt;br&gt;
completed steps are executed in reverse order.&lt;/p&gt;

&lt;p&gt;The saga doesn't provide isolation. This is not a defect of a particular&lt;br&gt;
implementation but by definition. Intermediate states are visible to other&lt;br&gt;
observers, because each step has genuinely committed. In exchange, no participant&lt;br&gt;
holds a lock while waiting for a distant coordinator, and each service retains&lt;br&gt;
authority over its own data.&lt;/p&gt;

&lt;p&gt;The naive implementation, which is very common, executes each step and defines its&lt;br&gt;
compensation as the arithmetic inverse. Let's consider the example of a money transfer&lt;br&gt;
service, like the one presented in the associated project. A debit is compensated&lt;br&gt;
by a credit of the same amount. This is wrong in two distinct ways.&lt;/p&gt;

&lt;p&gt;The first defect is the visible intermediate state. Between the debit and its&lt;br&gt;
compensation, the balance is genuinely reduced. Any concurrent observer, including&lt;br&gt;
another saga evaluating whether sufficient funds exist, will read that reduced&lt;br&gt;
value and may act upon information that is about to be invalidated.&lt;/p&gt;

&lt;p&gt;The second defect is that &lt;strong&gt;the compensating action can itself fail&lt;/strong&gt;. A credit is&lt;br&gt;
an operation like any other, subject to validation, to constraints, and to the&lt;br&gt;
account having been closed in the interval. A saga whose compensation fails is in&lt;br&gt;
a worse condition than one that never began, since it has performed part of an&lt;br&gt;
operation and it cannot undo it. The system requires human intervention, and the&lt;br&gt;
humans require an explanation.&lt;/p&gt;

&lt;p&gt;The remedy, illustrated in Section 10, is to arrange matters so that no&lt;br&gt;
compensating write is required. If the first step reserves rather than acts, then&lt;br&gt;
compensation releases the reservation, and a release cannot fail for business&lt;br&gt;
reasons because it removes an obligation rather than creating one.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. MicroProfile LRA
&lt;/h2&gt;

&lt;p&gt;MicroProfile LRA, where LRA denotes &lt;em&gt;Long Running Action&lt;/em&gt;, specifies a saga&lt;br&gt;
coordination protocol for services communicating over HTTP. It is best understood&lt;br&gt;
as an answer to the two negative effects identified in Section 6: it reintroduces a context&lt;br&gt;
that travels with the call, and it reintroduces a coordinator that drives&lt;br&gt;
participants to a common outcome. It does not, and cannot, reintroduce atomicity.&lt;/p&gt;

&lt;p&gt;The programming model is declarative. A method annotated &lt;code&gt;@LRA&lt;/code&gt; participates in a&lt;br&gt;
long-running action, beginning one if required. Context propagates in the&lt;br&gt;
&lt;code&gt;Long-Running-Action&lt;/code&gt; HTTP header, which the implementation attaches to outgoing&lt;br&gt;
calls and interprets on incoming ones. A participant declares its obligations&lt;br&gt;
through further annotations: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Compensate&lt;/code&gt;, which is invoked if the action is cancelled;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Complete&lt;/code&gt;, invoked if it is closed successfully;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Status&lt;/code&gt;, by which the coordinator may enquire what a participant did; &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Forget&lt;/code&gt;, by which a participant may discard state it no longer needs to retain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two properties of this protocol are commonly overlooked. The first is that participants&lt;br&gt;
must be &lt;strong&gt;idempotent&lt;/strong&gt;. The coordinator retries. A participant that applies its&lt;br&gt;
effect a second time corrupts its own data. This is the requirement that appears&lt;br&gt;
in every specification document and is absent from a remarkable proportion of &lt;br&gt;
first implementations.&lt;/p&gt;

&lt;p&gt;The second is that the coordinator is a &lt;strong&gt;separate process that calls the&lt;br&gt;
participants back&lt;/strong&gt;. As opposed to JTA where the transaction manager is embedded&lt;br&gt;
in the application, under LRA it is not and the participant must reach the coordinator&lt;br&gt;
to begin, join and end an action. The coordinator must reach every participant&lt;br&gt;
to invoke its callbacks. It is entirely possible to construct a deployment in &lt;br&gt;
which the first direction works as expected while the second does not, and the resulting &lt;br&gt;
behavior is discussed in Section 10.&lt;/p&gt;


&lt;h2&gt;
  
  
  9. The XA Implementation in Detail
&lt;/h2&gt;

&lt;p&gt;The use case is deliberately modest: transfer an amount from one account to&lt;br&gt;
another and record the transfer. Two databases are involved, one holding accounts&lt;br&gt;
and one holding the transfer log, because a single database would reduce the&lt;br&gt;
problem to a local transaction and demonstrate nothing.&lt;/p&gt;

&lt;p&gt;One component in this arrangement is essential: the Narayana transaction manager.&lt;br&gt;
Its lineage is running back to the Arjuna research at the University of Newcastle&lt;br&gt;
and provided two decades of good service as the transaction manager of JBoss and&lt;br&gt;
WildFly. Its Quarkus extension, used here, brings a JTA implementation embedded&lt;br&gt;
in the application's own process. It owns the object store, decides the outcome&lt;br&gt;
and executes the 2PC protocol on the application's behalf. The same project supplies&lt;br&gt;
the LRA coordinator that section 10 relies upon, so both implementations in this&lt;br&gt;
article rely on the same body of code.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;Each datasource is declared as an XA one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;quarkus.datasource.accounts.jdbc.transactions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xa&lt;/span&gt;
&lt;span class="py"&gt;quarkus.datasource.xfers.jdbc.transactions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;xa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flag is the entire mechanism. The connection pool supplies &lt;code&gt;XAConnection&lt;/code&gt;&lt;br&gt;
instances rather than ordinary ones, and each pool enlists an &lt;code&gt;XAResource&lt;/code&gt; in the&lt;br&gt;
active transaction upon first use.&lt;/p&gt;
&lt;h3&gt;
  
  
  The transactional method
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TransferDto&lt;/span&gt; &lt;span class="nf"&gt;processTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TransferRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSourceAccountId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSourceAccountId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="nc"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDestinationAccountId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccountNotFoundException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDestinationAccountId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBalance&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;compareTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InsufficientFundsException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="nc"&gt;Transfer&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Transfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Common&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRandomId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCurrency&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;TransferStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPLETED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="n"&gt;transferRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTransfer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transfer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toDto&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is no reference to two-phase commit in this code, and that is the&lt;br&gt;
observation worth recording. The annotation causes a JTA transaction to begin. The&lt;br&gt;
two repositories address two different datasources. Each datasource enlists itself.&lt;br&gt;
When the method returns, Narayana performs the protocol.&lt;/p&gt;
&lt;h3&gt;
  
  
  The protocol as executed
&lt;/h3&gt;

&lt;p&gt;Upon commit the transaction manager calls &lt;code&gt;prepare&lt;/code&gt; on both branches. Each&lt;br&gt;
PostgreSQL instance executes &lt;code&gt;PREPARE TRANSACTION&lt;/code&gt;, writing the changes durably in&lt;br&gt;
a state from which they can still either be commited or rolled back. If both &lt;br&gt;
branches vote affirmatively, the transaction manager writes its decision to &lt;br&gt;
its object store. This is the point at which the outcome becomes irrevocable&lt;br&gt;
and then the transaction manager calls &lt;code&gt;commit&lt;/code&gt; on both branches, each of which&lt;br&gt;
executes &lt;code&gt;COMMIT PREPARED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two consequences of operational importance follow. PostgreSQL refuses prepared&lt;br&gt;
transactions unless &lt;code&gt;max_prepared_transactions&lt;/code&gt; is configured with a positive&lt;br&gt;
value, its default being zero, so an otherwise correct application fails at the&lt;br&gt;
first commit. And the transaction manager's object store must be durable, since it&lt;br&gt;
holds the only record of a decision made but not yet applied. Recovery must be&lt;br&gt;
enabled so that transactions left in doubt by a failure are resolved when the&lt;br&gt;
process returns.&lt;/p&gt;
&lt;h3&gt;
  
  
  The architectural cost
&lt;/h3&gt;

&lt;p&gt;The transfer service interacts directly with the &lt;code&gt;accounts&lt;/code&gt; database. It does not&lt;br&gt;
ask anything of the account service, it holds the resource manager instead. This&lt;br&gt;
means that it owns a connection to the &lt;code&gt;accounts&lt;/code&gt; database and enlist it in its&lt;br&gt;
own transaction. The transfer service opens that connection, modifies the account&lt;br&gt;
rows itself through its own persistence layer, and the &lt;code&gt;XAResource&lt;/code&gt; behind that&lt;br&gt;
connection becomes a branch of the transaction that the transfer service began.&lt;br&gt;
The account service plays no part. Its code does not run and it is not informed.&lt;br&gt;
The commit decision belongs to the transfer service's transaction manager&lt;/p&gt;

&lt;p&gt;This is not an oversight but a requirement. &lt;code&gt;prepare&lt;/code&gt; and &lt;code&gt;commit&lt;/code&gt; are operations&lt;br&gt;
on an &lt;code&gt;XAResource&lt;/code&gt;, and a service boundary exposes no equivalent of them. HTTP&lt;br&gt;
offers no way to ask a remote service to make a change durable while leaving its&lt;br&gt;
outcome open. A coordinator that cannot reach the resource manager cannot enlist&lt;br&gt;
it. The consequence is that the two services need to share a database, and the&lt;br&gt;
independence that the service boundary was intended to establish is not present.&lt;br&gt;
The repository's &lt;code&gt;docker-compose.yaml&lt;/code&gt; states this fact in one line: &lt;code&gt;transfer&lt;/code&gt; is&lt;br&gt;
given its own database, as well as the accounts datasource URL, whereas &lt;br&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; is given, in addition to its own database URL, the one of the &lt;br&gt;
account service.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. The LRA Implementation in Detail
&lt;/h2&gt;

&lt;p&gt;The saga implementation exposes an identical REST contract and reaches balances&lt;br&gt;
only through the account service's HTTP interface. It has no dependency on the&lt;br&gt;
accounts persistence layer, and that absence is the entire architectural point.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reserve, then confirm
&lt;/h3&gt;

&lt;p&gt;In the LRA implementation and as opposed to the 2PC one, the account service does&lt;br&gt;
not move money when asked to reserve a debit. It records a &lt;code&gt;Reservation&lt;/code&gt; in state&lt;br&gt;
&lt;code&gt;PENDING&lt;/code&gt; and leaves the balance unchanged. Available funds are computed as the&lt;br&gt;
balance minus the sum of pending debits, so another possible concurrent saga &lt;br&gt;
cannot reserve the same money. Upon &lt;code&gt;@Complete&lt;/code&gt; the reservations are applied; &lt;br&gt;
upon &lt;code&gt;@Compensate&lt;/code&gt; they are released.&lt;/p&gt;

&lt;p&gt;Accordingly, &lt;strong&gt;because the balance never moved, compensation performs no update&lt;br&gt;
and, therefore, no persistence operation can fail during compensation.&lt;/strong&gt; Hence,&lt;br&gt;
the second issue identified in Section 7 is eliminated by construction rather &lt;br&gt;
than by coding.&lt;/p&gt;
&lt;h3&gt;
  
  
  Idempotency, enforced twice
&lt;/h3&gt;

&lt;p&gt;Reservations are identified, in business terms, by a triple consisting in the action identifier,&lt;br&gt;
the account and the reservation type. Notice that the triple is not the entity's primary key, &lt;br&gt;
which is a generated value, so nothing in the object model prevents a second row carrying the&lt;br&gt;
same triple. And in order to avoid that, it is declared as a unique constraint in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RESERVATIONS"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;uniqueConstraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@UniqueConstraint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"UK_RESERVATION_LRA"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;columnNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"LRA_ID"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ACCOUNT_ID"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"RESERVATION_TYPE"&lt;/span&gt;&lt;span class="o"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraint is needed because the account service, when asked to reserve a&lt;br&gt;
debit, looks for an existing reservation before writing one, which is a&lt;br&gt;
check-then-act sequence and, therefore, not atomic. Two deliveries of the same&lt;br&gt;
reserve request can both find nothing and both insert. The constraint avoids that&lt;br&gt;
and guarantees that the database refuses the 2nd reserve.&lt;/p&gt;

&lt;p&gt;Retries could also cause the effect described above. The coordinator waits for&lt;br&gt;
each callback, but a call that times out or whose reply is lost leaves the&lt;br&gt;
action unresolved, so it keeps calling again and again. A settlement instruction&lt;br&gt;
therefore might arrive more than once for a reservation that is already recorded. The&lt;br&gt;
database constraint cannot help here, since no row is being inserted. Then the guard&lt;br&gt;
sits one level down, in the service class shown below. As you can see, the two &lt;br&gt;
callbacks annotated with &lt;code&gt;@Compensate&lt;/code&gt; and &lt;code&gt;@Complete&lt;/code&gt; annotations obtain their&lt;br&gt;
reservations from a single helper, the &lt;code&gt;pendingFor(...)&lt;/code&gt; method, that selects&lt;br&gt;
only the &lt;code&gt;PENDING&lt;/code&gt; reservations, so that the ones already confirmed or already&lt;br&gt;
released are passed over rather than settled again. &lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@ApplicationScoped
public class AccountLraService
{
  ...
  @Transactional
  public ParticipantStatus complete(String lraId)
  {
    pendingFor(lraId).forEach(this::confirm);
    return ParticipantStatus.Completed;
  }
  ...
  @Transactional
  public ParticipantStatus compensate(String lraId)
  {
    pendingFor(lraId).forEach(reservation -&amp;gt; reservation.setState(ReservationState.RELEASED));
    return ParticipantStatus.Compensated;
  }
  ...
  private Stream&amp;lt;Reservation&amp;gt; pendingFor(String lraId)
  {
    return reservations.findByLra(lraId).stream()
     .filter(reservation -&amp;gt; reservation.getState() == ReservationState.PENDING);
  }
  ...
}  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  11 The Project
&lt;/h2&gt;

&lt;p&gt;The project that illustrates this article is a Quarkus workshop showing a simple&lt;br&gt;
money transfer use case in the two ways that we discussed along the article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XA / 2PC&lt;/strong&gt; (&lt;code&gt;transfer&lt;/code&gt;, port 8080): a genuine 2PC across two XA
datasources, coordinated by Narayana.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saga / MicroProfile LRA&lt;/strong&gt; (&lt;code&gt;transfer-lra&lt;/code&gt;, port 8090 and &lt;code&gt;account&lt;/code&gt;, port 8081): a compensation-based
consistency example across autonomous services, coordinated by a Narayana LRA coordinator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both expose the same &lt;code&gt;/xfers&lt;/code&gt; REST contract and are driven by the same shared&lt;br&gt;
test suite (&lt;code&gt;transfer-tck&lt;/code&gt;), so any difference you observe is a difference in&lt;br&gt;
transaction strategy and nothing else.&lt;/p&gt;
&lt;h3&gt;
  
  
  Modules
&lt;/h3&gt;

&lt;p&gt;The following modules are part of the workshop project.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Module&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;common&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared library: &lt;code&gt;Common&lt;/code&gt; utility, the transfer DTOs (&lt;code&gt;TransferDto&lt;/code&gt;, &lt;code&gt;TransferRequest&lt;/code&gt;, &lt;code&gt;TransferResponse&lt;/code&gt;, &lt;code&gt;TransferStatus&lt;/code&gt;), the application exception hierarchy and the single &lt;code&gt;ApplicationExceptionMapper&lt;/code&gt; that maps them to HTTP status codes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account-repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared persistence layer for accounts: the &lt;code&gt;Account&lt;/code&gt; entity, &lt;code&gt;AccountDto&lt;/code&gt;, the &lt;code&gt;AccountRepository&lt;/code&gt; interface and its two implementations. Depended on by &lt;code&gt;account&lt;/code&gt; and by &lt;code&gt;transfer&lt;/code&gt; — and pointedly &lt;strong&gt;not&lt;/strong&gt; by &lt;code&gt;transfer-lra&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-tck&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The REST contract both transfer implementations must satisfy, as an abstract test class. Test tooling is at compile scope so consumers depend on it with &lt;code&gt;&amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Account microservice (port 8081). Owns the &lt;code&gt;accounts&lt;/code&gt; schema. CRUD + debit/credit, &lt;strong&gt;plus&lt;/strong&gt; the LRA participant endpoints under &lt;code&gt;/lra/accounts&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XA coordinator (port 8080). Two XA datasources (&lt;code&gt;accounts&lt;/code&gt;, &lt;code&gt;xfers&lt;/code&gt;). Transfer CRUD plus the 2PC transfer operation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-lra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saga orchestrator (port 8090). One plain datasource for its own transfer log; reaches balances only through the account service's API.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each repository has two implementations: an in-memory one (&lt;code&gt;DefaultAccountRepository&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;DefaultTransferRepository&lt;/code&gt;) and a JPA/Panache one (&lt;code&gt;AccountRepositoryXA&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;TransferRepositoryXA&lt;/code&gt;). Both are &lt;code&gt;@Alternative&lt;/code&gt;, so neither is enabled by default;&lt;br&gt;
each application picks the XA pair explicitly through&lt;br&gt;
&lt;code&gt;quarkus.arc.selected-alternatives&lt;/code&gt;. Only the XA implementations participate in 2PC —&lt;br&gt;
the in-memory ones are there to run the services without a database.&lt;/p&gt;

&lt;p&gt;All four modules carry the Jandex plugin so their &lt;code&gt;@Entity&lt;/code&gt;, &lt;code&gt;@Provider&lt;/code&gt; and CDI bean&lt;br&gt;
classes are indexed when consumed as external dependencies.&lt;/p&gt;
&lt;h3&gt;
  
  
  Endpoints
&lt;/h3&gt;

&lt;p&gt;Three services expose an HTTP API, each with a Swagger UI at &lt;code&gt;/q/swagger-ui&lt;/code&gt;. The&lt;br&gt;
rows marked as callbacks are invoked by the LRA coordinator, not by clients.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create an account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}/debit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Debit, amount in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/accounts/{accountId}/credit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credit, amount in the body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/debit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reserve a debit, &lt;code&gt;@LRA(MANDATORY)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/credit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reserve a credit, &lt;code&gt;@LRA(MANDATORY)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/{accountId}/availability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance, held and available amounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Complete&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/compensate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Compensate&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Status&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;account&lt;/code&gt; (8081)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/lra/accounts/forget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Forget&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transfer, in one 2PC transaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the transfer log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer&lt;/code&gt; (8080)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Transfer, as a saga, &lt;code&gt;@LRA(REQUIRED)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List the transfer log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Read one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/{transferId}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete one transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Complete&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/compensate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Compensate&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Status&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; (8090)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/xfers/lra/forget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Forget&lt;/code&gt; callback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things are worth reading off the table. The two transfer services present the&lt;br&gt;
same &lt;code&gt;POST /xfers&lt;/code&gt; contract, taking the same request body of&lt;br&gt;
&lt;code&gt;{sourceAccountId, destinationAccountId, amount, currency}&lt;/code&gt;, which is what allows&lt;br&gt;
a single test class to exercise both. And the account service's participant API&lt;br&gt;
sits under its own path prefix instead of altering the existing endpoints, which&lt;br&gt;
is why the saga work left the original account tests untouched.&lt;/p&gt;
&lt;h3&gt;
  
  
  Build &amp;amp; test
&lt;/h3&gt;

&lt;p&gt;Integration tests use Quarkus Dev Services (Testcontainers), so a running Docker&lt;br&gt;
daemon is required. No manual database setup is needed. Each service seeds its own&lt;br&gt;
data in dev/test profiles, on the behalf of the &lt;code&gt;import.sql&lt;/code&gt; file for &lt;code&gt;account&lt;/code&gt; and of the&lt;br&gt;
&lt;code&gt;import-accounts.sql&lt;/code&gt; for &lt;code&gt;transfer&lt;/code&gt;. In the &lt;code&gt;prod&lt;/code&gt; profile the account service&lt;br&gt;
owns the &lt;code&gt;accounts&lt;/code&gt; schema and no generation or seeding happens.&lt;/p&gt;

&lt;p&gt;The following command runs the integration tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn clean verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the list of the integration tests provided with the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AbstractMoneyTransferContractIT&lt;/code&gt; (transfer-tck): this is and abstract class serving as a superclass for all the integration tests. It defines the shared scenarios: happy
path, insufficient funds, unknown account, listing
and deletion. Assertions are on deltas*, and the balance/count probes are
abstract, because the XA service can read the databases directly while the
saga service can only get them by calling the API over HTTP. Failure paths assert the response body
as well as the status. Both stacks must return the business error rather than
transaction plumbing, which a status-only assertion would not catch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MoneyTransferIT&lt;/code&gt; (transfer): the contract against XA, with repository-based probes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MoneyTransferSagaIT&lt;/code&gt; (transfer-lra): the contract against LRA, with HTTP probes,
plus saga-only assertions about the &lt;code&gt;COMPENSATED&lt;/code&gt; audit row and released reservations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AccountResourceIT&lt;/code&gt; (account): account CRUD, debit/credit, 404 on unknown account,
and rejection of overdrafts and invalid amounts. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Run the full stack
&lt;/h3&gt;

&lt;p&gt;In order to run the full stack, production like, proceed as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the container images (container-image.build=true is enabled)&lt;/span&gt;
mvn clean package
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;docker-compose.yaml&lt;/code&gt; starts the whole comparison:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accounts-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5432&lt;/td&gt;
&lt;td&gt;Accounts, &lt;code&gt;max_prepared_transactions=100&lt;/code&gt; for XA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xfers-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5433&lt;/td&gt;
&lt;td&gt;XA transfer log, &lt;code&gt;max_prepared_transactions=100&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;xfers-lra-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5434&lt;/td&gt;
&lt;td&gt;Saga transfer log (no XA needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lra-coordinator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;&lt;code&gt;quay.io/jbosstm/lra-coordinator&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8081&lt;/td&gt;
&lt;td&gt;CRUD + LRA participant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8080&lt;/td&gt;
&lt;td&gt;XA/2PC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transfer-lra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8090&lt;/td&gt;
&lt;td&gt;Saga/LRA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both transfer services run against the same accounts, so you can drive the same&lt;br&gt;
transfer through each and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BODY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"sourceAccountId":"ACC1","destinationAccountId":"ACC2","amount":10.00,"currency":"EUR"}'&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8080/xfers  &lt;span class="c"&gt;# XA&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-XPOST&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BODY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8090/xfers  &lt;span class="c"&gt;# Saga&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8081/lra/accounts/ACC1/availability                                &lt;span class="c"&gt;# reservations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;QUARKUS_LRA_BASE_URI&lt;/code&gt; on the two participants: it is the address the&lt;br&gt;
coordinator uses to call them back, and without it a participant advertises a URL&lt;br&gt;
that is only meaningful from its own side of the network.&lt;/p&gt;

&lt;p&gt;A Swagger interface is also available with each API, as explained at §11 Endpoints.&lt;br&gt;
Additionally, the smoke test script &lt;code&gt;test-all.sh&lt;/code&gt; is a very convenient way to make&lt;br&gt;
sure to cover all the test case possible combinations. here below is an example&lt;br&gt;
of how to run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;./test-all.sh

&lt;span class="o"&gt;===&lt;/span&gt; Account API - http://localhost:8081/accounts &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC90, 1000.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC91, 0.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS POST /accounts &lt;span class="o"&gt;(&lt;/span&gt;create ACC92, 10.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 201
PASS GET /accounts/ACC90 -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 1000.00
PASS GET /accounts &lt;span class="o"&gt;(&lt;/span&gt;list&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS PUT /accounts/ACC90 &lt;span class="o"&gt;(&lt;/span&gt;rename owner&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /accounts/ACC90 &lt;span class="o"&gt;(&lt;/span&gt;confirm rename&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS customerName updated
PASS POST /accounts/ACC90/credit &lt;span class="o"&gt;(&lt;/span&gt;50.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 1050.00
PASS POST /accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;200.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00

&lt;span class="o"&gt;===&lt;/span&gt; Account API - negative cases &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST credit &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;.00&lt;span class="o"&gt;)&lt;/span&gt; rejected -&amp;gt; 400
PASS POST debit &lt;span class="o"&gt;(&lt;/span&gt;0.00&lt;span class="o"&gt;)&lt;/span&gt; rejected -&amp;gt; 400
PASS POST debit &lt;span class="o"&gt;(&lt;/span&gt;999999.00&lt;span class="o"&gt;)&lt;/span&gt; insufficient funds -&amp;gt; 400
PASS GET /accounts/NOSUCH -&amp;gt; 404
PASS POST credit on unknown account -&amp;gt; 404
PASS POST credit without Content-Type -&amp;gt; 415
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00
PASS DELETE /accounts/ACC92 -&amp;gt; 204
PASS GET deleted account -&amp;gt; 404
PASS DELETE already-deleted account -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - http://localhost:8081/lra/accounts &lt;span class="o"&gt;===&lt;/span&gt;
PASS GET /lra/accounts/ACC90/availability -&amp;gt; 200
PASS availability exposes balance/held/available
PASS POST /lra/accounts/ACC90/debit without LRA context -&amp;gt; 412
PASS POST /lra/accounts/ACC90/credit without LRA context -&amp;gt; 412
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 850.00

&lt;span class="o"&gt;===&lt;/span&gt; XA Transfer API &lt;span class="o"&gt;(&lt;/span&gt;2PC&lt;span class="o"&gt;)&lt;/span&gt; - http://localhost:8080/xfers &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers &lt;span class="o"&gt;(&lt;/span&gt;ACC90 -&amp;gt; ACC91, 100.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS transferId returned: raizmjf
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 750.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 100.00
PASS GET /xfers/raizmjf -&amp;gt; 200
PASS GET /xfers &lt;span class="o"&gt;(&lt;/span&gt;list&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS PUT /xfers/raizmjf &lt;span class="o"&gt;(&lt;/span&gt;currency -&amp;gt; USD&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200

&lt;span class="o"&gt;===&lt;/span&gt; XA Transfer API - negative cases &lt;span class="o"&gt;(&lt;/span&gt;nothing must move&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers insufficient funds -&amp;gt; 400
PASS POST /xfers negative amount -&amp;gt; 400
PASS POST /xfers unknown &lt;span class="nb"&gt;source&lt;/span&gt; -&amp;gt; 404
PASS POST /xfers unknown destination -&amp;gt; 404
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 750.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 100.00
PASS GET /xfers/NOSUCH -&amp;gt; 404
PASS DELETE /xfers/raizmjf -&amp;gt; 204
PASS GET deleted transfer -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Saga Transfer API &lt;span class="o"&gt;(&lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; - http://localhost:8090/xfers &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers &lt;span class="o"&gt;(&lt;/span&gt;ACC90 -&amp;gt; ACC91, 25.00&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS transferId returned: etmlebm
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 125.00
PASS GET /xfers/etmlebm -&amp;gt; 200
PASS GET /xfers &lt;span class="o"&gt;(&lt;/span&gt;list, includes compensated&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS @Complete flipped the transfer to COMPLETED

&lt;span class="o"&gt;===&lt;/span&gt; Saga Transfer API - cancellation and compensation &lt;span class="o"&gt;===&lt;/span&gt;
PASS POST /xfers insufficient funds &lt;span class="o"&gt;(&lt;/span&gt;saga cancels&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 400
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 125.00
PASS @Compensate logged the cancelled transfer as COMPENSATED
PASS POST /xfers unknown destination &lt;span class="o"&gt;(&lt;/span&gt;saga cancels&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 404
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 725.00
PASS GET availability after settled sagas -&amp;gt; 200
PASS held &lt;span class="o"&gt;==&lt;/span&gt; 0 &lt;span class="o"&gt;(&lt;/span&gt;no reservation left dangling&lt;span class="o"&gt;)&lt;/span&gt;
PASS PUT /xfers/etmlebm &lt;span class="o"&gt;(&lt;/span&gt;currency -&amp;gt; USD&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /xfers/NOSUCH -&amp;gt; 404
PASS DELETE /xfers/etmlebm -&amp;gt; 204
PASS GET deleted transfer -&amp;gt; 404

&lt;span class="o"&gt;===&lt;/span&gt; Saga participant callbacks - @Status and @Forget &lt;span class="o"&gt;(&lt;/span&gt;transfer-lra&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1ed
PASS POST /xfers with an externally started LRA -&amp;gt; 412
PASS GET /xfers/lra/status &lt;span class="o"&gt;(&lt;/span&gt;@Status&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS @Status reports Compensated &lt;span class="k"&gt;for &lt;/span&gt;the cancelled LRA
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 725.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS DELETE /xfers/lra/forget &lt;span class="o"&gt;(&lt;/span&gt;@Forget&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS GET /xfers/lra/status after @Forget -&amp;gt; 404
PASS @Forget kept the transfer, only dropped its LRA &lt;span class="nb"&gt;id&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - driving a saga by hand &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1f2
PASS POST /lra/accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;40.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 725.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 40.00 &lt;span class="o"&gt;(&lt;/span&gt;now 40.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS POST /lra/accounts/ACC91/credit &lt;span class="o"&gt;(&lt;/span&gt;40.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 125.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS coordinator closed the LRA -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by &lt;span class="nt"&gt;-40&lt;/span&gt;.00 &lt;span class="o"&gt;(&lt;/span&gt;now 685.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC91&lt;span class="o"&gt;)&lt;/span&gt; moved by 40.00 &lt;span class="o"&gt;(&lt;/span&gt;now 165.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 0&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Account LRA participant API - cancelling a saga by hand &lt;span class="o"&gt;===&lt;/span&gt;
PASS coordinator started LRA: http://lra-coordinator:8080/lra-coordinator/0_ffffac120002_a03b_6ad35817_1f7
PASS POST /lra/accounts/ACC90/debit &lt;span class="o"&gt;(&lt;/span&gt;15.00, &lt;span class="k"&gt;in &lt;/span&gt;LRA&lt;span class="o"&gt;)&lt;/span&gt; -&amp;gt; 200
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 15.00 &lt;span class="o"&gt;(&lt;/span&gt;now 15.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS coordinator cancelled the LRA -&amp;gt; 200
PASS balance&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 685.00&lt;span class="o"&gt;)&lt;/span&gt;
PASS held&lt;span class="o"&gt;(&lt;/span&gt;ACC90&lt;span class="o"&gt;)&lt;/span&gt; moved by 0 &lt;span class="o"&gt;(&lt;/span&gt;now 0&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; Summary &lt;span class="o"&gt;===&lt;/span&gt;
87 passed, 0 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  12. Comparison
&lt;/h2&gt;

&lt;p&gt;The attached GitHub repository contains a multi-module Maven project with two&lt;br&gt;
implementations. The&lt;br&gt;
first uses JTA and XA to obtain a two-phase commit, and is contained entirely in&lt;br&gt;
the &lt;code&gt;transfer&lt;/code&gt; module. The second uses MicroProfile LRA, and is necessarily&lt;br&gt;
spread across two modules: &lt;code&gt;transfer-lra&lt;/code&gt; orchestrates the saga, while &lt;code&gt;account&lt;/code&gt;&lt;br&gt;
takes part in it. That difference in shape is not an accident of packaging. A&lt;br&gt;
JTA coordinator that enlists resource managers directly can live in a single &lt;br&gt;
module, the &lt;code&gt;transfer&lt;/code&gt; one, whereas a saga cannot exist unless its participants&lt;br&gt;
are reachable and callable, hence the necessity for it to live in two modules,&lt;br&gt;
&lt;code&gt;transfer-lra&lt;/code&gt; and &lt;code&gt;account&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both implementations satisfy the same test contract. The following differences&lt;br&gt;
are therefore differences of mechanism and of consequence, not of capability.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;XA and two-phase commit&lt;/th&gt;
&lt;th&gt;Saga and LRA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coordinator&lt;/td&gt;
&lt;td&gt;Embedded in the application process&lt;/td&gt;
&lt;td&gt;Separate process, must call participants back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data ownership&lt;/td&gt;
&lt;td&gt;Coordinator holds the resource managers, so services share a database&lt;/td&gt;
&lt;td&gt;Each service retains its own data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;Complete; no intermediate state is observable&lt;/td&gt;
&lt;td&gt;Absent by design; a reservation is observable while in flight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure&lt;/td&gt;
&lt;td&gt;Rollback leaves no trace whatsoever&lt;/td&gt;
&lt;td&gt;Leaves a record that the operation was attempted and undone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locks&lt;/td&gt;
&lt;td&gt;Held between prepare and commit&lt;/td&gt;
&lt;td&gt;None held across the action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Participants block if the coordinator fails while in doubt&lt;/td&gt;
&lt;td&gt;Participants remain available; completion may occur later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Location of difficulty&lt;/td&gt;
&lt;td&gt;Infrastructure, written once&lt;/td&gt;
&lt;td&gt;Application code, written for each operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure of the undo&lt;/td&gt;
&lt;td&gt;Not applicable; nothing was committed&lt;/td&gt;
&lt;td&gt;Possible, unless reservations are used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two entries deserve comment.&lt;/p&gt;

&lt;p&gt;The row concerning failure is frequently presented as an advantage of 2PC on the&lt;br&gt;
grounds that the rollbacks are clean. It may equally be regarded as a loss of &lt;br&gt;
information. A canceled saga records that a transfer was attempted and&lt;br&gt;
subsequently undone, which is an audit trail that a rollback cannot produce&lt;br&gt;
because the rows were never committed. Whether this constitutes an advantage&lt;br&gt;
depends entirely on whether the business is required to account for attempts as&lt;br&gt;
well as for outcomes, and this is a question for the domain rather than for the&lt;br&gt;
architecture.&lt;/p&gt;

&lt;p&gt;The row concerning the location of difficulty is, in my opinion, the&lt;br&gt;
one most often omitted from comparisons. 2PC is difficult once, in&lt;br&gt;
infrastructure maintained by specialists. Sagas are difficult repeatedly, in&lt;br&gt;
application code and this argument is important.&lt;/p&gt;

&lt;p&gt;The correct conclusion is not that one approach supersedes the other. It is that&lt;br&gt;
the choice is determined by whether the resource managers can be co-located under&lt;br&gt;
a single coordinator. If they can, 2PC provides a stronger guarantee&lt;br&gt;
for less application code. If they cannot, because the services are genuinely&lt;br&gt;
independent or the resources do not support XA, then a saga is not the inferior&lt;br&gt;
option but the only option, and the engineering effort should be directed at&lt;br&gt;
implementing it correctly rather than at regretting the atomicity that was never&lt;br&gt;
available.&lt;/p&gt;

&lt;h2&gt;
  
  
  13. The Present and the Future
&lt;/h2&gt;

&lt;p&gt;Two-phase commit is not disappearing. It remains appropriate wherever resources&lt;br&gt;
can be co-located, and a significant proportion of enterprise data continues to&lt;br&gt;
reside in exactly such arrangements. Reports of its obsolescence generally&lt;br&gt;
originate from contexts in which it was never applicable.&lt;/p&gt;

&lt;p&gt;Sagas are becoming ordinary. What is changing is the level of abstraction at which&lt;br&gt;
they are expressed. Handwritten orchestration is giving way to specified&lt;br&gt;
protocols such as LRA, to workflow engines that record the state of long-running&lt;br&gt;
processes durably, and to durable execution platforms in which the compensation&lt;br&gt;
logic is expressed as ordinary code. The direction of travel is consistent: the difficulty is being moved&lt;br&gt;
back out of application code and into infrastructure, which is where the industry&lt;br&gt;
placed it in the first place, some thirty years ago, and where it appears to&lt;br&gt;
belong.&lt;/p&gt;

&lt;p&gt;An important development merit attention: the growing use of the outbox&lt;br&gt;
pattern with event streaming, which achieves atomicity between a database change&lt;br&gt;
and a message by writing both to the same database and publishing asynchronously.&lt;br&gt;
This is neither 2PC nor a saga in the classical sense, and for many integration&lt;br&gt;
problems it is a better answer than either. The second is the increasing&lt;br&gt;
availability of formal verification tools for distributed protocols, which offers&lt;br&gt;
some prospect that compensation logic might eventually be checked rather than&lt;br&gt;
merely reviewed.&lt;/p&gt;

&lt;p&gt;The underlying tension, however, is not a matter of technology and will not be&lt;br&gt;
resolved by this development. Atomicity across independent parties&lt;br&gt;
requires either that they transfer autonomy to a coordinator or that they accept&lt;br&gt;
observable intermediate states. The FLP (&lt;em&gt;Fisher, Lynch and Paterson&lt;/em&gt;) result and its relatives indicate that no&lt;br&gt;
implementation will escape this choice. What improves is the quality of the tools&lt;br&gt;
with which each side of the choice may be pursued, and the clarity with which the&lt;br&gt;
choice is presented to the practitioners.&lt;/p&gt;

&lt;p&gt;The most costly systems are not those that chose two-phase commit, nor those that&lt;br&gt;
chose sagas, but those that believed they had chosen the former while in fact&lt;br&gt;
operating the latter, without compensations.&lt;/p&gt;




&lt;h2&gt;
  
  
  References and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gray, J. &lt;em&gt;Notes on Data Base Operating Systems&lt;/em&gt;, 1978.&lt;/li&gt;
&lt;li&gt;Fischer, M., Lynch, N., Paterson, M. &lt;em&gt;Impossibility of Distributed Consensus
with One Faulty Process&lt;/em&gt;, Journal of the ACM, 1985.&lt;/li&gt;
&lt;li&gt;Garcia-Molina, H., Salem, K. &lt;em&gt;Sagas&lt;/em&gt;, ACM SIGMOD, 1987.&lt;/li&gt;
&lt;li&gt;X/Open. &lt;em&gt;Distributed Transaction Processing: The XA Specification&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Object Management Group. &lt;em&gt;Object Transaction Service&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Eclipse MicroProfile. &lt;em&gt;MicroProfile LRA Specification&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The accompanying source code, &lt;code&gt;quarkus-distributed-tx&lt;/code&gt;, containing both
implementations, the shared contract test suite and a Docker Compose deployment
that runs the two stacks side by side.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>microservices</category>
    </item>
    <item>
      <title>50 Shades of Quarkus RESTful Services</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Fri, 25 Apr 2025 14:30:54 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/50-shades-of-quarkus-restful-services-lik</link>
      <guid>https://dev.to/nicolasduminil/50-shades-of-quarkus-restful-services-lik</guid>
      <description>&lt;p&gt;I'm so pleased to let my network know that my new booklet, entitled "50 Shades of Quarkus RESTful Services", is now available in both paperback and hardcover format. The Kindle format was already available as well, since last month.&lt;/p&gt;

&lt;p&gt;This booklet is dedicated to software architects and developers who think that the allegedly "no-code" solutions are rubbish and the ML and LLM based tools, while unavoidable nowadays, aren't meant to generate end-to-end code.&lt;/p&gt;

&lt;p&gt;What's inside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Historical approach of the RESTful services, from the RPC/Corba/DCOM ancestors, up to date.&lt;/li&gt;
&lt;li&gt;Quarkus RESTful services basics and annotations.
Eclipse MicroProfile implementation by Quarkus.&lt;/li&gt;
&lt;li&gt;Quarkus Synchronous RESTful services and clients.&lt;/li&gt;
&lt;li&gt;Quarkus Asynchronous RESTful services and clients.&lt;/li&gt;
&lt;li&gt;Quarkus Reactive RESTful services and clients.&lt;/li&gt;
&lt;li&gt;Anatomy of enterprise grade applications.&lt;/li&gt;
&lt;li&gt;Quarkus microservices based implemntation.&lt;/li&gt;
&lt;li&gt;Quarkus unit, integration and end-to-end testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to be a Java champion to read this booklet, just be willing to become one. Ready ?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shorturl.at/wtQVN" rel="noopener noreferrer"&gt;https://shorturl.at/wtQVN&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>quarkus</category>
      <category>restapi</category>
      <category>resteasy</category>
    </item>
    <item>
      <title>50 Shades of Java Executors: A deep dive into the Java multi-threading world</title>
      <dc:creator>Nicolas DUMINIL</dc:creator>
      <pubDate>Fri, 25 Apr 2025 13:56:51 +0000</pubDate>
      <link>https://dev.to/nicolasduminil/50-shades-of-java-executors-a-deep-dive-into-the-java-multi-threading-world-48j7</link>
      <guid>https://dev.to/nicolasduminil/50-shades-of-java-executors-a-deep-dive-into-the-java-multi-threading-world-48j7</guid>
      <description>&lt;p&gt;I'm pleased to announce the publication of my most recent booklet on concurrent programming techniques in Java, with a particular focus on race condition management through multi-threading and executors. It explores modern concurrency patterns and implementations, highlighting the latest features introduced in Java 21 and 23 LTS (&lt;em&gt;Long Term Support&lt;/em&gt;) releases. Special attention is given to the evolution of Java's executor framework and its role in managing concurrent operations efficiently.&lt;br&gt;
This booklet serves as a practical guide for developers seeking to understand and implement robust concurrent programming solutions using Java's latest threading capabilities. The presented concepts are illustrated by code examples implementing real business cases. A full benchmark comparing all the presented solutions is provided at the end of the report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.amazon.fr/50-Shades-Java-Executors-multi-threading/dp/B0DPJ1ZFLS" rel="noopener noreferrer"&gt;https://www.amazon.fr/50-Shades-Java-Executors-multi-threading/dp/B0DPJ1ZFLS&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>multithreading</category>
      <category>quarkus</category>
      <category>restful</category>
    </item>
  </channel>
</rss>
