<?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: Charles Hornick</title>
    <description>The latest articles on DEV Community by Charles Hornick (@charleshornick).</description>
    <link>https://dev.to/charleshornick</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3768525%2F2b4ba565-355d-4e86-9529-b325fcd6d06a.png</url>
      <title>DEV Community: Charles Hornick</title>
      <link>https://dev.to/charleshornick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charleshornick"/>
    <language>en</language>
    <item>
      <title>Ports &amp; Adapters: Beyond the Theory - Isolating the Application with JPMS</title>
      <dc:creator>Charles Hornick</dc:creator>
      <pubDate>Wed, 25 Mar 2026 10:58:21 +0000</pubDate>
      <link>https://dev.to/charleshornick/ports-adapters-beyond-the-theory-isolating-the-application-with-jpms-2gda</link>
      <guid>https://dev.to/charleshornick/ports-adapters-beyond-the-theory-isolating-the-application-with-jpms-2gda</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the third article in the "Ports &amp;amp; Adapters: Beyond the Theory" series.&lt;br&gt;
Article 2 showed how to organize the inside of the application by combining P&amp;amp;A with Simon Brown's &lt;em&gt;package-by-component&lt;/em&gt;.&lt;br&gt;
This article tackles the hole left by the previous article: the &lt;code&gt;public&lt;/code&gt; keyword exposing more than necessary.&lt;br&gt;
Every line of code in the companion repository was written by hand. No AI-generated code.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/charles-hornick/ports-and-adapters-beyond-the-theory/tree/article/3-jpms-isolation" rel="noopener noreferrer"&gt;GitHub - branch &lt;code&gt;article/3-jpms-isolation&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Series - Ports &amp;amp; Adapters: Beyond the Theory&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/runtime-adapter-hot-swapping-with-ports-adapters-the-pattern-alistair-cockburn-didnt-document-56cg"&gt;Runtime Adapter Hot-Swapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-organizing-the-application-with-package-by-component-49m3"&gt;Organizing the Application with Package-by-Component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-isolating-the-application-with-jpms-2gda"&gt;Isolating with JPMS&lt;/a&gt; ← you are here&lt;/li&gt;
&lt;li&gt;No Exceptions: Result&amp;lt;T&amp;gt; with Pragmatica (coming soon)&lt;/li&gt;
&lt;li&gt;Testing in P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;li&gt;Architecture-Driven AI Development (coming soon)&lt;/li&gt;
&lt;li&gt;Adapter Switching Strategies (coming soon)&lt;/li&gt;
&lt;li&gt;Spring Modulith + P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;/ol&gt;




&lt;blockquote&gt;
&lt;p&gt;"&lt;code&gt;JPMS&lt;/code&gt; will keep the local classes in line" - Grand Moff Tarkin, former Software Engineer.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Recap: where article 2 left us
&lt;/h2&gt;

&lt;p&gt;Article 2 showed how to combine Ports &amp;amp; Adapters with Simon Brown's &lt;em&gt;package-by-component&lt;/em&gt; to organize the inside of the application. Java's &lt;code&gt;package-private&lt;/code&gt; became the default visibility level, hiding the internal logic of each use case.&lt;/p&gt;

&lt;p&gt;But we ended on an honest assessment: primary port constructors are &lt;code&gt;public&lt;/code&gt;, and so is &lt;code&gt;SnapshotBuilder&lt;/code&gt;. &lt;code&gt;package-private&lt;/code&gt; covers a good portion of the isolation, but the rest is exposed, and Java offers nothing between &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;package-private&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is where JPMS comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPMS refresher
&lt;/h2&gt;

&lt;p&gt;The Java Platform Module System (JPMS), introduced in Java 9, adds a level of isolation above packages: the module. A module explicitly declares what it exports and what it needs through a &lt;code&gt;module-info.java&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The key point is that &lt;strong&gt;a module is closed by default&lt;/strong&gt;. If a package is not explicitly exported, it is invisible from outside the module, even if the classes it contains are &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's exactly the mechanism we were missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reorganizing the &lt;code&gt;snapshot&lt;/code&gt; package
&lt;/h2&gt;

