<?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>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>