&lt;p&gt;Before setting up modules, there's a problem to fix in the application's organization. The &lt;code&gt;snapshot/&lt;/code&gt; package contains both types that need to be visible to adapters (&lt;code&gt;Snapshot&lt;/code&gt;, &lt;code&gt;Action&lt;/code&gt;) and purely internal types (&lt;code&gt;CreationPoint&lt;/code&gt;, &lt;code&gt;InvestedPoint&lt;/code&gt;, &lt;code&gt;CreationPointConsumer&lt;/code&gt;, &lt;code&gt;Recorder&lt;/code&gt;, &lt;code&gt;SnapshotBuilder&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;But JPMS exports &lt;strong&gt;packages&lt;/strong&gt;, not classes. There's no way to say "export &lt;code&gt;Snapshot&lt;/code&gt; but hide &lt;code&gt;CreationPoint&lt;/code&gt;" if they're in the same package.&lt;/p&gt;

&lt;p&gt;The solution is to separate the two categories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state/
├── CreationPoint.java          # internal - not exported
├── CreationPointConsumer.java  # internal - not exported
├── InvestedPoint.java          # internal - not exported
├── Recorder.java               # internal - not exported
├── SnapshotBuilder.java        # internal - not exported
└── snapshot/
    ├── Snapshot.java           # exposed - adapters need it
    └── Action.java             # exposed - part of the contract
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state&lt;/code&gt; describes the internal state management mechanics, while &lt;code&gt;state.snapshot&lt;/code&gt; contains what comes out of it. The &lt;code&gt;state&lt;/code&gt; package is not exported, unlike the &lt;code&gt;state.snapshot&lt;/code&gt; package, meaning internal classes disappear for the outside world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application module
&lt;/h2&gt;

&lt;p&gt;Here is the application's &lt;code&gt;module-info.java&lt;/code&gt;:&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="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Pragmatica-core&lt;/span&gt;

    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ErrorCause, ForStoringSnapshot&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ForLoadingSnapshot, ToCharacter&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;race&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;profession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;define&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;characteristic&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;race&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;profession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary port&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;race&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Vocabulary&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;profession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Vocabulary&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;characteristic&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Vocabulary&lt;/span&gt;
    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Exposed state&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What stands out is that everything not listed is &lt;strong&gt;invisible&lt;/strong&gt;: &lt;code&gt;CreationPoint&lt;/code&gt;, &lt;code&gt;InvestedPoint&lt;/code&gt;, &lt;code&gt;CreationPointConsumer&lt;/code&gt;, &lt;code&gt;Recorder&lt;/code&gt;, &lt;code&gt;SnapshotBuilder&lt;/code&gt; are &lt;code&gt;public&lt;/code&gt; in their package but no other module can access them. The compiler refuses the import.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;public&lt;/code&gt; from article 2 is no longer a master key. It means "public &lt;strong&gt;within the module&lt;/strong&gt;".&lt;/p&gt;

&lt;h3&gt;
  
  
  Why exports are not qualified
&lt;/h3&gt;

&lt;p&gt;You'll notice that no export uses the &lt;code&gt;to&lt;/code&gt; clause:&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="c1"&gt;// We do NOT do this:&lt;/span&gt;
&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bootstrap&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is directly tied to Cockburn. The application is "blissfully ignorant of the nature of the input device". If the application declares which modules it exports to, it knows its consumers. That's a form of coupling, even if it's at the module level and not at the code level.&lt;/p&gt;

&lt;p&gt;A JDBC adapter today, a MongoDB adapter tomorrow, an in-memory adapter for tests. The application doesn't know and shouldn't know who implements its secondary ports, so exports remain open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapters enter the scene
&lt;/h2&gt;

&lt;p&gt;Until now in the series, the application lived alone. With JPMS and modules, it's time to introduce what lives on the outside.&lt;/p&gt;

&lt;h3&gt;
  
  
  The primary adapter
&lt;/h3&gt;

&lt;p&gt;A primary adapter translates signals from an external actor to the application's ports. In a complete system, this would be a REST controller, a web interface, a CLI. But for this article, and following the approach Cockburn uses in &lt;em&gt;Hexagonal Architecture Explained&lt;/em&gt;, a test console adapter is sufficient:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateFullCharacter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CreateCharacter&lt;/span&gt; &lt;span class="n"&gt;createCharacter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineRace&lt;/span&gt; &lt;span class="n"&gt;defineRace&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineProfession&lt;/span&gt; &lt;span class="n"&gt;defineProfession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineCharacteristic&lt;/span&gt; &lt;span class="n"&gt;defineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;GetLastestSnapshot&lt;/span&gt; &lt;span class="n"&gt;getLastestSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;GetAllSnapshots&lt;/span&gt; &lt;span class="n"&gt;getAllSnapshots&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;characterName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Borgrim"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateFullCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;CreateCharacter&lt;/span&gt; &lt;span class="n"&gt;createCharacter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                               &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineRace&lt;/span&gt; &lt;span class="n"&gt;defineRace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                               &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineProfession&lt;/span&gt; &lt;span class="n"&gt;defineProfession&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                               &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DefineCharacteristic&lt;/span&gt; &lt;span class="n"&gt;defineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                               &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;GetLastestSnapshot&lt;/span&gt; &lt;span class="n"&gt;getLastestSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                               &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;GetAllSnapshots&lt;/span&gt; &lt;span class="n"&gt;getAllSnapshots&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createCharacter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;createCharacter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defineRace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defineRace&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defineProfession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defineProfession&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defineCharacteristic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLastestSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getLastestSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllSnapshots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getAllSnapshots&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"=== Character Creation Test ==="&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createCharacter&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"✓ Created: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CreateFullCharacter:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;logError&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defineRace&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;HIGH_ELF&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharacterNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"✓ Race: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;race&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CreateFullCharacter:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;logError&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defineProfession&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ELF_ADVENTURER&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharacterNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"✓ Profession: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;profession&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;CreateFullCharacter:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;logError&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="o"&gt;[...]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adapter only knows the primary ports. It cannot instantiate a &lt;code&gt;SnapshotBuilder&lt;/code&gt;, nor access a &lt;code&gt;CreationPoint&lt;/code&gt;, nor even touch a &lt;code&gt;Recorder&lt;/code&gt;, JPMS forbids it at compile time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ekmm0f8i6kp5bfxngz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ekmm0f8i6kp5bfxngz5.png" alt="Proof the SnapshotBuilder is not accessible" width="780" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An important point about primary adapters: they are shaped by the consumer they serve, not by the application. The idea is the same as BFFs (&lt;em&gt;Backend For Frontend&lt;/em&gt;) in microservices: an element dedicated to a front-end, developed by the team that knows that front-end. The application behind doesn't change, only the translation does.&lt;/p&gt;

&lt;p&gt;This means the front-end team can develop their adapters in parallel against the ports, using fakes, without being blocked by the application's development. As explained in article 2, the port is an API, a contract, not a synchronization point.&lt;/p&gt;

&lt;h3&gt;
  
  
  The faked secondary adapter
&lt;/h3&gt;

&lt;p&gt;For this article, the secondary adapter is an in-memory fake located inside the composition root, exactly like the &lt;code&gt;FixedTaxRateRepository&lt;/code&gt; Cockburn uses in his book: a trivial implementation that allows testing the wiring without infrastructure.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemorySnapshotStorage&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ForLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForGettingSnapshot&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TreeSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Snapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;store&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;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Snapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getLastSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;option&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;TreeSet:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Snapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Snapshot&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;computeIfAbsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&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;TreeSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;()).&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Snapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;theLastest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLastSnapshot&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="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Snapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;allOrdered&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;option&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;List:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;or&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="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;This implements the secondary port &lt;code&gt;ForStoringSnapshot&lt;/code&gt;. It sees the port interface and the &lt;code&gt;Snapshot&lt;/code&gt; type because those packages are exported, but it doesn't see &lt;code&gt;SnapshotBuilder&lt;/code&gt;, &lt;code&gt;CreationPoint&lt;/code&gt;, or anything else. It doesn't need to, and JPMS makes sure it can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Composition Root
&lt;/h2&gt;

&lt;p&gt;The composition root is the only place that sees everything: the application, the primary adapters, the secondary adapters. It's the one that wires implementations together and launches the application.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;raceStorage&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;InMemoryRaceStorage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;professionStorage&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;InMemoryProfessionStorage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;snapshotStorage&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;InMemorySnapshotStorage&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createCharacter&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;CreateCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;defineRace&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;DefineRace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raceStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;defineProfession&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;DefineProfession&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;professionStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;defineCharacteristic&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;DefineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;getLastestSnapshot&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;GetLastestSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;getAllSnapshots&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;GetAllSnapshots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshotStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;testAdapter&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;CreateFullCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;createCharacter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;defineRace&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;defineProfession&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;defineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;getLastestSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;getAllSnapshots&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;testAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Spring, no automatic injection, no annotations — the wiring is explicit, readable, and verified at compile time. The composition root is the only module with a dependency on all the others, and that's its job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Composition root &lt;code&gt;module-info.java&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bootstrap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;facade&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;core&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;h3&gt;
  
  
  Test adapter &lt;code&gt;module-info.java&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;facade&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;facade&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;test&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;Adapters export their package so the composition root can instantiate them. The application doesn't even know these modules exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: &lt;code&gt;opens ... to&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;JPMS offers an &lt;code&gt;opens&lt;/code&gt; directive that allows reflection on a package. And the temptation is real:&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="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opens&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charleshornick&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supra&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;race&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fasterxml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jackson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;databind&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;This would allow Jackson to deserialize &lt;code&gt;Race&lt;/code&gt; types through reflection. Convenient. Except the application just declared that it knows about Jackson. It's no longer "blissfully ignorant" of the technology. The boundary is broken, right in the &lt;code&gt;module-info.java&lt;/code&gt; itself.&lt;/p&gt;

&lt;p&gt;And yes, Jackson &lt;strong&gt;is&lt;/strong&gt; a technology. The choice to use Jackson over Gson belongs to the secondary adapter, not the application.&lt;/p&gt;

&lt;p&gt;The right approach is for the secondary adapter to handle deserialization. It receives JSON, builds a &lt;code&gt;Race&lt;/code&gt; through the public constructor, and passes it to the port. The application doesn't know that JSON exists somewhere. The adapter does its job as an adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;@Transactional&lt;/code&gt; does not belong in the application
&lt;/h2&gt;

&lt;p&gt;Since we're talking about adapters and the composition root, a point that comes up often in discussions: where to place transactions?&lt;/p&gt;

&lt;p&gt;Some argue that &lt;code&gt;@Transactional&lt;/code&gt; can go in the application (what they call the "domain") because &lt;code&gt;jakarta.transaction.Transactional&lt;/code&gt; is a Java standard and therefore "not technology-specific."&lt;/p&gt;

&lt;p&gt;This is wrong. &lt;code&gt;jakarta.transaction.Transactional&lt;/code&gt; presupposes a transaction manager, which presupposes transactional persistence. Cockburn's application shouldn't even know there's a database. A transaction annotation in the application means the application knows there's transactional persistence behind it.&lt;/p&gt;

&lt;p&gt;But beyond theory, there's a practical argument. A single primary port can be wired with different secondary adapters depending on the context. A JDBC adapter needs transactions. An in-memory adapter doesn't. If &lt;code&gt;@Transactional&lt;/code&gt; is in the application, it applies in both cases, forcing a dependency on a transaction manager that might not even exist in the in-memory context.&lt;/p&gt;

&lt;p&gt;The transactional decision depends on the combination of primary adapter and secondary adapter. Only the composition root knows that combination. &lt;code&gt;@Transactional&lt;/code&gt; goes on the handler in the primary adapter, or in a composition root configuration. Not in the application.&lt;/p&gt;

&lt;p&gt;The application remains "blissfully ignorant" of everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest trade-off: the &lt;code&gt;public&lt;/code&gt; constructor survives
&lt;/h2&gt;

&lt;p&gt;JPMS closes non-exported packages. The &lt;code&gt;public&lt;/code&gt; on a &lt;code&gt;CreationPoint&lt;/code&gt; or a &lt;code&gt;SnapshotBuilder&lt;/code&gt; no longer leaks to adapters, and that's the main gain.&lt;/p&gt;

&lt;p&gt;But the &lt;code&gt;public&lt;/code&gt; constructor of primary ports survives. &lt;code&gt;CreateCharacter&lt;/code&gt; is in an exported package because the primary adapter needs to see it. And as discussed above, JPMS exports packages, not classes, so the &lt;code&gt;public&lt;/code&gt; constructor remains accessible to any module that depends on the application.&lt;/p&gt;

&lt;p&gt;An adapter doing &lt;code&gt;new CreateCharacter(myFakeChecker, myFakeStorage)&lt;/code&gt; still compiles.&lt;/p&gt;

&lt;p&gt;One could separate the port and its constructor into two different packages, one exported and one not. But that would break the per-use-case cohesion from Brown, the approach we set up in article 2. The cure would be worse than the disease.&lt;/p&gt;

&lt;p&gt;Architecture guides, it doesn't prevent. No technical mechanism will replace the discipline of a team that understands the &lt;em&gt;why&lt;/em&gt; behind the structure. JPMS + Brown + &lt;code&gt;package-private&lt;/code&gt; cover nearly all of the isolation. The &lt;code&gt;public&lt;/code&gt; constructor remains an act of trust in the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPMS trade-offs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;module-info.java&lt;/code&gt; complexity
&lt;/h3&gt;

&lt;p&gt;The larger the application grows, the longer the export list gets. It's verbose but explicit, and a &lt;code&gt;module-info.java&lt;/code&gt; of 30 lines remains infinitely more readable than an ArchUnit setup with 50 rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tooling not always module-friendly
&lt;/h3&gt;

&lt;p&gt;Some libraries and tools are not yet ready for JPMS. This is less and less true with recent Java versions and major frameworks, but it's a point to check before adoption.&lt;/p&gt;

&lt;h3&gt;
  
  
  More constrained tests
&lt;/h3&gt;

&lt;p&gt;Tests must respect the same boundaries as production code. A test cannot access a non-exported package. This is a constraint that forces testing through ports, which is the right level of testing for the application. Internal details shouldn't be tested directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spring Boot and JPMS
&lt;/h3&gt;

&lt;p&gt;Historically, Spring Boot and JPMS didn't get along well. With Spring Boot 4.x and Java 25, the situation has improved considerably. But it's a topic to watch when adding real Spring adapters in the following articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Article 2 posed the problem: &lt;code&gt;package-private&lt;/code&gt; covers a good portion of the isolation but &lt;code&gt;public&lt;/code&gt; is a master key. JPMS fills the gap by adding a level of control above.&lt;/p&gt;

&lt;p&gt;Two levels of isolation, two mechanisms, complementary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPMS&lt;/strong&gt; controls what leaves the module. Non-exported packages are invisible, even if classes are &lt;code&gt;public&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package-private&lt;/code&gt;&lt;/strong&gt; controls what leaves the package. Internal classes stay invisible within the module itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Brown organizes the inside through &lt;code&gt;package-private&lt;/code&gt; which enforces at the package level. JPMS enforces at the module level. Cockburn protects the boundary between the inside and the outside.&lt;/p&gt;

&lt;p&gt;The application is now structured, isolated, and verifiable at compile time. But we haven't talked about tests yet. How do we guarantee that primary ports work correctly, that secondary adapters respect their contract, and that everything holds together without test duplication?&lt;/p&gt;

&lt;p&gt;That's the subject of article 4: testing in P&amp;amp;A.&lt;/p&gt;




&lt;p&gt;The example application evolves from article 2 with the addition of JPMS modules, a test console adapter, an in-memory adapter, and a composition root. The application itself hasn't changed, only the project structure has evolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech stack:&lt;/strong&gt; Java 25, &lt;a href="https://github.com/pragmaticalabs/pragmatica" rel="noopener noreferrer"&gt;Pragmatica&lt;/a&gt; (&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;), JUnit 6, AssertJ, Maven. No Spring in this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/charles-hornick/ports-and-adapters-beyond-the-theory/tree/article/3-jpms-isolation" rel="noopener noreferrer"&gt;GitHub - &lt;code&gt;article/3-jpms-isolation&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the "Ports &amp;amp; Adapters: Beyond the Theory" series. The series was initiated after &lt;a href="https://en.wikipedia.org/wiki/Alistair_Cockburn" rel="noopener noreferrer"&gt;Alistair Cockburn&lt;/a&gt;, creator of the Ports &amp;amp; Adapters pattern and co-author of the Agile Manifesto, shared the first article and described the approach as "an amazing use of Hexagonal Architecture."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The architectural decisions in this series are grounded in &lt;a href="https://alistair.cockburn.us/hexagonal-architecture" rel="noopener noreferrer"&gt;Cockburn's original 2005 article&lt;/a&gt; and in &lt;a href="https://www.amazon.com/Hexagonal-Architecture-Explained-architecture-simplifies/dp/B0F5QSH28F" rel="noopener noreferrer"&gt;Hexagonal Architecture Explained&lt;/a&gt; (Cockburn &amp;amp; Garrido de Paz, updated 1st edition, 2025).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>designpatterns</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Ports &amp; Adapters: Beyond the Theory - Organizing the Application with Package-by-Component</title>
      <dc:creator>Charles Hornick</dc:creator>
      <pubDate>Wed, 18 Mar 2026 23:02:38 +0000</pubDate>
      <link>https://dev.to/charleshornick/ports-adapters-beyond-the-theory-organizing-the-application-with-package-by-component-49m3</link>
      <guid>https://dev.to/charleshornick/ports-adapters-beyond-the-theory-organizing-the-application-with-package-by-component-49m3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the second article in the "Ports &amp;amp; Adapters: Beyond the Theory" series.&lt;br&gt;
Article 1 demonstrated the ability to hot-swap adapters at runtime by leveraging the advantages of the P&amp;amp;A pattern.&lt;br&gt;
This article tackles what Alistair Cockburn deliberately left open: how to organize the inside of the application.&lt;br&gt;
Every line of code in the companion repository was written by hand. No AI-generated code.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/charles-hornick/ports-and-adapters-beyond-the-theory/tree/article/2-package-by-component" rel="noopener noreferrer"&gt;GitHub — branch &lt;code&gt;article/2-package-by-component&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;Series — Ports &amp;amp; Adapters: Beyond the Theory&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/runtime-adapter-hot-swapping-with-ports-adapters-the-pattern-alistair-cockburn-didnt-document-56cg"&gt;Runtime Adapter Hot-Swapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-organizing-the-application-with-package-by-component-49m3"&gt;Organizing the Application with Package-by-Component&lt;/a&gt; ← you are here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-isolating-the-application-with-jpms-2gda"&gt;Isolating with JPMS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;No Exceptions: Result&amp;lt;T&amp;gt; with Pragmatica (coming soon)&lt;/li&gt;
&lt;li&gt;Testing in P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;li&gt;Architecture-Driven AI Development (coming soon)&lt;/li&gt;
&lt;li&gt;Adapter Switching Strategies (coming soon)&lt;/li&gt;
&lt;li&gt;Spring Modulith + P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;/ol&gt;




&lt;blockquote&gt;
&lt;p&gt;"I need to be assured that certain interests are &lt;code&gt;package-private&lt;/code&gt;" — Arthur Case, talking about the application.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The application is not protected
&lt;/h2&gt;

&lt;p&gt;Let's be clear, Alistair Cockburn's Ports &amp;amp; Adapters pattern can be summed up with the following points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The system is split into two parts: the inside (the application) and the outside (the adapters);&lt;/li&gt;
&lt;li&gt;The application focuses solely on business needs;&lt;/li&gt;
&lt;li&gt;The application communicates with the outside world through ports;&lt;/li&gt;
&lt;li&gt;Adapters translate and communicate with the application through these ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There you go, as simple as it sounds.&lt;/p&gt;

&lt;p&gt;Imagine a laptop that only has USB-C ports. You want to connect your wired mouse but it's USB-A.&lt;br&gt;
To allow communication, you need a USB-A to USB-C adapter.&lt;br&gt;
Now your mouse click, going through the adapter, is understood by the laptop because the connection has been established.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This analogy is much harder to make with the term "hexagonal architecture")&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, while P&amp;amp;A guarantees an effective separation between the inside and the outside, it absolutely does not guarantee that the inside is properly organized.&lt;/p&gt;

&lt;p&gt;But what do we see in tutorials and blog posts? That the organization is almost always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;domain/
├── model/
├── port/
│   ├── in/
│   └── out/
└── service/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with such an approach, two things stand out immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;domain&lt;/code&gt; is used even though it's a DDD (&lt;em&gt;Domain-Driven Design&lt;/em&gt;) term, and is never used by Alistair in his writings, where he uses the term &lt;em&gt;Application&lt;/em&gt;;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;The application is everything on the inside, everything that isn't technology-specific.&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;everything ends up &lt;code&gt;public&lt;/code&gt; so that each element can access whatever it needs to function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this last point is the most problematic. The whole purpose of having an inside and an outside is that what's on the inside, and has no reason to leak out, &lt;strong&gt;stays there&lt;/strong&gt;. Yet with this keyword, everything that should remain internal becomes accessible to any adapter.&lt;/p&gt;

&lt;p&gt;Now, if article 1 showed one thing, it's that it is possible to swap an adapter on the fly with zero impact on the application, simply because it doesn't know and doesn't care.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;Remember, in Ports &amp;amp; Adapters you are free to organize the inside of the app in any way you like, and the things outside the app in any way you like. Just put ports in place.&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Right, just put ports in place. But what are they exactly?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are two types of ports that are important to explain in order to properly understand their role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Primary ports
&lt;/h3&gt;

&lt;p&gt;These ports represent the way the application wants to be talked to, meaning that any adapter talking to a primary port becomes by definition a primary adapter and follows a precise protocol.&lt;/p&gt;

&lt;p&gt;As Alistair puts it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;The protocol for a port is given by the purpose of the conversation between the two devices. The protocol takes the form of an application program interface (API).&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the case of primary ports, this protocol represents the action that the adapter wants to see executed when it uses the application.&lt;/p&gt;

&lt;p&gt;Except that when we look at articles, conference talks, and tutorials, this protocol often boils down to a Java &lt;code&gt;interface&lt;/code&gt;. And worse, that interface tends to become a catch-all.&lt;/p&gt;

&lt;p&gt;Yet in the updated edition of &lt;em&gt;Hexagonal Architecture Explained (2025)&lt;/em&gt;, Alistair explains that a port can be either an interface or a class.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;strong&gt;In your life, decide which way you prefer to write.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this implementation, primary ports are concrete &lt;code&gt;final&lt;/code&gt; classes that represent the API, and offer a protocol that makes the conversation between the adapter and the application explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secondary ports
&lt;/h3&gt;

&lt;p&gt;On their side, secondary ports remain interfaces in the Java sense.&lt;/p&gt;

&lt;p&gt;The reason is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;A port identifies a purposeful conversation. There will typically be multiple adapters for any one port, for various technologies that may plug into that port.&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The application sets up a conversation with an adapter it knows nothing about. If the application used anything other than an interface, it would mean that it becomes part of the implementation, breaking the boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about the rest of the application?
&lt;/h3&gt;

&lt;p&gt;While ports are perfectly justified in being &lt;code&gt;public&lt;/code&gt;, the same is not true for all the other elements that make up the application.&lt;/p&gt;

&lt;p&gt;This brings us back to the point raised earlier: primary and secondary adapters can only use what the application allows them to use, for the sake of the conversations/protocols.&lt;/p&gt;

&lt;p&gt;Except that this aspect is only reinforced if adapters rely solely on the defined conversation, not on the hallway rumors that an unfortunate keyword made &lt;code&gt;public&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But how do we protect the inside?&lt;/p&gt;

&lt;h2&gt;
  
  
  This is where Simon Brown enters the scene
&lt;/h2&gt;

&lt;p&gt;In "The Missing Chapter" of the book "Clean Architecture", Simon Brown offers a different approach, in opposition to the layered approach as well as, indeed, Ports &amp;amp; Adapters.&lt;/p&gt;

&lt;p&gt;But what if, instead of opposing them, the application used both in a complementary way, P&amp;amp;A protecting from adapters, Brown protecting from itself?&lt;/p&gt;

&lt;p&gt;Where P&amp;amp;A provides the boundaries protecting the application, &lt;em&gt;package-by-component&lt;/em&gt; provides the internal organization it lacks.&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of organizing code by technical layer (&lt;code&gt;model/&lt;/code&gt;, &lt;code&gt;port/&lt;/code&gt;, &lt;code&gt;service/&lt;/code&gt;), code is organized by use case and component. Java's &lt;code&gt;package-private&lt;/code&gt; becomes the default visibility level: &lt;strong&gt;what has no reason to be &lt;code&gt;public&lt;/code&gt; is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the example code, the application itself is the component, turning &lt;em&gt;package-by-component&lt;/em&gt; into &lt;em&gt;package-by-use-case&lt;/em&gt;: the same idea applied at the right level of granularity.&lt;/p&gt;

&lt;p&gt;Here is the application's organization, with brief explanations of each element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;be.charleshornick.supra
├── ErrorCause.java                         # cross-cutting error constants
├── ForStoringSnapshot.java                 # secondary port, shared across use cases
│
├── create/                               # use case: create a character
│   ├── CreateCharacter.java                # primary port (public final)
│   ├── ForCheckingNameUnicity.java         # secondary port (specific)
│   ├── Character.java                      # package-private
│   └── CharacterNameValidator.java         # package-private
│
├── define/                                 # use cases: modify an existing character
│   ├── ForLoadingSnapshot.java             # secondary port, shared within define/
│   ├── ToCharacter.java                    # step interface, shared within define/
│   ├── race/
│   │   ├── DefineRace.java                 # primary port (public final)
│   │   ├── ForLoadingRace.java             # secondary port (specific)
│   │   ├── DefineRaceStep.java             # package-private
│   │   └── Character.java                  # package-private
│   ├── profession/
│   │   ├── DefineProfession.java           # primary port (public final)
│   │   ├── ForLoadingProfession.java       # secondary port (specific)
│   │   ├── DefineProfessionStep.java       # package-private
│   │   └── Character.java                  # package-private
│   └── characteristic/
│       ├── DefineCharacteristic.java        # primary port (public final)
│       ├── AddOnePoint.java                 # package-private (step builder)
│       ├── RemoveOnePoint.java              # package-private (step builder)
│       └── Character.java                   # package-private
│
├── retrieve/                                # use cases: read data
│   ├── race/
│   │   ├── ForGettingRaces.java             # secondary port (specific)
│   │   ├── GetAllRaces.java                 # primary port (public final)
│   ├── profession/
│   │   ├── ForGettingProfessions.java       # secondary port (specific)
│   │   ├── GetAllProfessions.java           # primary port (public final)
│   └── snapshot/
│       ├── ForGettingSnapshot.java          # secondary port (specific)
│       ├── GetAllSnapshots.java             # primary port (public final)
│       └── GetLatestSnapshot.java           # primary port (public final)
│
├── race/                                    # vocabulary: what a race IS
│   ├── Race.java
│   └── RaceName.java
├── profession/                              # vocabulary: what a profession IS
│   ├── Profession.java
│   ├── ProfessionName.java
│   ├── ProfessionType.java
│   └── Prerequisite.java
├── characteristic/                          # vocabulary: what a characteristic IS
│   ├── PrimaryCharacteristic.java
│   └── PrimaryCharacteristicName.java
└── snapshot/                                # state management internals
  ├── Snapshot.java
  ├── SnapshotBuilder.java
  ├── Action.java
  ├── Recorder.java
  ├── CreationPoint.java
  ├── CreationPointConsumer.java
  └── InvestedPoint.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two categories of packages are visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;actions&lt;/strong&gt; (&lt;code&gt;create/&lt;/code&gt;, &lt;code&gt;define/&lt;/code&gt;, &lt;code&gt;retrieve/&lt;/code&gt;) describing what can be done;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;vocabulary&lt;/strong&gt; (&lt;code&gt;race/&lt;/code&gt;, &lt;code&gt;profession/&lt;/code&gt;, &lt;code&gt;characteristic/&lt;/code&gt;, &lt;code&gt;snapshot/&lt;/code&gt;) describing what things are.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first contains all the business logic, hidden behind &lt;code&gt;package-private&lt;/code&gt;, while the second contains the shared types that define the application's language.&lt;/p&gt;

&lt;p&gt;This isolation by use case keeps the risk of side effects as low as possible, since each internal element serves only its own case.&lt;br&gt;
This fits within the principle of code cohesion: what changes together lives together. Modifying a use case touches only a single package.&lt;/p&gt;

&lt;p&gt;Let's take a concrete example of what this isolation brings: the &lt;code&gt;Character&lt;/code&gt; class is present in all 3 sub-packages of &lt;code&gt;define/&lt;/code&gt; but its role is different each time.&lt;/p&gt;

&lt;p&gt;Following the organization model mentioned earlier, a single class would have existed in &lt;code&gt;models/&lt;/code&gt; and would have exposed as many methods as there are use cases.&lt;/p&gt;

&lt;p&gt;There is nothing wrong with this, but modifying that class for one use case risks breaking the others. Brown's approach makes that impossible.&lt;/p&gt;

&lt;p&gt;Now that the structure is in place, let's look at what's inside, starting with what stands out in the tree: &lt;strong&gt;the port names&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Port naming
&lt;/h3&gt;

&lt;p&gt;As mentioned earlier, Alistair explains that ports have precise conversations with adapters.&lt;/p&gt;

&lt;p&gt;A secondary port represents the &lt;strong&gt;request&lt;/strong&gt; that the use case makes to the outside world. To reflect this, I followed the naming convention promoted by Cockburn: &lt;code&gt;ForDoingSomething&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each secondary port starts a conversation with a specific need, an action it wants to see carried out. These ports are however not placed in a fixed package but as close as possible to where they are used.&lt;/p&gt;

&lt;p&gt;For instance, ForCheckingNameUnicity is in the character creation package because that's the only place where it's used. Conversely, ForLoadingSnapshot is at the root of the &lt;code&gt;define/&lt;/code&gt; action because it's used by all of its sub-packages.&lt;/p&gt;

&lt;p&gt;Secondary ports are functional interfaces to allow finer granularity in conversations. These secondary ports do not, and cannot, have &lt;code&gt;default&lt;/code&gt; methods for the reasons discussed earlier.&lt;/p&gt;

&lt;p&gt;On their side, primary ports don't request, they &lt;strong&gt;are&lt;/strong&gt; the actions themselves, and expose them through protocols. Their naming reflects this: each step in the protocol is explicit in order to form a clear protocol.&lt;/p&gt;

&lt;p&gt;But naming is not the only reason for setting up protocols this way, this writing also guarantees at compile time that the port is correctly called.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Step Builder reinforces the protocol
&lt;/h3&gt;

&lt;p&gt;Let's take the following example:&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="n"&gt;defineCharacteristic&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;byAddingOnePoint&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharacteristicNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CharacteristicName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COURAGE&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharacterNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Borgrim"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step refines the protocol by making it more precise about the action it will perform. The action reads naturally: define a characteristic by adding one point to the one named courage, on the character named Borgrim.&lt;/p&gt;

&lt;p&gt;But most importantly, it is impossible to call it in a different order, the compiler will automatically block anything that is not explicitly allowed by the protocol.&lt;/p&gt;

&lt;p&gt;To guarantee this, the Step Builder pattern is used. Looking at DefineCharacteristic, we can see that two methods are exposed: &lt;code&gt;byAddingOnePoint&lt;/code&gt; and &lt;code&gt;byRemovingOnePoint&lt;/code&gt;, each returning a new instance of a &lt;code&gt;package-private&lt;/code&gt; class implementing &lt;code&gt;DefineCharacteristic.ToCharacteristic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since the return type of these methods is &lt;code&gt;DefineCharacteristic.ToCharacteristic&lt;/code&gt;, the next available method is necessarily &lt;code&gt;toCharacteristicNamed(String)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Furthermore, that same method returns the &lt;code&gt;ToCharacter&lt;/code&gt; interface, which the same &lt;code&gt;package-private&lt;/code&gt; classes implement, completing the protocol without ever allowing to bypass the protocol's defined behavior.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DefineCharacteristic&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ToCharacteristic&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ToCharacter&lt;/span&gt; &lt;span class="nf"&gt;toCharacteristicNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PrimaryCharacteristicName&lt;/span&gt; &lt;span class="n"&gt;characterName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForLoadingSnapshot&lt;/span&gt; &lt;span class="n"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForStoringSnapshot&lt;/span&gt; &lt;span class="n"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DefineCharacteristic&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForLoadingSnapshot&lt;/span&gt; &lt;span class="n"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForStoringSnapshot&lt;/span&gt; &lt;span class="n"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forLoadingSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forStoringSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ToCharacteristic&lt;/span&gt; &lt;span class="nf"&gt;byAddingOnePoint&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AddOnePoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ToCharacteristic&lt;/span&gt; &lt;span class="nf"&gt;byRemovingOnePoint&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RemoveOnePoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach goes further than simply enabling protocol-like writing: the port &lt;strong&gt;is singleton and stateless&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Indeed, each call to the exposed methods creates a new ephemeral step instance, making the pattern &lt;em&gt;thread-safe&lt;/em&gt;. Since the instance only lives for the duration of the call, it is immediately collected by the garbage collector once the call ends.&lt;/p&gt;

&lt;p&gt;Note that the Step Builder Pattern is not systematic.&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="n"&gt;createCharacter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Borgrim"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the method exposed by the port is sufficient for the protocol. The pattern serves the application, not the other way around.&lt;/p&gt;

&lt;h3&gt;
  
  
  A note on &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;While going through the code, you will have noticed that ports systematically return &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;, and that tests use &lt;code&gt;.onSuccess()&lt;/code&gt; and &lt;code&gt;.onFailure()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href="https://github.com/pragmaticalabs/pragmatica" rel="noopener noreferrer"&gt;Pragmatica&lt;/a&gt; library (Apache 2.0, Java 25+) to never throw exceptions in the application. This choice considerably simplifies error handling and testing, but it's a rich enough topic to deserve its own article in this series.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testability
&lt;/h3&gt;

&lt;p&gt;One of the direct benefits of this organization is testability. The application is pure POJO, no framework is needed to test it.&lt;/p&gt;

&lt;p&gt;Since secondary ports are functional interfaces, a lambda is all it takes to create a fake:&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;@Test&lt;/span&gt;
&lt;span class="nd"&gt;@DisplayName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Succeed when the name is available"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;succeedWhenNameIsFreeToUse&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CreateCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Result:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Borgrim"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;assertThat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;isEqualTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SnapshotFixture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultOne&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot create new character: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&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;&lt;code&gt;_ -&amp;gt; true&lt;/code&gt; means the name is available. &lt;code&gt;Result::ok&lt;/code&gt; means storage always succeeds. No Mockito, no mock framework, no &lt;code&gt;when().thenReturn()&lt;/code&gt;.&lt;br&gt;
The behavior is explicit, readable, and verified at compile time: if a port's signature changes, the lambda breaks, whereas a Mockito mock can keep compiling silently.&lt;/p&gt;

&lt;p&gt;The Step Builder is tested the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefineRace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forLoadingSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forLoadingRace&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;named&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RaceName&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ELF&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharacterNamed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Borgrim"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;assertEqualsAgainst&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to define new race: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No setup, no teardown, no Spring context to load. We instantiate the port with fakes, call the protocol, verify the result. The test reads like a specification.&lt;/p&gt;

&lt;p&gt;The complete testing approach, including sharing scenarios between unit tests and integration tests, will be detailed in article 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest trade-off: &lt;code&gt;public&lt;/code&gt; betrays us
&lt;/h2&gt;

&lt;p&gt;Everything shown so far works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;package-private&lt;/code&gt; hides internal elements;&lt;/li&gt;
&lt;li&gt;the Step Builder enforces the protocol;&lt;/li&gt;
&lt;li&gt;ports are &lt;code&gt;final&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;no framework infiltrates the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is a hole because primary port constructors are &lt;code&gt;public&lt;/code&gt;:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateCharacter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForCheckingNameUnicity&lt;/span&gt; &lt;span class="n"&gt;forCheckingNameUnicity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ForStoringSnapshot&lt;/span&gt; &lt;span class="n"&gt;forStoringSnapshot&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is intentional, it has to be. A &lt;em&gt;composition root&lt;/em&gt;, which will live in another package and in another module, will need to instantiate this class and inject the secondary port implementations. Without a &lt;code&gt;public&lt;/code&gt; constructor, that's not possible.&lt;/p&gt;

&lt;p&gt;Except that also means any adapter can bypass the intended wiring and do:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rogue&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;CreateCharacter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myOwnChecker&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;myOwnStorage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same problem exists for &lt;code&gt;SnapshotBuilder&lt;/code&gt;. It is &lt;code&gt;public&lt;/code&gt; because the &lt;code&gt;Character&lt;/code&gt; classes, which are &lt;code&gt;package-private&lt;/code&gt; in each sub-package of &lt;code&gt;define/&lt;/code&gt;, need to call it from their &lt;code&gt;doSnapshot()&lt;/code&gt; methods.&lt;br&gt;
Different packages, so &lt;code&gt;public&lt;/code&gt; is mandatory. And yet, no adapter has any legitimate reason to touch &lt;code&gt;SnapshotBuilder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Java's visibility model gives us two options: &lt;code&gt;public&lt;/code&gt; (everyone sees it) and &lt;code&gt;package-private&lt;/code&gt; (only the same package sees it), there is nothing in between. No way to say "this constructor is visible only to that module" or "this class is only accessible from within the application."&lt;/p&gt;

&lt;p&gt;Brown's approach with &lt;code&gt;package-private&lt;/code&gt; covers a good portion of the isolation we need. The rest, such as &lt;code&gt;public&lt;/code&gt; constructors and shared utility classes, remain exposed.&lt;/p&gt;

&lt;p&gt;This is a limitation of the language, not of the pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;No approach is perfect, and organizing by use case with Ports &amp;amp; Adapters comes with its own set of trade-offs to be aware of.&lt;/p&gt;

&lt;h4&gt;
  
  
  The number of files increases
&lt;/h4&gt;

&lt;p&gt;Each use case has its own package with its own &lt;code&gt;Character&lt;/code&gt; class, its own step builder, its own ports. Navigating the IDE requires familiarity with the structure to find things quickly.&lt;/p&gt;

&lt;h4&gt;
  
  
  The learning curve exists
&lt;/h4&gt;

&lt;p&gt;Developers used to &lt;code&gt;service/&lt;/code&gt; and &lt;code&gt;repository/&lt;/code&gt; packages need to shift their mental model. The separation between actions and vocabulary is not intuitive for everyone at first.&lt;/p&gt;

&lt;h4&gt;
  
  
  Risk of application rigidity
&lt;/h4&gt;

&lt;p&gt;Applied indiscriminately, the approach can become rigid. A trivial use case, like a simple read that passes straight through to the secondary port, does not necessarily justify a dedicated package with four classes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Risk of duplication
&lt;/h4&gt;

&lt;p&gt;Two use cases that would need the same internal type must either extract it into a vocabulary package, or accept code duplication.&lt;/p&gt;

&lt;p&gt;These trade-offs are real and should be weighed carefully. But they should be balanced against the alternative: a &lt;code&gt;domain/&lt;/code&gt; package where everything is &lt;code&gt;public&lt;/code&gt;, where nothing is enforced, and where the isolation promise of Ports &amp;amp; Adapters relies solely on the team's good will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cockburn protects the application from the outside world. Brown protects the application from itself. Together, they offer an application that is structured, isolated, and testable without any framework.&lt;/p&gt;

&lt;p&gt;But Java leaves us a hole: the &lt;code&gt;public&lt;/code&gt; keyword is a master key that cannot be revoked at the package level. Internal classes that should only be used within the application are accessible to everyone.&lt;/p&gt;

&lt;p&gt;We need an enforcement mechanism beyond &lt;code&gt;package-private&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the subject of article 3: &lt;strong&gt;JPMS&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;The example application implements an RPG character creation system with real business rules: creation, race and profession definition with bidirectional constraints, and point investment in primary characteristics with race-dependent limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech stack:&lt;/strong&gt; Java 25, &lt;a href="https://github.com/pragmaticalabs/pragmatica" rel="noopener noreferrer"&gt;Pragmatica&lt;/a&gt; (&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;), JUnit 6, AssertJ, Maven. No Spring. No framework. No annotations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source code:&lt;/strong&gt; &lt;a href="https://github.com/charles-hornick/ports-and-adapters-beyond-the-theory/tree/article/2-package-by-component" rel="noopener noreferrer"&gt;GitHub — &lt;code&gt;article/2-package-by-component&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article is part of the "Ports &amp;amp; Adapters: Beyond the Theory" series. The series was initiated after &lt;a href="https://en.wikipedia.org/wiki/Alistair_Cockburn" rel="noopener noreferrer"&gt;Alistair Cockburn&lt;/a&gt;, creator of the Ports &amp;amp; Adapters pattern and co-author of the Agile Manifesto, shared the first article and described the approach as "an amazing use of Hexagonal Architecture."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The architectural decisions in this series are grounded in &lt;a href="https://alistair.cockburn.us/hexagonal-architecture" rel="noopener noreferrer"&gt;Cockburn's original 2005 article&lt;/a&gt; and in &lt;a href="https://www.amazon.com/Hexagonal-Architecture-Explained-architecture-simplifies/dp/B0F5QSH28F" rel="noopener noreferrer"&gt;Hexagonal Architecture Explained&lt;/a&gt; (Cockburn &amp;amp; Garrido de Paz, updated 1st edition, 2025).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>architecture</category>
      <category>designpatterns</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Runtime Adapter Hot-Swapping with Ports &amp; Adapters - The Pattern Alistair Cockburn Didn't Document</title>
      <dc:creator>Charles Hornick</dc:creator>
      <pubDate>Thu, 12 Feb 2026 21:07:44 +0000</pubDate>
      <link>https://dev.to/charleshornick/runtime-adapter-hot-swapping-with-ports-adapters-the-pattern-alistair-cockburn-didnt-document-56cg</link>
      <guid>https://dev.to/charleshornick/runtime-adapter-hot-swapping-with-ports-adapters-the-pattern-alistair-cockburn-didnt-document-56cg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Series — Ports &amp;amp; Adapters: Beyond the Theory&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/charleshornick/runtime-adapter-hot-swapping-with-ports-adapters-the-pattern-alistair-cockburn-didnt-document-56cg"&gt;Runtime Adapter Hot-Swapping&lt;/a&gt; ← you are here&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-organizing-the-application-with-package-by-component-49m3"&gt;Organizing the Application with Package-by-Component&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/charleshornick/ports-adapters-beyond-the-theory-isolating-the-application-with-jpms-2gda"&gt;Isolating with JPMS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;No Exceptions: Result&amp;lt;T&amp;gt; with Pragmatica (coming soon)&lt;/li&gt;
&lt;li&gt;Testing in P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;li&gt;Architecture-Driven AI Development (coming soon)&lt;/li&gt;
&lt;li&gt;Adapter Switching Strategies (coming soon)&lt;/li&gt;
&lt;li&gt;Spring Modulith + P&amp;amp;A (coming soon)&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;When Alistair Cockburn described the Hexagonal Architecture in 2005, he documented adapter interchangeability as a core property of the pattern. In Chapter 5 of his work, he writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For each port, there may be multiple adapters, for various technologies that may plug into that port."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the original pattern leaves something open: what happens when you need to switch adapters at runtime, across a distributed system, in response to a failure, and before other instances encounter the same problem?&lt;/p&gt;

&lt;p&gt;This article presents a concrete implementation of emergency adapter hot-swapping using Spring Boot 4, Spring Cloud Bus, and Resilience4j. When one service instance detects a failing adapter, it broadcasts the failure via a message bus. All other instances switch to a fallback adapter before they encounter the timeout themselves.&lt;/p&gt;

&lt;p&gt;Cockburn's reaction when I explained the concept on LinkedIn:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Fabulous! I got around to documenting live swaps for long-running systems, but didn't dare think about emergency hot-swapping — thank you!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Consider a standard Ports &amp;amp; Adapters setup where your domain service talks to an upstream API through an adapter. When that API starts timing out, every instance of your service will independently discover the failure, each burning through its own timeout window.&lt;/p&gt;

&lt;p&gt;With 10 instances and a 3-second timeout, you're looking at 30 seconds of cumulative degraded experience across your cluster, at minimum. In practice, retries, thread pool saturation, and cascading failures make this much worse.&lt;/p&gt;

&lt;p&gt;The standard circuit breaker pattern (Hystrix, Resilience4j) solves this per instance: each service independently opens its circuit after detecting failures. But there's no coordination. Instance 7 doesn't know that instance 1 already hit the timeout 2 seconds ago.&lt;/p&gt;

&lt;p&gt;The solution: &lt;strong&gt;broadcast-driven adapter switching&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;the first instance to detect a failure should warn all others&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instance 1: timeout detected → broadcast "switch to fallback"
Instance 2: receives event → switches adapter (never hits the timeout)
Instance 3: receives event → switches adapter (never hits the timeout)
...
Instance N: receives event → switches adapter (never hits the timeout)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is eventual consistency applied to infrastructure. The adapter state propagates across the cluster asynchronously, and every instance converges to the same adapter within milliseconds of the first detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Full source code: &lt;a href="https://github.com/charles-hornick/adapter-hotswap-spring" rel="noopener noreferrer"&gt;github.com/charles-hornick/adapter-hotswap-spring&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Port
&lt;/h3&gt;

&lt;p&gt;Nothing special here, just a standard port interface:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AnimalPort&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getAnimal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;name&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;h3&gt;
  
  
  The Adapter Registry
&lt;/h3&gt;

&lt;p&gt;The critical piece. An AtomicReference holds the active adapter, making the swap lock-free and thread-safe:&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;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdapterConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AnimalPort&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;adapters&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AtomicReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnimalPort&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;activeAdapter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AdapterConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@Qualifier&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"primaryAdapter"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AnimalPort&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Qualifier&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fallbackAdapter"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AnimalPort&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;adapters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fallback"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;activeAdapter&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;AtomicReference&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AnimalPort&lt;/span&gt; &lt;span class="nf"&gt;getActiveAdapter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;activeAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;switchTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;adapterName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapterName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAndSet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapterName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The swap is a single atomic operation. Threads currently executing a request through the old adapter will finish their call; new requests immediately use the new adapter. No locks, no synchronized blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout detection + broadcast
&lt;/h3&gt;

&lt;p&gt;The service layer wraps adapter calls with Resilience4j's TimeLimiter. On timeout, it publishes a custom Spring Cloud Bus event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnimalService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AdapterConfig&lt;/span&gt; &lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ApplicationEventPublisher&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BusProperties&lt;/span&gt; &lt;span class="n"&gt;busProperties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;TimeLimiter&lt;/span&gt; &lt;span class="n"&gt;timeLimiter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ExecutorService&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AnimalService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AdapterConfig&lt;/span&gt; &lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                         &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ApplicationEventPublisher&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                         &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BusProperties&lt;/span&gt; &lt;span class="n"&gt;busProperties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;adapterConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publisher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;busProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;busProperties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TimeLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;TimeLimiterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeoutDuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cancelRunningFuture&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getAnimal&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActiveAdapter&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;adapter:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAnimal&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;timeLimiter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeFutureSupplier&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// First instance to detect failure → broadcast to all&lt;/span&gt;
            &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishEvent&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;AdapterSwitchEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;busProperties&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="s"&gt;"**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fallback"&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;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActiveAdapter&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAnimal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Virtual threads&lt;/strong&gt; (Executors.newVirtualThreadPerTaskExecutor()) - Java 25's virtual threads keep the timeout wrapper lightweight. No platform thread blocking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"**" destination&lt;/strong&gt; - the bus event targets all services, not a specific instance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Immediate local fallback&lt;/strong&gt; - after broadcasting, the current request falls back locally without waiting for the bus round-trip.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bus Event
&lt;/h2&gt;

&lt;p&gt;A custom RemoteApplicationEvent that carries the target adapter name:&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdapterSwitchEvent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;RemoteApplicationEvent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;targetAdapter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AdapterSwitchEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;originService&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;destinationService&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                              &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;targetAdapter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;super&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="kc"&gt;null&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;originService&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;destinationService&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;destinationService&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"**"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;targetAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;targetAdapter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// getter&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Cloud Bus serializes this to JSON, pushes it to RabbitMQ, and every connected instance receives it. The listener on each instance performs the swap:&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;@EventListener&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onAdapterSwitch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AdapterSwitchEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;switchTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTargetAdapter&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;h2&gt;
  
  
  Recovery detection
&lt;/h2&gt;

&lt;p&gt;A scheduled health checker pings the primary service and broadcasts a switch-back when it recovers:&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;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixedDelayString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${health.check.interval:5000}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkPrimaryHealth&lt;/span&gt;&lt;span class="o"&gt;()&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="s"&gt;"primary"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adapterConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActiveAdapterName&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already on primary, nothing to check&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;restClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&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="s"&gt;"UP"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishEvent&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;AdapterSwitchEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;busProperties&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="s"&gt;"**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"primary"&lt;/span&gt;
            &lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// still down, do nothing&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;h2&gt;
  
  
  Running the demo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bashgit clone https://github.com/charles-hornick/adapter-hotswap-spring.git
&lt;span class="nb"&gt;cd &lt;/span&gt;adapter-hotswap-spring
docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo runs two instances of the hotswap service, a flaky primary service (cyclic timeouts), and a stable fallback. Watch the logs for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response: Chien          ← primary adapter
Response: Chien
EVENT: Primary adapter timeout — broadcasting switch to fallback
EVENT: Switching adapter from 'primary' to 'fallback'
(instance 2) EVENT RECEIVED: Switch to 'fallback'
Response: Chat            ← fallback adapter
HEALTHCHECK: Primary adapter responding again
EVENT: Switching adapter from 'fallback' to 'primary'
Response: Chien          ← back to primary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instance 2 switches &lt;strong&gt;without ever experiencing the timeout.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and trade-offs
&lt;/h2&gt;

&lt;p&gt;This is a demo, not a production framework. Real-world considerations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Split-brain risk&lt;/strong&gt;. If RabbitMQ is partitioned, instances may diverge on which adapter is active.&lt;br&gt;
Mitigation: use a consensus mechanism or accept eventual convergence via the health checker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thundering herd on recovery&lt;/strong&gt;. When the health checker broadcasts "switch back to primary", all instances hit the primary simultaneously.&lt;br&gt;
Mitigation: add jitter to the health check interval, or use a canary approach where only one instance switches back first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single point of decision&lt;/strong&gt;. Whichever instance first detects the failure makes the decision for the entire cluster. If that detection is a false positive (network blip), the whole cluster switches unnecessarily.&lt;br&gt;
Mitigation: require N consecutive failures before broadcasting, or use a quorum-based decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bus latency&lt;/strong&gt;. There's a window between the broadcast and reception where other instances may still hit the failing adapter. This is inherent to eventual consistency. For most use cases, the RabbitMQ propagation delay (typically &amp;lt;100ms) is negligible compared to a 3-second timeout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No adapter state persistence&lt;/strong&gt;. If an instance restarts, it defaults back to the primary adapter regardless of the cluster state.&lt;br&gt;
Mitigation: persist the adapter state in a shared store (Redis, database) or replay the latest bus event on startup.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use this
&lt;/h2&gt;

&lt;p&gt;This pattern is most valuable when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have &lt;strong&gt;multiple instances&lt;/strong&gt; of the same service&lt;/li&gt;
&lt;li&gt;Adapter failures are &lt;strong&gt;detectable&lt;/strong&gt; (timeout, HTTP 5xx, connection refused)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;cost of independent failure detection&lt;/strong&gt; across instances is significant&lt;/li&gt;
&lt;li&gt;You have a &lt;strong&gt;viable fallback&lt;/strong&gt; adapter (degraded service, cache, alternative provider)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's overkill for single-instance deployments or when failures are so rare that independent circuit breakers suffice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relation to Cockburn's original work
&lt;/h2&gt;

&lt;p&gt;The Hexagonal Architecture describes adapters as &lt;strong&gt;interchangeable&lt;/strong&gt;, so you can swap a database adapter for an in-memory adapter, or a REST adapter for a gRPC adapter. But the original pattern is silent on when and how this swap happens at runtime.&lt;/p&gt;

&lt;p&gt;This implementation extends the pattern in two directions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime swapping&lt;/strong&gt; - adapters switch while the system is running, without restart or redeployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluster-wide coordination&lt;/strong&gt; - the swap propagates across all instances, turning a local failure detection into a global infrastructure decision.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The adapter remains a first-class architectural concept. We're just giving it operational capabilities that the original pattern implied but never specified.&lt;/p&gt;




&lt;p&gt;Source code: &lt;a href="https://github.com/charles-hornick/adapter-hotswap-spring" rel="noopener noreferrer"&gt;github.com/charles-hornick/adapter-hotswap-spring&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Stack: Java 25, Spring Boot 4.0.2, Spring Cloud 2025.1.0, Resilience4j 2.3.0, RabbitMQ&lt;/p&gt;

&lt;p&gt;Charles Hornick - Java consultant specializing in smart refactoring and legacy application rescue. &lt;a href="https://charles-hornick.be" rel="noopener noreferrer"&gt;charles-hornick.be&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>hexagonal</category>
    </item>
  </channel>
</rss>
