<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://nicolasduminil.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://nicolasduminil.github.io/" rel="alternate" type="text/html" /><updated>2026-08-05T15:47:53+00:00</updated><id>https://nicolasduminil.github.io/feed.xml</id><title type="html">Simplex Software</title><subtitle>Senior Silver Software Architect</subtitle><author><name>Nicolas DUMINIL</name></author><entry><title type="html">Releasing Spring Boot Applications on Kubernetes</title><link href="https://nicolasduminil.github.io/posts-archive/sbk8s/" rel="alternate" type="text/html" title="Releasing Spring Boot Applications on Kubernetes" /><published>2026-08-05T00:00:00+00:00</published><updated>2026-08-05T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/sbk8s</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/sbk8s/"><![CDATA[<p>This project demonstrates how to deploy and run Spring Boot applications on K8S.
It requires <code class="language-plaintext highlighter-rouge">docker</code> and <code class="language-plaintext highlighter-rouge">minikube</code> installed locally, as well as having a 
docker hub account.</p>

<p>The typical way to deploy and run applications on K8S is to provide YAML manifest
files defining the kind of K8S controllers to be used, for example pods, deployments,
services, etc. It’s quite common that, in order to deploy and run a simple Spring
Boot application that does “hello world”, one needs a half dozen of YAML manifest
files. And the fact that these files could be concatenated in a single one doesn’t
change much to the fact that, at the end of the day, almost 100 YAML lines are 
required for such a simple operation.</p>

<p>The good news is that, using Cloud Native Buildpacks, you don’t need to provide
anymore all these YAML files and to bother with K8S details, because the <code class="language-plaintext highlighter-rouge">spring
-boot-maven-plugin</code> is able to generate the Docker image of the application, by
simply leveraging its <code class="language-plaintext highlighter-rouge">build-image</code> goal.</p>

<p>Here is the plugin configuration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;plugin&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
    &lt;configuration&gt;
      &lt;image&gt;
        &lt;name&gt;nicolasduminil/k8s-sb:buildpacks&lt;/name&gt;
        &lt;env&gt;
          &lt;BP_JVM_VERSION&gt;25&lt;/BP_JVM_VERSION&gt;
        &lt;/env&gt;
        &lt;publish&gt;true&lt;/publish&gt;
      &lt;/image&gt;
    &lt;/configuration&gt;
  &lt;/plugin&gt;
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">name</code> element above overrides the Docker image name that will be generated
while the <code class="language-plaintext highlighter-rouge">env</code> element defines the JVM version. The generated image is published
on DockerHub registry. By default, Spring Boot uses Packeto Bellsoft Liberica 
Java Buildpack, which is a Cloud Native Buildpacks provider. If you prefer to 
use another one, you need then to override the buildpacks list in the Maven plugin
configuration, for example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;plugin&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
    &lt;configuration&gt;
      &lt;image&gt;
        &lt;name&gt;nicolasduminil/k8s-sb:buildpacks&lt;/name&gt;
        &lt;buildpack&gt;
          &lt;buildpack&gt;gcr.io/packeto-buildpacks/adoptium:latest&lt;buldpack&gt;
          &lt;buildpack&gt;urn:cnb:builder:paketo-buildpacks/java&lt;/buildpack&gt;
        &lt;/env&gt;
        &lt;publish&gt;true&lt;/publish&gt;
      &lt;/image&gt;
    &lt;/configuration&gt;
  &lt;/plugin&gt;
</code></pre></div></div>

<p>The configuration above allows to use Eclipse Temurin instead of Bellsoft 
Liberica OpenJDK.</p>

<h2 id="building-deploying-and-running">Building, deploying and running</h2>

<p>In order to build the application execute the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mvn clean package spring-boot:build-image
</code></pre></div></div>

<p>This will create the application JAR and the Docker image associated to it.
You should see something like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INFO] Successfully built image 'docker.io/nicolasduminil/k8s-sb:buildpacks'
[INFO]
[INFO]  &gt; Pushing image 'docker.io/nicolasduminil/k8s-sb:buildpacks' 11%
[INFO]  &gt; Pushing image 'docker.io/nicolasduminil/k8s-sb:buildpacks' 15%
[INFO]  &gt; Pushing image 'docker.io/nicolasduminil/k8s-sb:buildpacks' 15%
[INFO]  &gt; Pushing image 'docker.io/nicolasduminil/k8s-sb:buildpacks' 100%
[INFO]  &gt; Pushed image 'docker.io/nicolasduminil/k8s-sb:buildpacks'
</code></pre></div></div>

<p>Now, start <code class="language-plaintext highlighter-rouge">minikube</code> if not already done:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ minikube start
😄  minikube v1.32.0 on Ubuntu 24.04  
✨  Using the docker driver based on user configuration
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
🔥  Creating docker container (CPUs=2, Memory=7900MB) ...
🐳  Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
▪ Generating certificates and keys ...
▪ Booting up control plane ...
▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎  Verifying Kubernetes components...
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
</code></pre></div></div>

<p>Next, you need to create a deployment:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl create deployment k8s-sb --image nicolasduminil/k8s-sb:buildpacks
</code></pre></div></div>

<p>and to expose the associated service:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl expose deployment k8s-sb --type=NodePort --port=8080
</code></pre></div></div>

<p>In order to invoke the REST endpoint, you need to have its public URL. The 
following command will show it to you:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ minikube service list
|-------------|------------|--------------|---------------------------|
|  NAMESPACE  |    NAME    | TARGET PORT  |            URL            |
|-------------|------------|--------------|---------------------------|
| default     | k8s-sb     |         8080 | http://192.168.49.2:30809 |
| default     | kubernetes | No node port |                           |
| kube-system | kube-dns   | No node port |                           |
|-------------|------------|--------------|---------------------------|
</code></pre></div></div>

<p>All you need to do now is to send a HTTP GET request to the endpoint, as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://192.168.49.2:30809/hello/nicolas
Hello nicolas
</code></pre></div></div>

<p>As you can see, as a proof a goodwill the endpoint greets you.</p>

<p>You’ve succeeded to release a Spring Boot application on Kubernetes without
having to write a line of YAML. Congratulations. Now, stay tuned to see what comes next.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Spring Boot 4" /><category term="Kubernetes" /><category term="Minikube" /><category term="LinkedIn" /><category term="DZone" /><summary type="html"><![CDATA[This project demonstrates how to deploy and run Spring Boot applications on K8S. It requires docker and minikube installed locally, as well as having a docker hub account.]]></summary></entry><entry><title type="html">Rethinking Java Design Patterns: from OOP to FP (5)</title><link href="https://nicolasduminil.github.io/posts-archive/fdp5/" rel="alternate" type="text/html" title="Rethinking Java Design Patterns: from OOP to FP (5)" /><published>2026-07-27T00:00:00+00:00</published><updated>2026-07-27T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/fdp5</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/fdp5/"><![CDATA[<p>After having rethought the <a href="http://www.simplex-software.fr/posts-archive/fdp/">Factory</a>, the <a href="https://www.simplex-software.fr/posts-archive/fdp2/">Visitor</a>, the <a href="https://www.simplex-software.fr/posts-archive/fdp3/">Builder</a> and the <a href="https://www.simplex-software.fr/posts-archive/fdp4/">Decorator</a> paterns in previous posts, let’s look now at a behavioral one: the Strategy.</p>

<h2 id="the-strategy">The Strategy</h2>

<p>This design pattern belongs to the behavioral category and its purpose is to
define a family of algorithms, encapsulate each one of them and make them
interchangeable, such that the algorithm may vary independently of its consumer.
Where the decorator asked <em>what else should happen to this object ?</em>,
the strategy asks <em>which one of these algorithms should be applied ?</em>.</p>

<p>We keep the same <code class="language-plaintext highlighter-rouge">Product</code> domain and we compute a shipping cost for it. Three
interchangeable algorithms are provided:</p>

<ul>
  <li>Standard: a flat <code class="language-plaintext highlighter-rouge">4.99</code> fee.</li>
  <li>Express: <code class="language-plaintext highlighter-rouge">9.99</code> plus 2% of the product price.</li>
  <li>FreeOver: the familiar <em>“free delivery over 50.00”</em> commercial rule. It is
parameterized by a price threshold and by the strategy to apply when the
threshold isn’t reached. Should the product price be greater than or equal to
the threshold, the shipping is free, otherwise the product doesn’t qualify and
the cost is the one computed by that other strategy.</li>
</ul>

<p>For our <code class="language-plaintext highlighter-rouge">100.00</code> book, the standard shipping costs <code class="language-plaintext highlighter-rouge">4.99</code>, the express one costs
<code class="language-plaintext highlighter-rouge">11.99</code> and, with a <code class="language-plaintext highlighter-rouge">50.00</code> threshold, the free-over one costs <code class="language-plaintext highlighter-rouge">0.00</code>, since <code class="language-plaintext highlighter-rouge">100.00</code>
is above the threshold. Raising that same threshold to <code class="language-plaintext highlighter-rouge">150.00</code> falls back to the
standard shipping and, hence, to <code class="language-plaintext highlighter-rouge">4.99</code>.
Notice that, unlike the visitor, nothing here varies per product <em>type</em>: what varies is the
algorithm and it is the caller that picks it.</p>

<h3 id="the-object-oriented-approach">The object-oriented approach</h3>

<p>The figure below shows the class diagram of the object-oriented strategy:</p>

<p><img src="/assets/images/strategy-oop.png" alt="strategy-oop" title="OOP Strategy" /></p>

<p>The classic <em>Gang of Four</em> Strategy declares an interface for the family of the
algorithms and one class per algorithm:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public interface ShippingStrategy
{
  BigDecimal cost(Product product);
}

public class ExpressShipping implements ShippingStrategy
{
  private static final BigDecimal FEE = new BigDecimal("9.99");
  private static final BigDecimal RATE = new BigDecimal("0.02");

  public BigDecimal cost(Product product)
  {
    return FEE.add(product.price().multiply(RATE).setScale(2, RoundingMode.HALF_UP));
  }
}
</code></pre></div></div>

<p>An algorithm that needs to be parameterized becomes a class with state. Here
<code class="language-plaintext highlighter-rouge">FreeOverShipping</code> holds its threshold and the strategy to fall back to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public FreeOverShipping(BigDecimal threshold, ShippingStrategy otherwise) { ... }

public BigDecimal cost(Product product)
{
  return product.price().compareTo(threshold) &gt;= 0 ? FREE : otherwise.cost(product);
}
</code></pre></div></div>

<p>Last but not least, the <code class="language-plaintext highlighter-rouge">ShippingCalculator</code> class, as the <em>context</em>, is the object that uses the algorithm without
knowing which one it is. It only holds a reference to the interface, which is
what allows the algorithm to be replaced at runtime:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
BigDecimal cost = calculator.cost(book);      // 4.99

calculator.setStrategy(new ExpressShipping());
cost = calculator.cost(book);                 // 11.99
</code></pre></div></div>

<p>Contrary to the visitor and to the decorator, the strategy doesn’t require
anything at all from the elements it processes: no <code class="language-plaintext highlighter-rouge">accept</code> method and no shared
component interface. Consequently, and this is the first time it happens on the
object-oriented side, the module reuses the sealed <code class="language-plaintext highlighter-rouge">common.Product</code> directly, with
neither its own hierarchy, nor any adapter.</p>

<h3 id="the-functional-approach">The functional approach</h3>

<p>Look now at the class diagram of the functional style implementation:</p>

<p><img src="/assets/images/strategy-fp.png" alt="strategy-fp" title="FP Strategy" /></p>

<p>Of all the patterns seen so far, this is the one where the functional answer is the
most radical. The interface <code class="language-plaintext highlighter-rouge">ShippingStrategy</code> in the OO implementation declares one single
method and, like any interface, holds no state, such that everything it tells us is
<em>a <code class="language-plaintext highlighter-rouge">Product</code> comes in, a <code class="language-plaintext highlighter-rouge">BigDecimal</code> comes out</em>.</p>

<p>In functional terms, this is nothing more than a <code class="language-plaintext highlighter-rouge">Function&lt;Product, BigDecimal&gt;</code> type.
So each algorithm becomes a plain value of the function type, for example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static final Function&lt;Product, BigDecimal&gt; EXPRESS = product -&gt;
  EXPRESS_FEE.add(product.price().multiply(EXPRESS_RATE).setScale(2, RoundingMode.HALF_UP));
</code></pre></div></div>

<p>As opposed to the OO side which required the <code class="language-plaintext highlighter-rouge">FreeOverShipping</code> class holding
the threshold and the shipping strategy, the FP side captures them in a closure.
So this class on the OO side becomes on the FP side a higher-order function,
i.e. a function returning the strategy itself:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static Function&lt;Product, BigDecimal&gt; freeOver(BigDecimal threshold,
  Function&lt;Product, BigDecimal&gt; otherwise)
{
  return product -&gt; product.price().compareTo(threshold) &gt;= 0 ? FREE : otherwise.apply(product);
}
</code></pre></div></div>

<p>The very same happens to <code class="language-plaintext highlighter-rouge">ShippingCalculator</code>, the context class on the OOP side.
Its whole reason to exist was to hold a strategy in a field, such that its <code class="language-plaintext highlighter-rouge">cost()</code>
and <code class="language-plaintext highlighter-rouge">total()</code> operations could delegate to it. But a context is just an operation
parameterized by an algorithm and this, once again, is precisely a higher-order
function. Hence, the <code class="language-plaintext highlighter-rouge">ShippingCalculator.total()</code> method on the OO side becomes on the FP one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static Function&lt;Product, BigDecimal&gt; totalWith(Function&lt;Product, BigDecimal&gt; strategy)
{
  return product -&gt; product.price().add(strategy.apply(product));
}
</code></pre></div></div>

<p>such that the following call on the OO side, shown above:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ShippingCalculator calculator = new ShippingCalculator(new StandardShipping());
...
BigDecimal total = calculator.total(book);
</code></pre></div></div>

<p>becomes on the FP side:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BigDecimal total = totalWith(STANDARD).apply(book);
</code></pre></div></div>

<p>There is no property to hold the strategy anymore and, consequently, no <code class="language-plaintext highlighter-rouge">setStrategy()</code>
method either. Here the strategy is an argument which doesn’t need to be stored
in the context, just call the function with the right value and that’s all.</p>

<p>But the real advantage of the strategies as ordinary values is that they can be
combined. Picking the cheapest of several shipping options requires yet another
class on the OO side, while here it’s a simple combinator:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Function&lt;Product, BigDecimal&gt; best = cheapest(STANDARD, EXPRESS);   // 4.99
</code></pre></div></div>

<p>And as usual, they compose with <code class="language-plaintext highlighter-rouge">andThen</code>, for example to apply
a promotion to whatever cost has been computed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Function&lt;Product, BigDecimal&gt; promo =
  EXPRESS.andThen(cost -&gt; cost.divide(TWO, 2, RoundingMode.HALF_UP));   // 6.00
</code></pre></div></div>

<p>The OO Strategy encapsulates each algorithm in a class implementing a
common interface and injects the chosen one into a context object, while the
functional one observes that such an interface describes nothing but a function type
which the JDK already provides and, consequently, keeps only the algorithms
themselves. “Turtles all the way down”, and both compute the same cost.</p>

<h2 id="project-structure">Project structure</h2>

<p>The code is organized as a multi-module Maven project. The product domain lives
in its own <code class="language-plaintext highlighter-rouge">common</code> module: a <code class="language-plaintext highlighter-rouge">sealed</code> <code class="language-plaintext highlighter-rouge">Product</code> interface, the three product
records and the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated which already carries the FP factory
function seen above. Everything that can reuse that domain does:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oop-fp-design-patterns        (parent POM)
├── common                    sealed Product, the records, ProductType(+factory)
├── factory   (→ common)      ProductFactory (OOP); the FP factory *is* common.ProductType
├── visitor   (→ common)      FP: operations over the common records (switch + lambda bundle)
│                             OOP: its own element hierarchy (see below)
├── builder   (→ common)      immutable Order over the common records; OOP: fluent
│                             OrderBuilder; FP: composed UnaryOperator&lt;Order&gt; steps
├── decorator (→ common)      FP: composed UnaryOperator&lt;Product&gt; decorations over the
│                             common records; OOP: its own Product interface (see below)
└── strategy  (→ common)      shipping algorithms over the common records; OOP: the
                              ShippingStrategy hierarchy + context; FP: plain
                              Function&lt;Product, BigDecimal&gt; values
</code></pre></div></div>

<p>The FP factory, the FP visitor and the FP decorator all operate directly on the
<code class="language-plaintext highlighter-rouge">common</code> records, so nothing is duplicated there, and the Strategy does so on
<em>both</em> of its sides. The two exceptions are the object-oriented Visitor and the
object-oriented Decorator.</p>

<p>The Visitor needs an
<code class="language-plaintext highlighter-rouge">accept</code> method on every element (double dispatch); the Decorator needs a
non-sealed <code class="language-plaintext highlighter-rouge">Product</code> interface that its wrappers can implement. In both cases
<code class="language-plaintext highlighter-rouge">common.Product</code> is <code class="language-plaintext highlighter-rouge">sealed</code> and cannot be extended from another module, so each
owns its own element/component types and reuses only the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated -
the OOP decorator bridges back to <code class="language-plaintext highlighter-rouge">common</code> through a small <code class="language-plaintext highlighter-rouge">BaseProduct</code> adapter.
This asymmetry is not accidental.</p>

<p>The classic Visitor requires every element to
expose an <code class="language-plaintext highlighter-rouge">accept</code> method and the classic Decorator requires every component to
share the wrappers’ interface; both couple the elements to the pattern’s
abstraction, so they cannot be the sealed records defined in <code class="language-plaintext highlighter-rouge">common</code>. The
functional approach has no such coupling: it operates over the sealed type from the
outside - pattern-matching for the visitor, rebuilding through the factory for the
decorator - so the elements know nothing about the operations applied to them and,
hence, can be the shared <code class="language-plaintext highlighter-rouge">common</code> records.</p>

<p>The Strategy confirms the rule the other
way around: it doesn’t couple the elements to its abstraction either, only the
<em>client</em> to it, and this is precisely why it is the only pattern here whose
object-oriented implementation reuses <code class="language-plaintext highlighter-rouge">common</code> as freely as its functional one.</p>

<p>The full code of these examples, including the associated unit tests,
can be found <a href="https://github.com/nicolasduminil/oop-fp-design-patterns">here</a>.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Design patterns" /><category term="Functional style" /><category term="LinkedIn" /><summary type="html"><![CDATA[After having rethought the Factory, the Visitor, the Builder and the Decorator paterns in previous posts, let’s look now at a behavioral one: the Strategy.]]></summary></entry><entry><title type="html">Rethinking Java Design Patterns: from OOP to FP (4)</title><link href="https://nicolasduminil.github.io/posts-archive/fdp4/" rel="alternate" type="text/html" title="Rethinking Java Design Patterns: from OOP to FP (4)" /><published>2026-07-19T00:00:00+00:00</published><updated>2026-07-12T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/fdp4</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/fdp4/"><![CDATA[<p>After having rethought the <a href="http://www.simplex-software.fr/posts-archive/fdp/">Factory</a>, the <a href="https://www.simplex-software.fr/posts-archive/fdp2/">Visitor</a> and the <a href="https://www.simplex-software.fr/posts-archive/fdp3/">Builder</a> paterns in previous posts, let’s look now at another structural one: the Decorator.</p>

<h2 id="the-decorator">The Decorator</h2>

<p>This design pattern belongs to the structural category and its purpose is to
attach additional responsibilities to an object dynamically, by wrapping it in
another object that shares the same interface. It is the flexible alternative to
subclassing for extending behavior: rather than a combinatorial explosion of
<code class="language-plaintext highlighter-rouge">DiscountedTaxedGiftWrappedProduct</code> subclasses, you wrap a product in as many
independent decorators as you need, and they stack.</p>

<p>We reuse the same <code class="language-plaintext highlighter-rouge">Product</code> domain. Each decorator changes the <code class="language-plaintext highlighter-rouge">price()</code> and the
<code class="language-plaintext highlighter-rouge">description()</code> while leaving everything else untouched. To keep the pattern
visibly distinct from the visitor, whose rules varied per product type, the
decorators here apply the same rule to every product:</p>

<ul>
  <li>Discounted: 10% off the wrapped price.</li>
  <li>Taxed: adds 20% VAT to the wrapped price.</li>
  <li>GiftWrapped: adds a flat <code class="language-plaintext highlighter-rouge">5.00</code> wrapping fee.</li>
</ul>

<p>Because they stack, a 100.00 book decorated <code class="language-plaintext highlighter-rouge">Discounted</code> → <code class="language-plaintext highlighter-rouge">Taxed</code> →
<code class="language-plaintext highlighter-rouge">GiftWrapped</code> goes <code class="language-plaintext highlighter-rouge">100.00 → 90.00 → 108.00 → 113.00</code>, and its description reads
<code class="language-plaintext highlighter-rouge">"A book (discounted) (VAT incl.) (gift-wrapped)"</code>.</p>

<h3 id="the-object-oriented-approach">The object-oriented approach</h3>

<p>The figure below shows the class diagram of the object-oriented decorator:</p>

<p><img src="/assets/images/decorator-oop.png" alt="decorator-oop" title="OOP Decorator" /></p>

<p>The classic <em>Gang of Four</em> Decorator is an object that implements the component
interface and holds a reference to another component, delegating the untouched
operations and overriding the ones it enhances. An abstract <code class="language-plaintext highlighter-rouge">ProductDecorator</code>
captures the delegation once:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public abstract class ProductDecorator implements Product
{
  protected final Product product;

  protected ProductDecorator(Product product)
  {
    this.product = Objects.requireNonNull(product, "Product is null");
  }

  public String name() { return product.name(); }
  public String description() { return product.description(); }
  public BigDecimal price() { return product.price(); }
  public ProductType type() { return product.type(); }
}
</code></pre></div></div>

<p>Each concrete decorator then overrides only what it changes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class Discounted extends ProductDecorator
{
  private static final BigDecimal RATE = new BigDecimal("0.10");

  public Discounted(Product product) { super(product); }

  public BigDecimal price()
  {
    return product.price().subtract(amount(product.price(), RATE));
  }

  public String description()
  {
    return product.description() + " (discounted)";
  }
}
</code></pre></div></div>

<p>Since a decorator is a <code class="language-plaintext highlighter-rouge">Product</code>, decorators wrap decorators and the enhancements
compose by nesting:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Product wrapped = new GiftWrapped(new Taxed(new Discounted(new BaseProduct(book))));
BigDecimal price = wrapped.price();   // 113.00
</code></pre></div></div>

<p>The leaf being wrapped is a <code class="language-plaintext highlighter-rouge">BaseProduct</code>, a small record that adapts a shared
<code class="language-plaintext highlighter-rouge">common.Product</code> into the decorator’s own interface. This is necessary because
<code class="language-plaintext highlighter-rouge">common.Product</code> is <code class="language-plaintext highlighter-rouge">sealed</code> and so, exactly like the object-oriented visitor, the
decorator cannot make the <code class="language-plaintext highlighter-rouge">common</code> records implement its interface directly.</p>

<h3 id="the-functional-approach">The functional approach</h3>

<p>Look now at the class diagram of the functional style implementation:</p>

<p><img src="/assets/images/decorator-fp.png" alt="decorator-fp" title="FP Decorator" /></p>

<p>The functional counterpart of a decorator is simply a function whicg maps a product
to an enhanced product and implemented as an <code class="language-plaintext highlighter-rouge">UnaryOperator&lt;Product&gt;</code>. Because
the <code class="language-plaintext highlighter-rouge">common</code> records are immutable, “enhancing” one means rebuilding it through
the <code class="language-plaintext highlighter-rouge">ProductType</code> factory, already seen at the very beginning, which is why the
FP side reuses <code class="language-plaintext highlighter-rouge">common</code> directly with no adapter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static final UnaryOperator&lt;Product&gt; DISCOUNTED = product -&gt;
  product.type().newInstance(product.name(),
    product.description() + " (discounted)",
    product.price().subtract(amount(product.price(), "0.10")));
</code></pre></div></div>

<p>Being ordinary values, the decorations compose with <code class="language-plaintext highlighter-rouge">andThen</code>, exactly as the
factory composed its <code class="language-plaintext highlighter-rouge">factory</code> function, the visitor composed its operations and
the builder composed its steps:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UnaryOperator&lt;Product&gt; decorate = DISCOUNTED.andThen(TAXED).andThen(GIFT_WRAPPED);
Product wrapped = decorate.apply(book);   // price 113.00
</code></pre></div></div>

<p>And, just like the functional builder step, a decoration is a reusable first-class
value. For example, the same discount could be applied twice:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Product wrapped = DISCOUNTED.andThen(DISCOUNTED).apply(book);   // 100 -&gt; 90 -&gt; 81
</code></pre></div></div>

<p>The object-oriented Decorator wraps the component in a stack of objects sharing its
interface, while the functional one expresses the very same stacking as the
composition of pure <code class="language-plaintext highlighter-rouge">Product</code> to <code class="language-plaintext highlighter-rouge">Product</code> functions. “Turtles all the way down”,
and both land on the same enhanced product.</p>

<h2 id="project-structure">Project structure</h2>

<p>The code is organized as a multi-module Maven project. The product domain lives
in its own <code class="language-plaintext highlighter-rouge">common</code> module: a <code class="language-plaintext highlighter-rouge">sealed</code> <code class="language-plaintext highlighter-rouge">Product</code> interface, the three product
records and the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated which already carries the FP factory
function seen above. Everything that can reuse that domain does:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oop-fp-design-patterns        (parent POM)
├── common                    sealed Product, the records, ProductType(+factory)
├── factory   (→ common)      ProductFactory (OOP); the FP factory *is* common.ProductType
├── visitor   (→ common)      FP: operations over the common records (switch + lambda bundle)
│                             OOP: its own element hierarchy (see below)
├── builder   (→ common)      immutable Order over the common records; OOP: fluent
│                             OrderBuilder; FP: composed UnaryOperator&lt;Order&gt; steps
└── decorator (→ common)      FP: composed UnaryOperator&lt;Product&gt; decorations over the
                              common records; OOP: its own Product interface (see below)
</code></pre></div></div>

<p>The FP factory, the FP visitor and the FP decorator all operate directly on the
<code class="language-plaintext highlighter-rouge">common</code> records, so nothing is duplicated there. The two exceptions are the
object-oriented Visitor and the object-oriented Decorator. The Visitor needs an
<code class="language-plaintext highlighter-rouge">accept</code> method on every element (double dispatch); the Decorator needs a
non-sealed <code class="language-plaintext highlighter-rouge">Product</code> interface that its wrappers can implement. In both cases
<code class="language-plaintext highlighter-rouge">common.Product</code> is <code class="language-plaintext highlighter-rouge">sealed</code> and cannot be extended from another module, so each
owns its own element/component types and reuses only the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated -
the OOP decorator bridges back to <code class="language-plaintext highlighter-rouge">common</code> through a small <code class="language-plaintext highlighter-rouge">BaseProduct</code> adapter.
This asymmetry is not accidental. The classic Visitor requires every element to
expose an <code class="language-plaintext highlighter-rouge">accept</code> method and the classic Decorator requires every component to
share the wrappers’ interface; both couple the elements to the pattern’s
abstraction, so they cannot be the sealed records defined in <code class="language-plaintext highlighter-rouge">common</code>. The
functional approach has no such coupling: it operates over the sealed type from the
outside - pattern-matching for the visitor, rebuilding through the factory for the
decorator - so the elements know nothing about the operations applied to them and,
hence, can be the shared <code class="language-plaintext highlighter-rouge">common</code> records.</p>

<p>The full code of these examples, including the associated unit tests,
can be found <a href="https://github.com/nicolasduminil/oop-fp-design-patterns">here</a>.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Design patterns" /><category term="Functional style" /><category term="LinkedIn" /><summary type="html"><![CDATA[After having rethought the Factory, the Visitor and the Builder paterns in previous posts, let’s look now at another structural one: the Decorator.]]></summary></entry><entry><title type="html">Rethinking Java Design Patterns: from OOP to FP (3)</title><link href="https://nicolasduminil.github.io/posts-archive/fdp3/" rel="alternate" type="text/html" title="Rethinking Java Design Patterns: from OOP to FP (3)" /><published>2026-07-12T00:00:00+00:00</published><updated>2026-07-12T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/fdp3</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/fdp3/"><![CDATA[<p>After having rethought the <a href="http://www.simplex-software.fr/posts-archive/fdp/">Factory</a> and the <a href="https://www.simplex-software.fr/posts-archive/fdp2/">Visitor</a> paterns in previous posts
let’s look now at another creational one: the Builder.</p>

<h2 id="the-builder">The Builder</h2>

<p>This design pattern belongs to the creational category, like the factory, but it
solves a different problem. The factory hides <em>which</em> concrete type gets
instantiated while the Builder assembles a single, complex object step by step,
separating its construction from its representation. It is the classic answer to
the <em>telescoping-constructor problem</em>: an object with many parameters, among which some
required, most optional, whose constructor would otherwise explode into a
combinatorial set of overloads.</p>

<p>Our <code class="language-plaintext highlighter-rouge">Product</code> records have only three required fields, so they don’t motivate a
builder. We therefore introduce an <code class="language-plaintext highlighter-rouge">Order</code>: a customer order that aggregates the
<code class="language-plaintext highlighter-rouge">common</code> products as line items and adds several optional attributes - a coupon
code, a gift-wrap flag and a free-text note. Whatever the style, the target is the
same immutable value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public record Order(
  String customer,
  String currency,
  List&lt;Product&gt; items,
  Optional&lt;String&gt; coupon,
  boolean giftWrapped,
  Optional&lt;String&gt; note)
{
  public Order
  {
    Objects.requireNonNull(customer, "Customer is null");
    Objects.requireNonNull(currency, "Currency is null");
    items = items == null ? List.of() : List.copyOf(items);
    coupon = coupon == null ? Optional.empty() : coupon;
    note = note == null ? Optional.empty() : note;
  }

  public BigDecimal subtotal() { ... }
}
</code></pre></div></div>

<h3 id="the-object-oriented-approach">The object-oriented approach</h3>

<p>The figure below shows the class diagram of the object-oriented builder:</p>

<p><img src="/assets/images/builder-oop.png" alt="builder-oop" title="OOP Builder" /></p>

<p>The classic <em>Gang of Four</em> Builder is a mutable accumulator. The required
arguments are captured up front, the optional ones are added through fluent calls
that all return <code class="language-plaintext highlighter-rouge">this</code>, and <code class="language-plaintext highlighter-rouge">build()</code> freezes the accumulated state into the
immutable <code class="language-plaintext highlighter-rouge">Order</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public final class OrderBuilder
{
  private final String customer;
  private final String currency;
  private final List&lt;Product&gt; items = new ArrayList&lt;&gt;();
  private String coupon;
  private boolean giftWrapped;
  private String note;

  public static OrderBuilder of(String customer, String currency) { ... }

  public OrderBuilder addItem(Product item) { items.add(item); return this; }
  public OrderBuilder coupon(String coupon) { this.coupon = coupon; return this; }
  public OrderBuilder giftWrap() { this.giftWrapped = true; return this; }
  public OrderBuilder note(String note) { this.note = note; return this; }

  public Order build()
  {
    return new Order(customer, currency, items,
      Optional.ofNullable(coupon), giftWrapped, Optional.ofNullable(note));
  }
}
</code></pre></div></div>

<p>Building an order reads as a sentence, and you only mention the parts you actually
need:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Order order = OrderBuilder.of("Alice", "EUR")
  .addItem(book).addItem(phone)
  .coupon("SUMMER").giftWrap()
  .build();
</code></pre></div></div>

<h3 id="the-functional-approach">The functional approach</h3>

<p>Look now at the class diagram of the functional style implementation:</p>

<p><img src="/assets/images/builder-fp.png" alt="builder-fp" title="FP Builder" /></p>

<p>The functional counterpart keeps the same immutable <code class="language-plaintext highlighter-rouge">Order</code> target but drops the
mutable accumulator. Each build step becomes a first-class
<code class="language-plaintext highlighter-rouge">UnaryOperator&lt;Order&gt;</code> value - a pure function mapping one immutable <code class="language-plaintext highlighter-rouge">Order</code> to
the next by returning a modified copy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static UnaryOperator&lt;Order&gt; addItem(Product item)
{
  return order -&gt; new Order(order.customer(), order.currency(),
    Stream.concat(order.items().stream(), Stream.of(item)).toList(),
    order.coupon(), order.giftWrapped(), order.note());
}
</code></pre></div></div>

<p>Because the steps are ordinary values, they are not called <em>on</em> a builder, but they
are composed with <code class="language-plaintext highlighter-rouge">andThen</code>, exactly as the factory composed its <code class="language-plaintext highlighter-rouge">factory</code>
function and the visitor composed its operations:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Function&lt;Order, Order&gt; config = addItem(book)
  .andThen(addItem(phone))
  .andThen(coupon("SUMMER"))
  .andThen(giftWrap());

Order order = config.apply(OrderBuilder.empty("Alice", "EUR"));
</code></pre></div></div>

<p>This is more than a stylistic variation. In the OOP version a step is a method
call that exists only for the duration of the chain. In the FP version a step is
a value which can be stored it in a variable, passed to another method, kept in a
list of steps and applied later, or reused the very same step twice:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UnaryOperator&lt;Order&gt; addBook = addItem(book);
Order order = addBook.andThen(addBook).apply(OrderBuilder.empty("Alice", "EUR"));
</code></pre></div></div>

<p>The object-oriented Builder wraps a stateful object around the immutable target
while the functional one expresses construction as the composition of pure copy
functions over it. “Turtles all the way down”, and both land on the same <code class="language-plaintext highlighter-rouge">Order</code>.</p>

<h2 id="project-structure">Project structure</h2>

<p>The code is organized as a multi-module Maven project. The product domain lives
in its own <code class="language-plaintext highlighter-rouge">common</code> module: a <code class="language-plaintext highlighter-rouge">sealed</code> <code class="language-plaintext highlighter-rouge">Product</code> interface, the three product
records and the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated - which already carries the FP factory
function seen above. Everything that <em>can</em> reuse that domain does:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oop-fp-design-patterns        (parent POM)
├── common                    sealed Product, the records, ProductType(+factory)
├── factory   (→ common)      ProductFactory (OOP); the FP factory *is* common.ProductType
├── visitor   (→ common)      FP: operations over the common records (switch + lambda bundle)
│                             OOP: its own element hierarchy (see below)
└── builder   (→ common)      immutable Order over the common records; OOP: fluent
                              OrderBuilder; FP: composed UnaryOperator&lt;Order&gt; steps
</code></pre></div></div>

<p>The FP factory and the FP visitor both operate directly on the <code class="language-plaintext highlighter-rouge">common</code> records,
so nothing is duplicated there. The one exception is the object-oriented
Visitor which needs an <code class="language-plaintext highlighter-rouge">accept</code> method on every element (double dispatch), and
since <code class="language-plaintext highlighter-rouge">common.Product</code> is <code class="language-plaintext highlighter-rouge">sealed</code> it cannot be extended from another module.
The OOP visitor therefore owns its element hierarchy and reuses only the
<code class="language-plaintext highlighter-rouge">ProductType</code> enumerated. This asymmetry is not accidental. The classic Visitor
requires every element to expose an <code class="language-plaintext highlighter-rouge">accept</code> method, which couples the elements to
the visitor abstraction. These elements must be defined together with the visitor abstraction, and so cannot be
the sealed records defined in <code class="language-plaintext highlighter-rouge">common</code>. The functional approach has no such coupling: it
pattern-matches over the sealed type from the outside, so the elements know nothing
about the operations applied to them and, hence, can be the shared <code class="language-plaintext highlighter-rouge">common</code> records.</p>

<p>The full code of these examples, including the associated unit tests,
can be found <a href="https://github.com/nicolasduminil/oop-fp-design-patterns">here</a>.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Design patterns" /><category term="Functional style" /><category term="LinkedIn" /><summary type="html"><![CDATA[After having rethought the Factory and the Visitor paterns in previous posts let’s look now at another creational one: the Builder.]]></summary></entry><entry><title type="html">Rethinking Java Design Patterns: from OOP to FP (2)</title><link href="https://nicolasduminil.github.io/posts-archive/fdp2/" rel="alternate" type="text/html" title="Rethinking Java Design Patterns: from OOP to FP (2)" /><published>2026-07-04T00:00:00+00:00</published><updated>2026-07-04T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/fdp2</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/fdp2/"><![CDATA[<p>In a previous <a href="http://www.simplex-software.fr/posts-archive/fdp/">post</a>, we’ve looked at how to combine the Java’s object oriented and
functional styles to implement a new and modern Factory design pattern. After
this creational pattern, let’s continue with a behavioral one: the Visitor.</p>

<h2 id="the-visitor">The Visitor</h2>

<p>This design pattern belongs to the behavioral category and its purpose is to
add new operations to an existing object hierarchy without modifying the classes
of that hierarchy. It is the classic answer to the <em>expression problem</em>: when the
set of types is stable but the set of operations grows, the Visitor lets you keep
adding operations cheaply.</p>

<p>We reuse the same domain as the factory: a <code class="language-plaintext highlighter-rouge">Product</code> implemented by <code class="language-plaintext highlighter-rouge">BookProduct</code>,
<code class="language-plaintext highlighter-rouge">ElectronicProduct</code> and <code class="language-plaintext highlighter-rouge">FashionProduct</code>. To give the visitor a reason to exist,
each operation now behaves differently per product type:</p>

<ul>
  <li>VAT: a reduced 5.5% rate for books, the standard 20% rate otherwise.</li>
  <li>Shipping: <code class="language-plaintext highlighter-rouge">10.00 + 2%</code> of the price for fragile, insured electronics, a flat <code class="language-plaintext highlighter-rouge">3.00</code> for books and a flat <code class="language-plaintext highlighter-rouge">5.00</code> for fashion.</li>
  <li>Discount: 10% for electronics, 5% for books, 15% for fashion.</li>
</ul>

<h3 id="the-object-oriented-approach">The object-oriented approach</h3>

<p>The classic Visitor relies on <em>double dispatch</em>. Each <code class="language-plaintext highlighter-rouge">Product</code> accepts a visitor
and calls back the overloaded <code class="language-plaintext highlighter-rouge">visit(...)</code> matching its own type</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public interface Product
{
  ...
  &lt;R&gt; R accept(ProductVisitor&lt;R&gt; visitor);
}

public record BookProduct (String name, String description, BigDecimal price) implements Product
{
  ...
  public &lt;R&gt; R accept(ProductVisitor&lt;R&gt; visitor)
  {
    return visitor.visit(this);
  }
}
</code></pre></div></div>

<p>The operation lives in a generic visitor, one <code class="language-plaintext highlighter-rouge">visit(...)</code> overload per concrete type:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public interface ProductVisitor&lt;R&gt;
{
  R visit(ElectronicProduct product);
  R visit(BookProduct product);
  R visit(FashionProduct product);
}
</code></pre></div></div>

<p>Computing the VAT of any product is then a matter of applying a concrete visitor:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BigDecimal vat = book.accept(new VatVisitor());
</code></pre></div></div>

<p>Adding a new operation only requires a new <code class="language-plaintext highlighter-rouge">ProductVisitor</code> class while
the <code class="language-plaintext highlighter-rouge">Product</code> implementation ones never change. However, we need to notice that,
as the reverse of the trade-off the factory made, adding a new operation is easy
but adding a new product type is expansive since it requires to edit its central
<code class="language-plaintext highlighter-rouge">switch</code>. The visitor makes adding a new operation free but shifts that same
cost onto types, because a new product type now forces every visitor to be updated.
This is the classic <em>expression problem</em>: you can make types cheap to add or
operations cheap to add, but not both.</p>

<p>The following figure below shows the object-oriented implementation class diagram:</p>

<p><img src="/assets/images/visitor-oop.png" alt="visitor-oop" title="OOP Visitor" /></p>

<h3 id="the-functional-approach">The functional approach</h3>

<p>Look now at the class diagram of the Vistor functional style implemntation:</p>

<p><img src="/assets/images/visitor-fp.png" alt="visitor-fp" title="FP Visitor" /></p>

<p>In modern Java the functional counterpart of the Visitor is exhaustive pattern
matching over a sealed type. We first seal the hierarchy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public sealed interface Product permits ElectronicProduct, BookProduct, FashionProduct
{
  ...
}
</code></pre></div></div>

<p>An operation is then just a <code class="language-plaintext highlighter-rouge">Function&lt;Product, R&gt;</code> built on a <code class="language-plaintext highlighter-rouge">switch</code> that
deconstructs each record. Because <code class="language-plaintext highlighter-rouge">Product</code> is sealed, the compiler proves the
switch is exhaustive - no <code class="language-plaintext highlighter-rouge">default</code> branch, no double dispatch, no <code class="language-plaintext highlighter-rouge">accept</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public static final Function&lt;Product, BigDecimal&gt; VAT = product -&gt; switch (product)
{
  case BookProduct(String name, String description, BigDecimal price) -&gt; amount(price, "0.055");
  case ElectronicProduct(String name, String description, BigDecimal price) -&gt; amount(price, "0.20");
  case FashionProduct(String name, String description, BigDecimal price) -&gt; amount(price, "0.20");
};
</code></pre></div></div>

<p>Being ordinary functions, these operations can be composed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ProductOperations.DISCOUNT.andThen(amount -&gt; "discount=" + amount).apply(fashion);
</code></pre></div></div>

<p>Between the classic Visitor and pure pattern matching sits an intermediate step:
the visitor as a <em>bundle of functions</em>, one lambda per type, instead of an
interface with one method per type:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public record ProductVisitor&lt;R&gt;(
  Function&lt;ElectronicProduct, R&gt; onElectronic,
  Function&lt;BookProduct, R&gt; onBook,
  Function&lt;FashionProduct, R&gt; onFashion)
{
  public R visit(Product product)
  {
    return switch (product)
    {
      case ElectronicProduct e -&gt; onElectronic.apply(e);
      case BookProduct b -&gt; onBook.apply(b);
      case FashionProduct f -&gt; onFashion.apply(f);
    };
  }
}
</code></pre></div></div>

<p>Which makes an operation a value you can assemble on the fly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ProductVisitor&lt;BigDecimal&gt; vat = new ProductVisitor&lt;&gt;(
  e -&gt; ..., b -&gt; ..., f -&gt; ...);
BigDecimal amount = vat.visit(book);
</code></pre></div></div>

<p>Of course, you may find these examples in the same GitHub <a href="https://github.com/nicolasduminil/oop-fp-design-patterns">repository</a> as the previous
factory pattern. It has been reorganized as a multi-module Maven project.</p>

<p>The product domain lives now in its own <code class="language-plaintext highlighter-rouge">common</code> module containing a <code class="language-plaintext highlighter-rouge">sealed</code>
<code class="language-plaintext highlighter-rouge">Product</code> interface, the three product records and the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated
which already carries the FP factory function seen above.
Everything that can reuse that domain does:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oop-fp-design-patterns        (parent POM)
├── common                    sealed Product, the records, ProductType(+factory)
├── factory   (→ common)      ProductFactory (OOP); the FP factory *is* common.ProductType
└── visitor   (→ common)      FP: operations over the common records (switch + lambda bundle)
                              OOP: its own element hierarchy (see below)
</code></pre></div></div>

<p>The FP factory and the FP visitor both operate directly on the <code class="language-plaintext highlighter-rouge">common</code> records,
so nothing is duplicated there. The one exception is the object-oriented
Visitor which needs an <code class="language-plaintext highlighter-rouge">accept</code> method on every product type, for the double dispatch.
Since <code class="language-plaintext highlighter-rouge">common.Product</code> is <code class="language-plaintext highlighter-rouge">sealed</code> it cannot be extended from another module.
The OOP visitor therefore owns its product hierarchy and reuses only the
<code class="language-plaintext highlighter-rouge">ProductType</code> enumerated. That asymmetry is not accidental.
The classic Visitor requires every product type to expose an <code class="language-plaintext highlighter-rouge">accept</code> method, which
couples them to the visitor abstraction. Accordingly, each product type must be
defined in sync with it and so cannot be the sealed <code class="language-plaintext highlighter-rouge">common</code> records. The functional
approach has no such coupling. It pattern-matches over the sealed type from the
outside, so the products know nothing about the operations applied to them and
can stay the shared <code class="language-plaintext highlighter-rouge">common</code> records.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Design patterns" /><category term="Functional style" /><category term="LinkedIn" /><summary type="html"><![CDATA[In a previous post, we’ve looked at how to combine the Java’s object oriented and functional styles to implement a new and modern Factory design pattern. After this creational pattern, let’s continue with a behavioral one: the Visitor.]]></summary></entry><entry><title type="html">Rethinking Java Design Patterns: from OOP to FP</title><link href="https://nicolasduminil.github.io/posts-archive/fdp/" rel="alternate" type="text/html" title="Rethinking Java Design Patterns: from OOP to FP" /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/fdp</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/fdp/"><![CDATA[<p>Functional Programming answer, to those who wonder how to integrate or combine
it with Object-Oriented Programming, is usually: <em>Turtles all the way down</em>.</p>

<p>This is an aphorism which origin is credited to Richard Feynman. In his book,
<em>Surely You’re Joking, Mr. Feynman !</em>, published in 1985, he tells the story
of one of his conferences on the nature on the universe, where he has been challenged
by someone in the audience, saying that the universe rests on a turtle. Feynman
asked then what the turtle is resting on and the answer was: “another bigger turtle”.
And when he smugly asked what the bigger turtle is resting on, the attendee said:
“It’s turtles all the way down, you can’t trick me !”</p>

<p>This metaphor is often used in the context of the Functional Programming to describe
an infinite series of entities governed by a recursive principle. And it’s also
the answer of the Functional Programming to developers coming from an Object-Oriented
mindset: “just do functional all the way down”. But, in order to adopt a more systematic
approach of combining object-oriented principles in a functional style, a more
practical answer is required and this is what I’m trying to do here.</p>

<p>We, as developers, fortunatelly don’t have to reinvent the wheel. All the problems
are solved nowadays, especially since the LLM agents became the most common
digital infrastructure. But as surprizing as it might seem to our younger
colleagues, who can’t live 48 hours without AI, even before LLMs, a general
approach fitting solutions to problems existed, in the form of design patterns.</p>

<p>As a matter of fact, the Object-Oriented Programming proposes repeatable solutions
tested, proven and formalized, called design patterns, that you most likely already
used, even if you aren’t mandatory aware that you did.</p>

<p>The <em>Gang of Four</em> classified these patterns in three groups:</p>

<ul>
  <li><em>Behavioral patterns</em> which deal with responsibilities and communication between objects.</li>
  <li><em>Creational patterns</em> that abstract the objects creation / instantiation process.</li>
  <li><em>Structural patterns</em> that compose objects such that to form larger or enhanced ones.</li>
</ul>

<p>Let’s take one of the most commonly used patterns in each category and see how
to combine their object-oriented inherent nature to a more functional approach.</p>

<h2 id="the-factory">The Factory</h2>

<p>This design pattern belongs to the creational category and its purpose is to
instantiate objects without exposing implementation details.</p>

<h3 id="the-object-oriented-approach">The object-oriented approach</h3>

<p>The figure below shows the class diagram of a factory design pattern:</p>

<p><img src="/assets/images/factory-oop.png" alt="factory-oop" title="OOP Factory" /></p>

<p>Our scenario here is a simple one: a <code class="language-plaintext highlighter-rouge">Product</code> interface implemented by three
classes: <code class="language-plaintext highlighter-rouge">BookProduct</code>, <code class="language-plaintext highlighter-rouge">ElectronicProduct</code> and <code class="language-plaintext highlighter-rouge">FashionProduct</code>. They can be
created through the <code class="language-plaintext highlighter-rouge">ProductFactory</code> class, as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class ProductFactory
{
  public static Product newProduct (String name, String description, BigDecimal price, ProductType productType)
  {
    Objects.requireNonNull(name, "Name is null");
    ...
    return switch (productType)
    {
      case BOOK -&gt; new BookProduct(name, description, price);
      case ELECTRONIC -&gt; new ElectronicProduct(name, description, price);
      case FASHION -&gt; new FashionProduct(name, description, price);
      default -&gt; throw new IllegalArgumentException ("Unknown type: %s".formatted(productType));
    };
  }
}
</code></pre></div></div>

<p>Using this factory, it’s very easy to create a <code class="language-plaintext highlighter-rouge">BookProduct</code>, for example, while
avoiding to expose implementation details:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    ...
    Product product = ProductFactory.newProduct("Book1", "A book",
      new BigDecimal("20.50"), ProductType.BOOK);
    ...
</code></pre></div></div>

<p>As you probably noticed, the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated defines the three categories.
If a new product is to be introduced, the factory has to be modified such that to
reflect this business changement. And this interdependence of the factory and the
enumerated makes the whole approach fragile.</p>

<p>In order to reduce this fragility, we need to introduce a compile-time validation
with a more functional approach.</p>

<h3 id="the-functional-approach">The functional approach</h3>

<p>Our example is an over-simplified case of a product management system. The presented
factory instantiate different simple records having the same arguments. These
identical constructors give us the possibility to move the factory directly into
the <code class="language-plaintext highlighter-rouge">ProductType</code> enumerated, such that any new product automatically requires
a correspondent factory.</p>

<p>Java <code class="language-plaintext highlighter-rouge">enum</code> types are based on constant names, but we can attach to each one its
correspondent value. Or, even better, a factory function for creating discrete
products. Look at that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public enum ProductType
{
  ELECTRONIC(ElectronicProduct::new),
  FASHION(FashionProduct::new),
  BOOK(BookProduct::new);

  public final TriFunction&lt;String, String, BigDecimal, Product&gt; factory;

  ProductType (TriFunction&lt;String, String, BigDecimal, Product&gt; factory)
  {
    this.factory = factory;
  }

  public Product newInstance (String name, String description, BigDecimal price)
  {
    Objects.requireNonNull(name, "Name is null");
    ...
    return this.factory.apply (name, description, price);
  }
}
</code></pre></div></div>

<p>Now, creating a new <code class="language-plaintext highlighter-rouge">Product</code> instance is easier:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Product product = ProductType.BOOK.newInstance("Book1",
  "A book", new BigDecimal("20.45"));
</code></pre></div></div>

<p>The public property <code class="language-plaintext highlighter-rouge">factory</code> seems redundant now that a dedicated method for the
instance creation is available. But it provides a very convenient functional way
to interact further with the factory. For example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ProductType.BOOK.factory.andThen(showThePrice).apply("Book1",
  "A book", new BigDecimal("20.45"));
</code></pre></div></div>

<p>as shown in the <code class="language-plaintext highlighter-rouge">TestProductFactory</code> class, in the <code class="language-plaintext highlighter-rouge">fp_design_paterns.factory</code>
package. Of course, given that our products need three arguments constructors and
since Java doesn’t provide an equivalent of the <code class="language-plaintext highlighter-rouge">BiFunction</code> class, but with three
input arguments, you will need to craft a <code class="language-plaintext highlighter-rouge">TriFunction</code> class, as shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@FunctionalInterface
public interface TriFunction&lt;A, B, C, R&gt;
{
  R apply(A a, B b, C c);
  default &lt;K&gt; TriFunction&lt;A, B, C, K&gt; andThen(Function&lt;? super R, ? extends K&gt; f)
  {
    Objects.requireNonNull(f);
    return (A a, B b, C c) -&gt; f.apply(apply(a, b, c));
  }
}
</code></pre></div></div>

<p>You can do that or, if like me, you prefer to use a reliable library, then Vavr
already defines a <code class="language-plaintext highlighter-rouge">Function3</code> interface that has the behavior you want. Just
include the following Maven dependency:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;dependency&gt;
  &lt;groupId&gt;io.vavr&lt;/groupId&gt;
  &lt;artifactId&gt;vavr&lt;/artifactId&gt;
  &lt;version&gt;1.0.1&lt;/version&gt;
&lt;/dependency&gt;
</code></pre></div></div>

<p>This library is the good choice if you need to define functions with up to 8
arguments. Then, you just need to replace, in <code class="language-plaintext highlighter-rouge">ProductType</code>, the following
definition:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public final TriFunction&lt;String, String, BigDecimal, Product&gt; factory;

ProductType (TriFunction&lt;String, String, BigDecimal, Product&gt; factory)
{
  this.factory = factory;
}
</code></pre></div></div>

<p>bvy this one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public final Function3&lt;String, String, BigDecimal, Product&gt; factory;

ProductType (Function3&lt;String, String, BigDecimal, Product&gt; factory)
{
  this.factory = factory;
}
</code></pre></div></div>

<p>The full code of these examples, including the associated unit tests,
can be found here: https://github.com/nicolasduminil/oop-fp-design-patterns.</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Design patterns" /><category term="Functional style" /><category term="LinkedIn" /><summary type="html"><![CDATA[Functional Programming answer, to those who wonder how to integrate or combine it with Object-Oriented Programming, is usually: Turtles all the way down.]]></summary></entry><entry><title type="html">Rethinking Java CRUDs with Event Sourcing and CQRS patterns</title><link href="https://nicolasduminil.github.io/posts-archive/cqrs/" rel="alternate" type="text/html" title="Rethinking Java CRUDs with Event Sourcing and CQRS patterns" /><published>2026-05-03T00:00:00+00:00</published><updated>2026-05-03T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/cqrs</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/cqrs/"><![CDATA[<p>Traditional CRUD systems store only the current state of an entity. When a
record is updated, the previous value is overwritten and lost forever. Event
Sourcing inverts this model: instead of persisting state, the system persists
the sequence of events that caused each state transition. The current state
is never stored directly, but it is always derived by replaying the event history.</p>

<p>Command Query Responsibility Segregation (CQRS) separates the write model from
the read model. A command expresses intent to change state, for example <code class="language-plaintext highlighter-rouge">PlaceOrder</code>,
<code class="language-plaintext highlighter-rouge">AddItem</code>, <code class="language-plaintext highlighter-rouge">ShipOrder</code>. A query reads state without modifying it. The two sides
use separate models, separate logic, and, in a full implementation, separate storage.</p>

<p>CQRS and Event Sourcing are complementary: the event stream is the write side’s
source of truth, while one or more projections (read models) are derived from
those events for fast querying.</p>

<p>This article aims at showing how to apply, in practice, these concepts, using
for illustration purposes a modified version of one of <a href="https://www.the-main-thread.com/p/event-sourcing-quarkus-java-records-cqrs-tutorial?utm_source=post-email-title&amp;publication_id=4194688&amp;post_id=181216251&amp;utm_campaign=email-post-title&amp;isFreemail=true&amp;r=3b6280&amp;triedRedirect=true">Markus Eisele’s article,
from the 27th of December 2025 on Substack</a>. 
In his article, Markus shows a Quarkus based project implementing a simplified 
order management system. Here, I’m presenting the Spring Boot implementation of
this same system, to change. You can find it here: https://github.com/nicolasduminil/cqrs-showcase.</p>

<h2 id="introduction">Introduction</h2>

<p>In a <em>classical</em> order management system, by analyzing the associated data model,
we can gather a lot of information about orders and their flow in the organization.
But while we would be able to account about any order’s current status, the data
and the data model analysis wouldn’t allow us to reconstitute the story of how 
each order got to its current state.</p>

<h3 id="event-sourcing">Event sourcing</h3>

<p>The event sourcing pattern introduces the dimension of time into the data model.
Instead of a schema reflecting the orders’ current state, an event sourcing
based system persists events documenting every change in the orders’ lifecycle.
Then, by querying these events, we can reconstitute the whole story od a given 
order, or any other general aggregate, from its initial creation until its 
current status.</p>

<h3 id="cqrs">CQRS</h3>

<p>The only problem here is that querying a single aggregate instance event story 
at a time doesn’t allow us to retrieve and consolidate data relative to other
aggregates in the data model. Hence, the CQRS pattern closely related to the 
event sourcing one, designed such that to provide the possibility of materializing
projected models into logical data structures, reliable enough to support flexible
querying options.</p>

<h3 id="commands">Commands</h3>

<p>CQRS dedicates commands to execute operations that modify the system state. The 
command based execution model is then the only one able to implement business 
logic, to validate rules and to enforce invariants.</p>

<h3 id="projections">Projections</h3>

<p>The system can define as many models as required to provide data to users or to
other systems. Thus, a <em>read model</em> is a fast, denormalized and shaped pre-cached
projection containing read-only data that the application needs to answer queries.
The system project changes from the command execution model to all its read models.
The projection notion is similar to the one of materialized view in relational 
databases, meaning that whenever the source tables are updated, the changes have
to be reflected in all the read model views.</p>

<h3 id="model-segregation">Model segregation</h3>

<p>In a CQRS architecture, the responsibilities of the system’s models are segregated
according to their type. A command can only operate on its only execution model,
while a query cannot directly modify any of the system’s persisted state.</p>

<h2 id="how-this-project-illustrates-event-sourcing-and-cqrs">How this project illustrates event sourcing and CQRS</h2>

<p>This is a <em>true</em> CQRS implementation (not just a naming convention) because:</p>

<ol>
  <li>The write path never reads from the read model. <code class="language-plaintext highlighter-rouge">CommandHandler</code> reconstructs state exclusively by replaying events from the event store via <code class="language-plaintext highlighter-rouge">EventProjection.replayEvents()</code>. It never touches <code class="language-plaintext highlighter-rouge">OrderReadModel</code> or <code class="language-plaintext highlighter-rouge">OrderRepository</code>.</li>
  <li>The read path never touches the event store. <code class="language-plaintext highlighter-rouge">OrderResource.getOrderReadModel()</code> reads directly from the denormalized <code class="language-plaintext highlighter-rouge">ORDERS</code> table. It is a pure query with no business logic.</li>
  <li>There are two physically distinct storage tables: <code class="language-plaintext highlighter-rouge">EVENT_STORE</code> (write side) and <code class="language-plaintext highlighter-rouge">ORDERS</code> (read side).</li>
  <li>The read model is a projection, not a view. <code class="language-plaintext highlighter-rouge">OrderProjection</code> listens to domain events and rebuilds the read model incrementally. The <code class="language-plaintext highlighter-rouge">ORDERS</code> table could be dropped and rebuilt from scratch by replaying the event store.</li>
  <li>Commands return <code class="language-plaintext highlighter-rouge">CommandResult</code>, a sealed type that communicates success or failure without leaking state. The caller must query the read model separately if it needs current state.</li>
</ol>

<p>Let’s look now at the project’s key implemntation details:</p>

<h3 id="modeling-state-with-records">Modeling State with Records</h3>

<p><code class="language-plaintext highlighter-rouge">OrderState</code> is a Java record immutable by construction. No setters, no mutation. Every command produces a <em>new</em> state object:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="n">record</span> <span class="nf">OrderState</span><span class="o">(</span>
      <span class="no">UUID</span> <span class="n">orderId</span><span class="o">,</span>
      <span class="nc">String</span> <span class="n">customerEmail</span><span class="o">,</span>
      <span class="nc">List</span><span class="o">&lt;</span><span class="nc">OrderLine</span><span class="o">&gt;</span> <span class="n">items</span><span class="o">,</span>
      <span class="nc">OrderStatus</span> <span class="n">status</span><span class="o">,</span>
      <span class="nc">BigDecimal</span> <span class="n">total</span>
  <span class="o">)</span> <span class="o">{</span>                                                                                                                                                                                                                                       
      <span class="kd">public</span> <span class="kd">static</span> <span class="nc">OrderState</span> <span class="nf">initial</span><span class="o">(</span><span class="no">UUID</span> <span class="n">orderId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">email</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>
      <span class="kd">public</span> <span class="kd">static</span> <span class="nc">OrderState</span> <span class="nf">empty</span><span class="o">()</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span>                                                                                                                                                                                              
  <span class="o">}</span>               
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">OrderLine</code> is likewise a record with a derived field:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="n">record</span> <span class="nf">OrderLine</span><span class="o">(</span><span class="nc">String</span> <span class="n">productName</span><span class="o">,</span> <span class="kt">int</span> <span class="n">quantity</span><span class="o">,</span> <span class="nc">BigDecimal</span> <span class="n">price</span><span class="o">)</span> <span class="o">{</span>
      <span class="kd">public</span> <span class="nc">BigDecimal</span> <span class="nf">lineTotal</span><span class="o">()</span> <span class="o">{</span>
          <span class="k">return</span> <span class="n">price</span><span class="o">.</span><span class="na">multiply</span><span class="o">(</span><span class="nc">BigDecimal</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">quantity</span><span class="o">));</span>
      <span class="o">}</span>                                                                                                                                                                                                                                     
  <span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">lineTotal()</code> is a derived record component: it is computed, not stored, 
demonstrating that records can carry behavior alongside data.</p>

<h3 id="events-as-a-sealed-type-hierarchy">Events as a Sealed Type Hierarchy</h3>

<p><code class="language-plaintext highlighter-rouge">OrderEvent</code> is a sealed interface, restricting all permitted implementations 
to a known, closed set:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="n">sealed</span> <span class="kd">interface</span> <span class="nc">OrderEvent</span>
      <span class="n">permits</span> <span class="nc">OrderEvent</span><span class="o">.</span><span class="na">OrderPlaced</span><span class="o">,</span>
              <span class="nc">OrderEvent</span><span class="o">.</span><span class="na">ItemAdded</span><span class="o">,</span>                                                                                                                                                                                                         
              <span class="nc">OrderEvent</span><span class="o">.</span><span class="na">ItemRemoved</span><span class="o">,</span>
              <span class="nc">OrderEvent</span><span class="o">.</span><span class="na">OrderCancelled</span><span class="o">,</span>                                                                                                                                                                                                    
              <span class="nc">OrderEvent</span><span class="o">.</span><span class="na">OrderShipped</span> <span class="o">{</span>

      <span class="no">UUID</span> <span class="nf">orderId</span><span class="o">();</span>
      <span class="nc">OrderState</span> <span class="nf">applyTo</span><span class="o">(</span><span class="nc">OrderState</span> <span class="n">current</span><span class="o">);</span>
                                                                                                                                                                                                                                            
      <span class="n">record</span> <span class="nf">OrderPlaced</span><span class="o">(</span><span class="no">UUID</span> <span class="n">orderId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">customerEmail</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">OrderEvent</span> <span class="o">{</span>
          <span class="kd">public</span> <span class="nc">OrderState</span> <span class="nf">applyTo</span><span class="o">(</span><span class="nc">OrderState</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>                                                                                                                                                                                         
              <span class="k">return</span> <span class="nc">OrderState</span><span class="o">.</span><span class="na">initial</span><span class="o">(</span><span class="n">orderId</span><span class="o">,</span> <span class="n">customerEmail</span><span class="o">);</span>                                                                                                                                                                            
          <span class="o">}</span>
      <span class="o">}</span>                                                                                                                                                                                                                                     
      <span class="c1">// ... other event types</span>
  <span class="o">}</span>                                                                                                                                                                                                                                         
</code></pre></div></div>

<p>Using a sealed interface means the compiler enforces exhaustiveness in <code class="language-plaintext highlighter-rouge">switch</code>
expressions. Adding a new event type without handling it is a compile error, 
not a runtime surprise.</p>

<p>Each event carries only the data it needs and knows how to apply itself to the 
current state via <code class="language-plaintext highlighter-rouge">applyTo(OrderState)</code>. This is the <em>self-describing event</em> 
pattern.</p>

<h3 id="the-fold-event-replay">The Fold (Event Replay)</h3>

<p>A fold, also known as left reduction, is the process of reconstructing state 
from a list of events  over the event stream:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// EventProjection.java</span>
  <span class="kd">public</span> <span class="nc">OrderState</span> <span class="nf">replayEvents</span><span class="o">(</span><span class="nc">List</span><span class="o">&lt;</span><span class="nc">OrderEvent</span><span class="o">&gt;</span> <span class="n">events</span><span class="o">)</span> <span class="o">{</span>                                                                                                                                                                                 
      <span class="k">return</span> <span class="n">events</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
          <span class="o">.</span><span class="na">reduce</span><span class="o">(</span><span class="nc">OrderState</span><span class="o">.</span><span class="na">empty</span><span class="o">(),</span> <span class="k">this</span><span class="o">::</span><span class="n">apply</span><span class="o">,</span> <span class="o">(</span><span class="n">a</span><span class="o">,</span> <span class="n">b</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="o">);</span>                                                                                                                                                                            
  <span class="o">}</span>                                                                                                                                                                                                                                         
   
  <span class="kd">private</span> <span class="nc">OrderState</span> <span class="nf">apply</span><span class="o">(</span><span class="nc">OrderState</span> <span class="n">state</span><span class="o">,</span> <span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>                                                                                                                                                                            
      <span class="k">return</span> <span class="n">event</span><span class="o">.</span><span class="na">applyTo</span><span class="o">(</span><span class="n">state</span><span class="o">);</span>                                                                                                                                                                                                          
  <span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">OrderState.empty()</code> is the identity element or the seed. Each event is a step 
function that transforms one immutable state into the next. This is pure functional
programming: no side effects, no shared mutable state, entirely deterministic 
and testable in isolation.</p>

<h3 id="commands-as-sealed-records">Commands as Sealed Records</h3>

<p>Commands are sealed records grouped in a container interface:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="n">sealed</span> <span class="kd">interface</span> <span class="nc">Command</span>
      <span class="n">permits</span> <span class="nc">Command</span><span class="o">.</span><span class="na">PlaceOrderCommand</span><span class="o">,</span>
              <span class="nc">Command</span><span class="o">.</span><span class="na">AddItemCommand</span><span class="o">,</span>                                                                                                                                                                                                       
              <span class="nc">Command</span><span class="o">.</span><span class="na">ShipOrderCommand</span><span class="o">,</span>
              <span class="nc">Command</span><span class="o">.</span><span class="na">CancelOrderCommand</span> <span class="o">{</span>                                                                                                                                                                                                  
                  
      <span class="n">record</span> <span class="nf">PlaceOrderCommand</span><span class="o">(</span><span class="nc">String</span> <span class="n">customerEmail</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">Command</span> <span class="o">{}</span>
      <span class="n">record</span> <span class="nf">AddItemCommand</span><span class="o">(</span><span class="no">UUID</span> <span class="n">orderId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">productName</span><span class="o">,</span>
                            <span class="kt">int</span> <span class="n">quantity</span><span class="o">,</span> <span class="nc">BigDecimal</span> <span class="n">price</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">Command</span> <span class="o">{}</span>                                                                                                                                                           
      <span class="c1">// ...</span>
  <span class="o">}</span>                                                                                                                                                                                                                                         
</code></pre></div></div>

<p>Sealed records give commands value semantics, (equality by content,
<code class="language-plaintext highlighter-rouge">toString</code> for free. and type safety (exhaustive pattern matching in the handler).</p>

<h3 id="command-results-as-sealed-types">Command Results as Sealed Types</h3>

<p><code class="language-plaintext highlighter-rouge">CommandResult</code> is a sealed interface expressing all possible outcomes without 
exceptions:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="n">sealed</span> <span class="kd">interface</span> <span class="nc">CommandResult</span>
      <span class="n">permits</span> <span class="nc">CommandResult</span><span class="o">.</span><span class="na">Success</span><span class="o">,</span>
              <span class="nc">CommandResult</span><span class="o">.</span><span class="na">InvalidState</span><span class="o">,</span>
              <span class="nc">CommandResult</span><span class="o">.</span><span class="na">NotFound</span><span class="o">,</span>
              <span class="nc">CommandResult</span><span class="o">.</span><span class="na">ValidationError</span> <span class="o">{</span>

      <span class="n">record</span> <span class="nf">Success</span><span class="o">(</span><span class="no">UUID</span> <span class="n">aggregateId</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">CommandResult</span> <span class="o">{}</span>
      <span class="n">record</span> <span class="nf">InvalidState</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">CommandResult</span> <span class="o">{}</span>                                                                                                                                                                       
      <span class="n">record</span> <span class="nf">NotFound</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">CommandResult</span> <span class="o">{}</span>
      <span class="n">record</span> <span class="nf">ValidationError</span><span class="o">(</span><span class="nc">String</span> <span class="n">message</span><span class="o">)</span> <span class="kd">implements</span> <span class="nc">CommandResult</span> <span class="o">{}</span>                                                                                                                                                                    
  <span class="o">}</span>               
</code></pre></div></div>

<p>The caller can <code class="language-plaintext highlighter-rouge">switch</code> on the result exhaustively. There are no checked exceptions, 
no nullable returns, and the type system documents all possible failure modes.</p>

<h3 id="the-event-store">The Event Store</h3>

<p><code class="language-plaintext highlighter-rouge">EventStore</code> is the write-side infrastructure. It does two things atomically:</p>

<ol>
  <li>Persists the event to <code class="language-plaintext highlighter-rouge">EVENT_STORE</code> (JPA via <code class="language-plaintext highlighter-rouge">EventRepository</code>).</li>
  <li>Publishes the event to the Spring application event bus.</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">append</span><span class="o">(</span><span class="no">UUID</span> <span class="n">aggregateId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">aggregateType</span><span class="o">,</span> <span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
      <span class="kt">int</span> <span class="n">version</span> <span class="o">=</span> <span class="n">nextVersion</span><span class="o">(</span><span class="n">aggregateId</span><span class="o">);</span>                                                                                                                                                                                               
      <span class="nc">String</span> <span class="n">json</span> <span class="o">=</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">writeValueAsString</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
      <span class="nc">StoredEvent</span> <span class="n">entity</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StoredEvent</span><span class="o">(</span><span class="n">aggregateId</span><span class="o">,</span> <span class="n">aggregateType</span><span class="o">,</span>                                                                                                                                                                      
                                           <span class="n">version</span><span class="o">,</span> <span class="n">eventType</span><span class="o">,</span> <span class="n">json</span><span class="o">);</span>
      <span class="n">eventRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">entity</span><span class="o">);</span>                                                                                                                                                                                                         
      <span class="n">applicationEventPublisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span><span class="n">event</span><span class="o">);</span>
  <span class="o">}</span>                                                                                                                                                                                                                                         
</code></pre></div></div>

<p>Versioning provides a lightweight optimistic concurrency guard, by preventing 
concurrent writes from corrupting the stream, based on the unique value <code class="language-plaintext highlighter-rouge">aggregateId + version</code>.</p>

<h3 id="the-read-side-projection">The Read-Side Projection</h3>

<p><code class="language-plaintext highlighter-rouge">OrderProjection</code> is a Spring component that listens for domain events and updates
the read model:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nd">@TransactionalEventListener</span><span class="o">(</span><span class="n">phase</span> <span class="o">=</span> <span class="nc">TransactionPhase</span><span class="o">.</span><span class="na">AFTER_COMMIT</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">on</span><span class="o">(</span><span class="nc">OrderEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">OrderReadModel</span> <span class="n">model</span> <span class="o">=</span> <span class="n">orderRepository</span><span class="o">.</span><span class="na">findByOrderId</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">orderId</span><span class="o">())</span>
          <span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="k">new</span> <span class="nc">OrderReadModel</span><span class="o">());</span>                                                                                                                                                                                                    
      <span class="c1">// update fields from event ...</span>
      <span class="n">orderRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">model</span><span class="o">);</span>                                                                                                                                                                                                          
  <span class="o">}</span>               
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@TransactionalEventListener(phase = AFTER_COMMIT)</code> ensures the read model is 
only updated after the event store transaction commits successfully, preventing
this way phantom updates if the write-side transaction rolls back.</p>

<hr />

<h2 id="running-the-application">Running the Application</h2>

<p>Prerequisites: Java 21, Maven, Docker (for PostgreSQL via TestContainers in tests).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c"># Build and run all tests (requires Docker)</span>
  ./mvnw clean package
                                                                                                                                                                                                                                            
  <span class="c"># Run the application (requires a running PostgreSQL instance)</span>
  ./mvnw spring-boot:run                                                                                                                                                                                                                    
                  
  <span class="c"># Skip tests</span>
  ./mvnw clean package <span class="nt">-DskipTests</span>
</code></pre></div></div>

<hr />

<h2 id="api-reference">API Reference</h2>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th>Path</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders</code></td>
      <td>Place a new order</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}/items</code></td>
      <td>Add an item to an order</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}/ship</code></td>
      <td>Ship an order</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">POST</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}/cancel</code></td>
      <td>Cancel an order</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}</code></td>
      <td>Reconstruct current state from events</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}/events</code></td>
      <td>Retrieve the full event stream</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GET</code></td>
      <td><code class="language-plaintext highlighter-rouge">/orders/{id}/read-model</code></td>
      <td>Retrieve the denormalized read model</td>
    </tr>
  </tbody>
</table>

<p>Place an order:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="err">POST</span><span class="w"> </span><span class="err">/orders</span><span class="w">                                                                                                                                                                                                                              
  </span><span class="p">{</span><span class="w"> </span><span class="nl">"customerEmail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"alice@example.com"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Add an item:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="err">POST</span><span class="w"> </span><span class="err">/orders/</span><span class="p">{</span><span class="err">id</span><span class="p">}</span><span class="err">/items</span><span class="w">                                                                                                                                                                                                                   
  </span><span class="p">{</span><span class="w"> </span><span class="nl">"productName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget"</span><span class="p">,</span><span class="w"> </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mf">9.99</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Ship an order:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="err">POST</span><span class="w"> </span><span class="err">/orders/</span><span class="p">{</span><span class="err">id</span><span class="p">}</span><span class="err">/ship</span><span class="w">
  </span><span class="p">{</span><span class="w"> </span><span class="nl">"trackingNumber"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TRACK-001"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Spring Boot" /><category term="DDD" /><category term="CQRS" /><category term="Event Sourcing" /><category term="LinkedIn" /><summary type="html"><![CDATA[Traditional CRUD systems store only the current state of an entity. When a record is updated, the previous value is overwritten and lost forever. Event Sourcing inverts this model: instead of persisting state, the system persists the sequence of events that caused each state transition. The current state is never stored directly, but it is always derived by replaying the event history.]]></summary></entry><entry><title type="html">Rethinking Java Web UIs with Jakarta Faces and Quarkus</title><link href="https://nicolasduminil.github.io/posts-archive/quarkuspf/" rel="alternate" type="text/html" title="Rethinking Java Web UIs with Jakarta Faces and Quarkus" /><published>2026-02-11T00:00:00+00:00</published><updated>2026-02-11T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/quarkuspf</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/quarkuspf/"><![CDATA[<p>Nowadays, Java enterprise applications often default to Angular, React, or Vue for
the frontend. But for this kind of applications, the most natural UI framework
already exists in the Java ecosystem: Jakarta Faces.</p>

<p>Do enterprise grade Java Applications really need heavy JavaScript libraries ?
This is the question to which we’ll be trying to answer here.</p>

<p>Modern Java enterprise applications tend to follow a familiar pattern: a Java
backend exposing REST APIs and a JavaScript/TypeScript frontend built with some
library like Angular, React, or Vue. This architecture has become so standard
that we rarely question it.</p>

<p>But is this always the most natural choice ? Certainly not, given that the Java
ecosystem already provides a mature, productive, and integrated web UI framework:
Jakarta Faces.</p>

<p>In former times, when dinosaurs still populated the Earth, the enterprise-grade
Java applications development only required the knowledge of a single technology:
Java with possibly its enterprise extensions, appointed successively as J2EE,
Java EE, and finally Jakarta EE. Unless it was Spring, the applications and
services were deployed on Jakarta EE-compliant application servers, like
Glassfish, Payara, Wildfly, JBoss, WebLogic, WebSphere, etc. These application
servers were providing out-of-the-box all the required implementations of the
above-mentioned specifications. Among these specifications, Jakarta Faces (formerly
called JSF: <em>Java Server Faces</em>) was meant to offer a framework that facilitates
and standardizes the development of web applications in Java.</p>

<p>The Jakarta Faces history goes back to 2001 to its initial JSR (<em>Java Specifications
Request</em>) 127. At that time, another web framework, known under the name of Struts
and available under an Apache open-source license, was widely popular. As it
sometimes happens in the web frameworks space, the advent of Jakarta Faces was
perceived by the Apache community as being in conflict with Struts and, this
alleged conflict was resolved through a long and heavy negotiation process of
several years, between Sun Microsystems and the Apache community. Finally, Sun
agreed to lift the restrictions preventing JSRs from being independently implemented
under an open-source license, and the first implementation (RI <em>Reference Implementation</em>),
was provided in 2003.</p>

<p>Jakarta Faces was generally well received despite a market crowded with competitors.
Its RI was followed by other implementations over the years, starting with Apache
MyFaces in early 2004 and continuing with RedHat RichFaces in 2005, PrimeTek
PrimeFaces in 2008, ICEsoft ICEfaces and Oracle ADF Faces in 2009, OmniFaces in
2012, etc. The specifications have evolved as well, from the 1.0 released in 2001
to the 4.1 released in 2024. Hence, more than 20 years of history to advent
to the last Jakarta Faces release 4.1, a part of the Jakarta EE 11 specifications,
named Mojara.</p>

<p>The software history is sometimes convoluted. In 2010, Oracle acquired Sun Microsystems
and became the owner of the Java trademark. All along the time period that they
were under the Oracle stewardship, the Java EE specifications were in a kind of
status quo before becoming Eclipse Jakarta EE. The company didn’t really manage
to set up a dialogue with users, communities, work groups, and all those involved
in the recognition and promotion of the Java enterprise-grade services. Their
evolution requests and expectations were ignored by the editor, who didn’t know
how to deal with their new responsibility as the Java/Jakarta EE owner. In such
a way that, little by little, this has led to a guarded reaction from software
architects and developers, who began to prefer and adopt alternative technological
solutions.</p>

<p>While trying to find alternative solutions to Jakarta EE and to remedy issues like
the apparent heaviness and the expensive prices of application servers, many
software professionals have adopted Spring Boot as a development platform. Other
solutions, closer to real Jakarta EE alternatives, have emerged as well and, among
them, Netty, Quarkus, Micronaut, Helidon are the best-known and most popular. All these
solutions were based on a couple of software design principles, like single concern,
discrete boundaries, transportability across runtimes, auto-discovery, etc.,
which were known since the dawn of time. But because the software industry
continuously needs new names, the new name that has been found for these alternative
solutions was “microservices.”</p>

<p>More and more microservice architecture-based applications have appeared during the
next few years, to such an extent that the word “microservice” became one of the most
common buzzwords in the software industry. In order to optimize and standardize the
microservices technology, the Eclipse Foundation decided to apply to microservices
the same process that was used to design the Jakarta EE specifications. The Eclipse
MicroProfile was born.</p>

<p>But all these convolutions have definitely impacted the web framework technologies.
While the high majority of the Java enterprise-grade applications were using
Jakarta Faces for their web tier, switching from a software architecture based on
Jakarta EE-compliant application servers to microservices resulted in a phasing-out
of these architectures in favor of some more lightweight ones, often based on
Eclipse Microprofile specifications. And since Jakarta Faces components needed
an application server to be deployed on, or at least a servlet engine, other
lighter alternatives, based on JavaScript or TypeScript libraries, like Angular,
Vue, ExtJS, jQuery, and others, have been adopted to make up for its absence and
to become the front-end preferred stack.</p>

<p>Such applications generally require two development teams:</p>
<ul>
  <li>A front-end team specialized in JavaScript / TypeScript, Angular, CSS, and HTML development, using Node.js as a deployment platform, NPM as a build tool, Bower as a dependency management, Gulp as a streaming system, Karma and Jasmine for testing, WebPack as a code bundler, and probably many others.</li>
  <li>A back-end team specialized in Java development with Jakarta EE / Eclipse Microprofile specifications, including but not limited to MP Config, MP REST Client, MP OpenAPI, MP Health, etc. or Jakarta REST, Jakarta Persistence, Jakarta Messaging, Jakarta Security, Jakarta JSON Binding, etc.</li>
</ul>

<p>Building enterprise-grade project teams became too difficult as it
required at least two categories of profiles and, given the technology’s complexity,
the mentioned profiles should have better been seniors. Hence, the software
industry has been
facing a shortage of qualified developers which determined several organizations
to favor full-stack JavaScript / TypeScript enterprise applications. This has
lead to kind of unnatural and convoluted architectures, where the back-ends were
written in a browser dedicated programming language, with all the issues that
this choice implied, like performance, security, maintainability, etc.</p>

<p>This situation sharply contrasts with what happened in the former times when the
front-end could have been implemented using Jakarta Faces and, hence, a single
Java development team was able to take charge of such an enterprise-grade project.
Jakarta Faces is a great web framework whose implementations offer hundreds of
ready-to-use widgets and other visual controls. Compared with Angular, where the
visual components are a part of external libraries, like Material, NG-Bootstrap,
Clarity, Kendo, Nebular, and many others, Jakarta Faces implementations not only
provide ways more widgets and features but also are part of the official JSR 372
specifications and, in this respect, they are standard, as opposed to the mentioned
libraries, which evolve with their authors prevailing moods, without any guarantee
of consistency and stability.</p>

<p>The figure below shows a two typical architectures:</p>

<ul>
  <li>an SPA (<em>Single Page Application</em>) architecture based on a JavaScript front-end, and a Java backend exposing REST APIs. As you can see, it requires two codebases and, probably, two separate development teams, one for the front-end and one for the back-end, and it also requires a lot of different technologies and tools.</li>
  <li>a classical Java enterprise-grade application built with Jakarta Faces and Quarkus. The frontend is implemented using Jakarta Faces, which provides a rich set of components and features for building complex web applications. The backend is implemented using Quarkus, which provides excellent support for Jakarta Faces, via its <a href="https://quarkus.io/extensions/io.quarkiverse.primefaces/quarkus-primefaces/">PrimeFaces extension</a>, and allows for fast development and efficient performance.</li>
</ul>

<p><img src="/assets/images/fig1.png" alt="Modern Java Web UI options" title="Modern Java Web UI options" /></p>

<p>One of the criteria that has formed many organizations’ decision basis to switch
from Jakarta Faces web applications to JavaScript/TypeScript frameworks was
client-side rendering. It was considered that the server-side rendering, which
is the way the Jakarta Faces works, is less performant than the client-side
rendering, provided by the browser-based applications. This argument has to be
taken with a grain of salt:</p>

<ul>
  <li>Client-side rendering means rendering pages directly in the browser with JavaScript. All logic, data fetching, templating, and routing are handled by the client. The primary downside of this rendering type is that the amount of JavaScript required tends to grow as an application grows, which can have negative effects on a page’s capacity to consistently respond to user inputs. This becomes especially difficult with the addition of new JavaScript libraries, polyfills, and third-party code, which compete for processing power and must often be processed before a page’s content can be rendered.</li>
  <li>Server-side rendering generates the full HTML for a page on the server in response to navigation. This avoids additional round-trips for data fetching and templating on the client since it’s handled before the browser gets a response.</li>
  <li>Server-side rendering generally reduces the time required for the page content to become visible. It makes it possible to avoid sending lots of JavaScript to the client. This helps to reduce a page’s TBT (<em>Total Blocking Time</em>), which can also lead to a lower average response time as the main thread is not blocked as often during page load. When the main thread is blocked less often, user interactions will have more opportunities to run sooner.</li>
  <li>With server-side rendering, users are less likely to be left waiting for CPU-bound JavaScript to run before they can access a page.</li>
  <li>Server‑side rendering often has a better Time to First Byte (TTFB) and avoids large JavaScript bundles, which can improve perceived performance for many business use‑cases.</li>
</ul>

<p>Accordingly, the argument consisting of saying that the server-side rendering is
bad while the client-side one would be better is just a myth.</p>

<p>Consequently, it appears clearly from this analysis that developing Java web
applications using server-side rendering frameworks, like Jakarta Faces, not
only leads to more performant applications, but it’s also much simpler and less
expensive. This approach doesn’t require so many different technology stacks as
its JavaScript/TypeScript-based alternatives. The development teams don’t need
several categories of profiles, and the same developer can directly contribute
to both the front end and the back end without having to operate any paradigm
switch. This last argument is all the more important as Java developers, concerned
by things like multi-threading, transaction management, security, etc., aren’t
comfortable when it comes to command programming languages that have been designed
to run in a browser.</p>

<p>The following table summarizes the main differences between the two approaches:</p>

<table>
  <thead>
    <tr>
      <th>Criteria</th>
      <th>JavaScript/TypeScript-based front-end</th>
      <th>Jakarta Faces-based front-end</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Technology stack</td>
      <td>Multiple (Angular, React, Vue, etc.)</td>
      <td>Single (Jakarta Faces)</td>
    </tr>
    <tr>
      <td>Development teams</td>
      <td>Multiple (front-end and back-end)</td>
      <td>Single (full-stack Java)</td>
    </tr>
    <tr>
      <td>Rendering</td>
      <td>Client-side rendering</td>
      <td>Server-side rendering</td>
    </tr>
    <tr>
      <td>Teams expertise</td>
      <td>Requires expertise in multiple technologies</td>
      <td>Requires expertise in a single technology</td>
    </tr>
    <tr>
      <td>Performance</td>
      <td>Potentially lower due to client-side rendering</td>
      <td>Potentially higher due to server-side rendering</td>
    </tr>
    <tr>
      <td>Complexity</td>
      <td>Higher due to multiple technologies and teams</td>
      <td>Lower due to single technology and team</td>
    </tr>
    <tr>
      <td>Maintainability</td>
      <td>Potentially lower due to multiple codebases and technologies</td>
      <td>Potentially higher due to single codebase and technology</td>
    </tr>
    <tr>
      <td>Security</td>
      <td>Potentially lower due to client-side vulnerabilities</td>
      <td>Potentially higher due to server-side control</td>
    </tr>
    <tr>
      <td>User experience</td>
      <td>Potentially lower due to slower initial load and client-side rendering</td>
      <td>Potentially higher due to faster initial load and server-side rendering</td>
    </tr>
    <tr>
      <td>Cost</td>
      <td>Potentially higher due to multiple teams and technologies</td>
      <td>Potentially lower due to single team and technology</td>
    </tr>
    <tr>
      <td>Scalability</td>
      <td>Potentially higher due to client-side rendering</td>
      <td>Potentially lower due to server-side rendering</td>
    </tr>
    <tr>
      <td>Development speed</td>
      <td>Potentially lower due to multiple technologies and teams</td>
      <td>Potentially higher due to single technology and team</td>
    </tr>
  </tbody>
</table>

<p>So the good news here is that, if like me, you’re a nostalgic of Jakarta Faces,
from now on, you can start implementing your front-ends with it, without the need
for any Jakarta EE-compliant application server. That’s because Quarkus, our
famous Supersonic Subatomic Java platform, provides a Jakarta Faces extension,
allowing you to write beautiful front-ends like in the old good times.</p>

<p>Quarkus offers fast development mode, cloud‑native performance, and optional
native compilation, making Jakarta Faces viable even in microservices‑centric
environments. At
Melloware Inc., they provide a PrimeFaces extension for Quarkus, as described
<a href="https://github.com/quarkiverse/quarkus-primefaces">here</a>. You’ll find in the mentioned GIT repository a <a href="https://github.com/melloware/quarkus-faces">showcase</a> application that
demonstrates, with consistent code examples, how to use every single PrimeFaces
widget. Please follow the guide in the README.md file to build and run the showcase
on both an application server, like Wildfly, and in Quarkus.</p>

<p>I’ve tested it recently. Those of you having used in the past Jakarta Faces must
certainly remember the book <a href="https://www.amazon.fr/PrimeFaces-Cookbook-Second-Mert-Caliskan/dp/1784393428">“Primefaces Cookbook”</a>,
by Mert Caliskan and Oleg Varaskin, published in 2013, with a 2nd edition in 2015.
This book is one of the most comprehensive and detailed resources about
Jakarta Faces and PrimeFaces. So, I refactored all the code examples of this
book to make them work with the latest versions of Quarkus and PrimeFaces. If
you’re interested, you’ll find the project <a href="https://github.com/nicolasduminil/primefaces-showcase.git">here</a>.</p>

<p>If you want to give it a try, proceed as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ git clone https://github.com/nicolasduminil/primefaces-showcase.git
$ cd primefaces-showcase
$ mvn package
$ java -jar target/quarkus-app/quarkus-run.jar
</code></pre></div></div>

<p>Then, go to http://localhost:8080 and the following screen will be displayed in your browser:</p>

<p><img src="/assets/images/fig2.png" alt="Live samples of the PrimeFaces with Quarkus" title="Live samples of the PrimeFaces with Quarkus" /></p>

<p>Here, you are given the chance to exercice most of Jakarta Faces and its
implementation Mojara and PrimeFaces, and to see how they work in a modern Java
web application built with Quarkus. Just unfold the different nodes in the left-hand
tree and click on the different samples to see them in action. You may change
the current theme as well by clicking on the “Change Theme” button at the top
right of the page.</p>

<p>You’ll tell me what it feels like there!</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Quarkus" /><category term="Jakarta Faces" /><category term="PrimeFaces" /><category term="DZone" /><summary type="html"><![CDATA[Nowadays, Java enterprise applications often default to Angular, React, or Vue for the frontend. But for this kind of applications, the most natural UI framework already exists in the Java ecosystem: Jakarta Faces.]]></summary></entry><entry><title type="html">Building a Containerized Quarkus API and a CI/CD Pipeline on AWS EKS/Fargate with CDK</title><link href="https://nicolasduminil.github.io/posts-archive/customer-service-eks/" rel="alternate" type="text/html" title="Building a Containerized Quarkus API and a CI/CD Pipeline on AWS EKS/Fargate with CDK" /><published>2025-12-20T00:00:00+00:00</published><updated>2025-12-20T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/customer-service-eks</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/customer-service-eks/"><![CDATA[<p>In a recent <a href="http://www.simplex-software.fr/posts-archive/customer-service-ecs/">post</a>, I have demonstrated the benefits
of using AWS ECS (<em>Elastic Container Service</em>), with Quarkus and the CDK (<em>Cloud Development Kit</em>), in order to implement
an API for the customer management.</p>

<p>In the continuity of this previous post, the current one will try to go a bit further and replace ECS by EKS (<em>Elastic
Kubernetes Service</em>) as the environment for running containerized workloads. Additionally, an automated CI/CD pipeline,
using AWS CodePipeline and AWS CodeBuild, is provided.</p>

<h2 id="architecture-overview">Architecture Overview</h2>

<p>The solution that you’re about to look at implements a complete production-ready architecture consisting of:</p>

<ul>
  <li><strong>Presentation Layer</strong>: A Quarkus REST API with OpenAPI/Swagger implementing the customer management solution. This implementation is exactly the same used in the previous project which leverages ECS.</li>
  <li><strong>Application Layer</strong>: Business logic with Quarkus Panache for data access</li>
  <li><strong>Data Layer</strong>: PostgreSQL (RDS) for persistence, Redis (ElastiCache) for caching</li>
  <li><strong>Container Orchestration</strong>: AWS EKS with Fargate for serverless container execution</li>
  <li><strong>Infrastructure as Code</strong>: AWS CDK implemented in Quarkus</li>
  <li><strong>CI/CD</strong>: Automated pipeline with AWS CodePipeline, CodeBuild, and GitHub integration</li>
</ul>

<p>Before starting, a couple of explanations are probably required. As you probably know, EKS can be used with two compute
engines: EC2 or Fargate. In this example we’ve chosen to use Fargate, as it was also the case of our previous, ECS-based
project.</p>

<p>Fargate is a serverless compute engine for containers that provisions and manages the underlying infrastructure and
provides automatic scaling. It is designed to make it easy to run containers without having to manage servers or
clusters. It’s a great fit for workloads that don’t have long-running connections or require frequent scaling. This project
uses Fargate because it needs a continuously running containerized application. Fargate provides the serverless operational
model (no server management) while maintaining the traditional container execution model your Quarkus API requires.</p>

<p>The figure below shows the project’s architecture diagram:</p>

<p><img src="/assets/images/architecture-diagram.png" alt="Architecture Diagram" /></p>

<p>Please notice that, as mentioned above, several layers like: presentation, application and data are the same ones used in
the previous ECS-based example. Hence, we created a new module, called <code class="language-plaintext highlighter-rouge">customer-service-eks</code>, in the current Maven multi-module
project. This module is similar to the <code class="language-plaintext highlighter-rouge">customer-service-ecs</code> one and they both share the same presentation, application
and data layers, that have been moved in a shared Maven module, called <code class="language-plaintext highlighter-rouge">customer-service-cdk-common</code>.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>The following prerequisites are required to run this project:</p>

<ul>
  <li>Java 21+</li>
  <li>Maven 3.9+</li>
  <li>Docker</li>
  <li>AWS CLI installed and configured with appropriate credentials</li>
  <li>kubectl installed</li>
  <li>AWS CDK CLI installed</li>
  <li>GitHub account with OAuth token stored in AWS Secrets Manager</li>
</ul>

<h2 id="project-structure">Project Structure</h2>

<p>The project is structured as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>customer-service-eks/
├── src/main/java/
│ └── fr/simplex_software/workshop/customer_service_eks/
    │ └── config/
      │ └── CiCdConfig.java # CI/CD Pipeline configuration
    │ ├── CiCdPipelineStack.java # CDK Quarkus CI/CD pipeline infrastructure
    │ ├── CustomerManagementEksApp.java # Quarkus CDK application
    │ ├── CustomerManagementEksMain.java # Quarkus main application
    │ ├── CustomerManagementEksProducer.java # Quarkus CDI producer
    │ ├── EksClusterStack.java # Quarkus CDK EKS cluster infrastructure
    │ ├── MonitoringStack.java # Quarkus CDK monitoring stack infrastructure
    │ ├── VpcStack.java # Quarkus CDK VPC stack infrastructure
├── src/main/resources/
│ ├── buildspecs/
│ │ ├── build-spec.yaml # CodeBuild build specification
│ │ └── deploy-spec.yaml # CodeBuild deploy specification
│ ├── k8s/
│ │ └── customer-service.yaml # Kubernetes manifests
│ ├── scripts/  #several shell scripts
      ...
│ └── application.properties # Configuration
└── src/test/java/
    └── fr/simplex_software/workshop/customer_service_eks/tests/
        └── CustomerServiceE2EIT.java # End-to-end integration tests
</code></pre></div></div>

<h2 id="configuration">Configuration</h2>

<p>The project’s configuration is stored in two files:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">env.properties</code> file</li>
  <li>the <code class="language-plaintext highlighter-rouge">src/main/resources/application.properties</code>file.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">env.properties</code> file contains environment variables that are used by the Maven build process. Its structure is
reproduced below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CONTAINER_IMAGE_GROUP=nicolasduminil
CONTAINER_IMAGE_NAME=customers-api
CDK_DEFAULT_ACCOUNT=...
CDK_DEFAULT_REGION=eu-west-3
CDK_DEFAULT_USER=nicolas
</code></pre></div></div>

<p>The properties <code class="language-plaintext highlighter-rouge">CONTAINER_IMAGE_GROUP</code> and <code class="language-plaintext highlighter-rouge">CONTAINER_IMAGE_NAME</code> are used to build the container image and push it to
the ECR repository. They are used by the JIB Quarkus extension to build the container image. The other properties are
used by the CDK application to deploy the infrastructure and their meanings don’t require any explicit explanation.</p>

<p>The project uses AWS Secrets Manager to store sensitive data like GitHub OAuth token which is used by the CI/CD pipeline.
In order to create the secret, you can use the script <code class="language-plaintext highlighter-rouge">setup-github-token.sh</code> reproduced below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
set -e
echo "=== GitHub Token Setup for CI/CD Pipeline ==="

# Read token from stdin or argument
if [ $# -eq 0 ]; then
  if [ -t 0 ]; then
    # No arguments and no piped input
    echo "Usage:"
    echo "  $0 &lt;github-personal-access-token&gt;"
    ...
    exit 1
  else
    # Read from stdin
    GITHUB_TOKEN=$(cat | tr -d '\n\r')
  fi
else
  # Read from argument
  GITHUB_TOKEN=$1
fi

SECRET_NAME="github-oauth-token"

echo "Creating secret in AWS Secrets Manager..."

# Check if secret already exists
if aws secretsmanager describe-secret --secret-id "$SECRET_NAME" &gt;/dev/null 2&gt;&amp;1; then
  echo "Secret already exists. Updating..."
  aws secretsmanager update-secret \
    --secret-id "$SECRET_NAME" \
    --secret-string "$GITHUB_TOKEN"
else
  echo "Creating new secret..."
  aws secretsmanager create-secret \
    --name "$SECRET_NAME" \
    --description "GitHub OAuth token for CI/CD pipeline" \
    --secret-string "$GITHUB_TOKEN"
fi

echo "✅ GitHub token stored successfully!"
echo "You can now run: cdk deploy --all"
</code></pre></div></div>

<p>This script takes a parameter which could be either an argument or a piped input. The GIT OAuth token should already be
acquired from GitHub. In order to do that, proceed as follows:</p>

<ol>
  <li>Go to: https://github.com/settings/tokens”.</li>
  <li>Click <code class="language-plaintext highlighter-rouge">Generate new token (classic)</code></li>
  <li>Select <code class="language-plaintext highlighter-rouge">repo scope</code></li>
  <li>Copy the generated token.</li>
</ol>

<p>The othe configuration file, <code class="language-plaintext highlighter-rouge">src/main/resources/application.properties</code>, contains the following key properties:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># CI/CD Configuration
cdk.cicd.repository.name=${CONTAINER_IMAGE_GROUP}/${CONTAINER_IMAGE_NAME}
cdk.cicd.github.owner=${CONTAINER_IMAGE_GROUP}
cdk.cicd.github.repo=aws-cdk-quarkus
cdk.cicd.github.token-secret=github-oauth-token

# EKS Configuration
cdk.infrastructure.eks.namespace=customer-service
cdk.infrastructure.eks.cluster-name=customer-service-cluster
cdk.infrastructure.eks.service-account-name=customer-service-account
...
</code></pre></div></div>

<p>In addition to these configuration files and scripts, the class <code class="language-plaintext highlighter-rouge">CiCdConfig</code> uses the MP Config API to define properties
relative to different services and stages of the CI/CD pipeline.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@ConfigMapping(prefix = "cdk.cicd")
public interface CiCdConfig
{
  RepositoryConfig repository();
  GitHubConfig github();
  BuildConfig build();
  PipelineConfig pipeline();
  ...
}
</code></pre></div></div>

<p>As we can see, <code class="language-plaintext highlighter-rouge">CiCdConfig</code> is an interface which contains several sub-interfaces, one for each service or stage. Each
sub-interface defines a set of properties that are used to configure the corresponding service or stage, for exzmple:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
interface RepositoryConfig
{
  @WithDefault("customer-service")
  String name();
}

interface GitHubConfig
{
  @WithDefault("your-github-user")
  String owner();
  @WithDefault("customer-service")
  String repo();
  @WithDefault("github-token")
  String tokenSecret();
}
...
</code></pre></div></div>

<h2 id="the-cdk-stacks">The CDK Stacks</h2>

<p>The IaC code is organized into several CDK stacks, each responsible for a specific aspect of the infrastructure.</p>

<h3 id="the-vpcstack">The <code class="language-plaintext highlighter-rouge">VpcStack</code></h3>

<p>This stack creates the foundational networking infrastructure for the entire solution. It provisions a VPC (<em>Virtual Private
Cloud</em>) with multi-AZ (<em>Availability Zone</em>) support for high availability. The VPC is configured with both public and
private subnets across multiple availability zones, as specified by the <code class="language-plaintext highlighter-rouge">maxAzs</code> configuration property (default: 2). The
stack also creates NAT Gateways to enable outbound internet access for resources in private subnets, with the number
controlled by the <code class="language-plaintext highlighter-rouge">natGateways</code> property (default: 1). This VPC serves as the network foundation for all other stacks,
including the EKS cluster, RDS database, and ElastiCache Redis instances. The implementation is minimal, as shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vpc = Vpc.Builder.create(this, "EksVpc")
  .maxAzs(config.vpc().maxAzs())           // Default: 2 AZs
  .natGateways(config.vpc().natGateways()) // Default: 1 NAT Gateway
  .build();
</code></pre></div></div>

<p>The code above uses the <code class="language-plaintext highlighter-rouge">software.amazon.awscdk.services.ec2.Vpc</code> CDK construct that automatically creates 6 subnets across
two AZs:</p>

<ul>
  <li>
    <p>2 public subnets (one per AZ) connected to an IGW (<em>Internet Gateway</em>). An IGW is a horizontally scaled, redundant AWS-managed component that allows bidirectional communication between resources in the VPC and the internet. It enables resources with public IP addresses to receive inbound traffic from the internet and send outbound traffic to the internet. In our case, it is used as an NLB (<em>Network Load Balancer</em>) which receives external traffic.</p>
  </li>
  <li>
    <p>2 private subnets with “egress” (one per AZ) connected to a NAT Gateway. A Nat Gateway is a managed service that enables resources in private subnets to initiate outbound connections to the internet (for software updates, API calls, etc.) while preventing unsolicited inbound connections from the internet. In this context, “egress” means outbound-only traffic flow. These 2 private subnets are used for EKS Fargate pods, RDS database and ElastiCache Redis which require all outbound internet access but should not be directly accessible from the internet.</p>
  </li>
  <li>
    <p>2 isolated subnets (one per AZ). These subnets have neither IGW, nor NatGateway and, hence, they don’t have internet connectivity. They are created by default by the <code class="language-plaintext highlighter-rouge">Vpc</code> construct but they aren’t used in this project as they are typically dedicated to highly sensitive resources that should never communicate with the internet.</p>
  </li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">maxAzs</code> property (default: 2) determines how many availability zones to span for high availability. The <code class="language-plaintext highlighter-rouge">natGateways</code>
property (default: 1) controls the number of NAT Gateways - using 1 instead of 2 reduces costs but creates a single point
of failure for outbound internet connectivity.</p>

<p>This VPC serves as the network foundation for all other stacks, including the EKS cluster, RDS database, and ElastiCache
Redis instances. We need to mention that any AWS account has a default VPC and that we could have used it here, instead
of creating another one. While this alternative would have been much simpler with no additional network cost, having a
dedicated VPC is a more “production ready” solution, as it provides better isolation, customized CIDR blocks and more
subnets.</p>

<h3 id="the-eksclusterstack">The <code class="language-plaintext highlighter-rouge">EksClusterStack</code></h3>

<p>This is the core infrastructure stack that creates and configures the EKS cluster with a Fargate compute profile. The stack
performs the following several critical operations:</p>

<ol>
  <li>
    <p>creates an EKS cluster (version 1.34) with API authentication mode and public endpoint access. The cluster is deployed in the private subnets of the VPC for enhanced security.</p>
  </li>
  <li>
    <p>adds to the previous created cluster a Fargate profile that targets the <code class="language-plaintext highlighter-rouge">customer-service</code> namespace, ensuring all pods in this namespace run on Fargate serverless compute. The profile’s pod execution role is granted CloudWatch Logs permissions for centralized logging.</p>
  </li>
  <li>
    <p>sets up a Kubernetes ServiceAccount with IRSA (<em>IAM Roles for Service Accounts</em>), granting the pods secure access to AWS services without embedding credentials. The service account is granted permissions to connect to the RDS database and read secrets from AWS Secrets Manager.</p>
  </li>
  <li>
    <p>programmatically creates Kubernetes manifests including a namespace for workload isolation, a <code class="language-plaintext highlighter-rouge">ConfigMap</code> containing database and Redis connection strings, a deployment and a service resource, loaded from the YAML file in the <code class="language-plaintext highlighter-rouge">resources/k8s</code> directory.</p>
  </li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public void initStack() throws IOException
{
  createCluster();
  KubernetesManifest namespace = createNamespace();
  addFargateProfile();
  ServiceAccount serviceAccount = setupServiceAccountWithIAM();
  serviceAccount.getNode().addDependency(namespace);
  KubernetesManifest configMap = addConfigMap();
  configMap.getNode().addDependency(serviceAccount);
  addDeploymentAndService(configMap);
}
</code></pre></div></div>

<p>The stack establishes dependencies to ensure resources are created in the correct order, with the <code class="language-plaintext highlighter-rouge">ConfigMap</code> depending on
the <code class="language-plaintext highlighter-rouge">ServiceAccount</code>, and the <code class="language-plaintext highlighter-rouge">Deployment</code> depending on the <code class="language-plaintext highlighter-rouge">ConfigMap</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@SuppressWarnings("unchecked")
private void addDeploymentAndService(KubernetesManifest configMap) throws IOException
{
  List&lt;Map&lt;String, Object&gt;&gt; manifests = loadYamlManifests("k8s/customer-service.yaml");
  KubernetesManifest previous = configMap;
  for (int i = 0; i &lt; manifests.size(); i++)
  {
    KubernetesManifest current =
      cluster.addManifest("CustomerService-%d".formatted(i), manifests.get(i));
    current.getNode().addDependency(previous);
    previous = current;
  }
}
</code></pre></div></div>

<p>The code above shows how the file <code class="language-plaintext highlighter-rouge">customer-service.yaml</code>, containing the <code class="language-plaintext highlighter-rouge">ServiceAccount</code> and the <code class="language-plaintext highlighter-rouge">Deployment</code> manifests,
is parsed and the manifests added to the cluster, each one being dependent of the previous one, in order to prevent possible
cyclic dependencies.</p>

<h3 id="the-cicdpipelinestack">The <code class="language-plaintext highlighter-rouge">CiCdPipelineStack</code></h3>

<p>This stack implements a complete CI/CD pipeline using AWS native services to automate the build and deployment process.
It consists of three stages:</p>

<ol>
  <li>
    <p>Source Stage: integrates with GitHub using a webhook trigger. When code is pushed to the repository, the pipeline automatically retrieves the source code using a GitHub OAuth token stored in AWS Secrets Manager.</p>
  </li>
  <li>
    <p>Build Stage: Uses AWS CodeBuild with a Standard 7.0 Linux image to build the Quarkus application, create a Docker image using the JIB Maven plugin and push the image to Amazon ECR (<em>Elastic Container Registry</em>). The build project has privileged mode enabled for Docker operations and is granted necessary IAM permissions for ECR operations.</p>
  </li>
  <li>
    <p>Deploy Stage: Uses a separate CodeBuild project to update the kubeconfig to access the EKS cluster and apply the updated Kubernetes manifests with the new container image. The deploy project is granted EKS cluster access through IAM role assumption.</p>
  </li>
</ol>

<p>The pipeline uses build specifications defined in <code class="language-plaintext highlighter-rouge">buildspecs/build-spec.yaml</code> and <code class="language-plaintext highlighter-rouge">buildspecs/deploy-spec.yaml</code>, and
stores artifacts in S3 between stages. All configuration is externalized through the <code class="language-plaintext highlighter-rouge">CiCdConfig</code> interface using
MicroProfile Config.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  public void initStack()
  {
    IRepository ecrRepo = Repository.fromRepositoryName(this,
      "CustomerServiceRepo", cicdConfig.repository().name());

    Project buildProject = Project.Builder.create(this, "CustomerServiceBuild")
      .source(Source.gitHub(GitHubSourceProps.builder()
      ...
      .build();
    ecrRepo.grantPullPush(buildProject);
    buildProject.addToRolePolicy(PolicyStatement.Builder.create()
      .actions(List.of("ecr:GetAuthorizationToken"))
      .resources(List.of("*"))
      .build());
    buildProject.addToRolePolicy(PolicyStatement.Builder.create()
      .actions(List.of("secretsmanager:GetSecretValue"))
      .resources(List.of("arn:aws:secretsmanager:eu-west-3:" + this.getAccount() + ":secret:redhat-registry-credentials-*"))
      .build());

    Project deployProject = Project.Builder.create(this, "CustomerServiceDeploy")
      ....
      build();
    deployProject.getRole().addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName("AmazonEKSClusterPolicy"));
    eksStack.getCluster().getRole().grantAssumeRole(deployProject.getRole());
    deployProject.addToRolePolicy(PolicyStatement.Builder.create()
      .actions(List.of("eks:DescribeCluster"))
      .resources(List.of(eksStack.getCluster().getClusterArn()))
      .build());

    GitHubSourceAction sourceAction = GitHubSourceAction.Builder.create()
      .actionName(cicdConfig.pipeline().actions().source())
      ...
     .build();

    CodeBuildAction buildAction = CodeBuildAction.Builder.create()
      .actionName(cicdConfig.pipeline().actions().build())
      ...
      .build();

    CodeBuildAction deployAction = CodeBuildAction.Builder.create()
      .actionName(cicdConfig.pipeline().actions().deploy())
      ...
      .build();

    Pipeline pipeline = Pipeline.Builder.create(this, cicdConfig.pipeline().name())
      .build();
    pipeline.addStage(StageOptions.builder()
      .stageName(cicdConfig.pipeline().stages().source())
      .actions(List.of(sourceAction))
      .build());
    pipeline.addStage(StageOptions.builder()
      .stageName(cicdConfig.pipeline().stages().build())
      .actions(List.of(buildAction))
      .build());
    pipeline.addStage(StageOptions.builder()
      .stageName(cicdConfig.pipeline().stages().deploy())
      .actions(List.of(deployAction))
      .build());
  }
</code></pre></div></div>

<p>The code above create two CodeBuild projects: a build and a deploy one. It assigns to them the required security policies,
like <code class="language-plaintext highlighter-rouge">AmazonEKSClusterPolicy</code> and it creates then three actions: one <code class="language-plaintext highlighter-rouge">GitHubSourceAction</code> and two <code class="language-plaintext highlighter-rouge">CodeBuildAction</code>, one
for the build and the other one for the deploy operation. Last but not least, a <code class="language-plaintext highlighter-rouge">Pipeline</code> is created and the three mentioned
actions are added as its stages.</p>

<h3 id="the-monitoringstack">The <code class="language-plaintext highlighter-rouge">MonitoringStack</code></h3>

<p>This stack provides observability and monitoring capabilities for the EKS cluster and running applications. It creates
a dedicated CloudWatch log group named <code class="language-plaintext highlighter-rouge">/aws/eks/customer-service</code> with a one-week retention policy to collect and store
logs from the EKS pods and cluster components and a CloudWatch dashboard named <code class="language-plaintext highlighter-rouge">customer-service-eks</code> that visualizes key
metrics including but not limited to pod CPU utilization from the EKS namespace.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public void initStack()
{
  LogGroup.Builder.create(this, "EksLogGroup")
    .logGroupName("/aws/eks/customer-service")
    .retention(RetentionDays.ONE_WEEK)
    .build();
  Dashboard dashboard = Dashboard.Builder.create(this, "CustomerServiceDashboard")
  .dashboardName("customer-service-eks")
  .build();
  dashboard.addWidgets(
    GraphWidget.Builder.create()
      .title("Pod CPU Utilization")
      .left(List.of(Metric.Builder.create()
        .namespace("AWS/EKS")
        .metricName("pod_cpu_utilization")
        .build()))
      .build()
  );
}
</code></pre></div></div>

<p>This stack depends on the EksClusterStack to ensure the cluster exists before monitoring resources are created. The
monitoring infrastructure enables real-time visibility into cluster health, performance metrics, and troubleshooting
capabilities through centralized log aggregation.</p>

<h2 id="building-deploying-and-testing">Building, deploying and testing</h2>

<p>The API to be built a deployed on EKS with Fargate is the same as the one we used previously for the ECS project (see the
<code class="language-plaintext highlighter-rouge">customer-service-api</code> module). Other shared artifacts are provided by the <code class="language-plaintext highlighter-rouge">customer-service-cdk-common</code> module. Here is
their list:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">DatabaseConstruct</code>: implements a CDK construct for RDS (<em>Relational Database Service</em>) with PostgreSQL;</li>
  <li><code class="language-plaintext highlighter-rouge">RedisCluster</code>: implements a CDK construct for ElasticCache with Redis;</li>
  <li><code class="language-plaintext highlighter-rouge">RedisClusterProps</code>: gropus together, in one record, several common Redis properties like the cluster ID, the number of nodes, their types, etc.</li>
  <li><code class="language-plaintext highlighter-rouge">DatabaseStack</code>: implements a database CDK stack which includes the previous mentioned PostgreSQL and Redis constructs.</li>
</ul>

<p>Since these common artifacts are all required in order to build and deploy our stack, they need to be installed in the
local Maven repository:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd aws-cdk-quarkus/customer-service-api
$ mvn clean install
$ cd aws-cdk-quarkus/customer-service-cdk-common
$ mvn clean install
</code></pre></div></div>

<p>The Maven build process of the <code class="language-plaintext highlighter-rouge">customer-service-api</code> will run 2 integration tests, one using RESTassured against the
Quarkus embedded web service, the other against a full local containerized infrastructure described by a <code class="language-plaintext highlighter-rouge">docker-compose.yaml</code>
file. This has been fully documented and explained in te 1st part of this series.</p>

<p>Now, we can build, deploy and test our new stack. The <code class="language-plaintext highlighter-rouge">customer-service-eks</code> module provides two ways to do it:</p>

<ul>
  <li>in development mode, using minikube;</li>
  <li>in production mode, using AWS infrastructure;</li>
</ul>

<p>Please notice that <code class="language-plaintext highlighter-rouge">localstack</code>, which is a very practical way to test AWS based IaC code without the cloud heavyness and
costs, isn’t an option here, as it doesn’t support EKS, VPC, ECR, etc.</p>

<h3 id="building-deploying-and-testing-in-dev-mode">Building, deploying and testing in dev mode</h3>

<p>As mentioned, using the dev mode, all our stacks are deployed locally, on minikube. So, this mode requires minikube to be
installed and running.</p>

<p>The <code class="language-plaintext highlighter-rouge">pom.xml</code> file defines two profiles:</p>

<ul>
  <li>a dev mode one named <code class="language-plaintext highlighter-rouge">local</code>;</li>
  <li>a prod mode one named <code class="language-plaintext highlighter-rouge">e2e</code>;</li>
</ul>

<p>Here is the dev mode one, which is also the default one:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;profile&gt;
  &lt;id&gt;local&lt;/id&gt;
  &lt;activation&gt;
    &lt;activeByDefault&gt;true&lt;/activeByDefault&gt;
  &lt;/activation&gt;
  &lt;build&gt;
    &lt;plugins&gt;
      &lt;plugin&gt;
        &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
        &lt;artifactId&gt;exec-maven-plugin&lt;/artifactId&gt;
        &lt;executions&gt;
          &lt;execution&gt;
            &lt;id&gt;start-minikube&lt;/id&gt;
            &lt;phase&gt;pre-integration-test&lt;/phase&gt;
            &lt;goals&gt;&lt;goal&gt;exec&lt;/goal&gt;&lt;/goals&gt;
            &lt;configuration&gt;
              &lt;executable&gt;minikube&lt;/executable&gt;
              &lt;arguments&gt;
                &lt;argument&gt;start&lt;/argument&gt;
                &lt;argument&gt;--driver=docker&lt;/argument&gt;
              &lt;/arguments&gt;
            &lt;/configuration&gt;
          &lt;/execution&gt;
          &lt;execution&gt;
            &lt;id&gt;deploy-to-minikube&lt;/id&gt;
            &lt;phase&gt;pre-integration-test&lt;/phase&gt;
            &lt;goals&gt;&lt;goal&gt;exec&lt;/goal&gt;&lt;/goals&gt;
            &lt;configuration&gt;
              &lt;executable&gt;bash&lt;/executable&gt;
              &lt;arguments&gt;
                &lt;argument&gt;src/main/resources/scripts/deploy-to-minikube.sh&lt;/argument&gt;
              &lt;/arguments&gt;
            &lt;/configuration&gt;
          &lt;/execution&gt;
          &lt;execution&gt;
            &lt;id&gt;stop-minikube&lt;/id&gt;
            &lt;phase&gt;clean&lt;/phase&gt;
            &lt;goals&gt;&lt;goal&gt;exec&lt;/goal&gt;&lt;/goals&gt;
            &lt;configuration&gt;
              &lt;executable&gt;minikube&lt;/executable&gt;
              &lt;arguments&gt;
                &lt;argument&gt;delete&lt;/argument&gt;
              &lt;/arguments&gt;
            &lt;/configuration&gt;
          &lt;/execution&gt;
        &lt;/executions&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
  &lt;/build&gt;
&lt;/profile&gt;
</code></pre></div></div>

<p>As you can see, here we’re using the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> with 3 executions that starts minikube, deploy to minikube and,
respectively, stop minikube. As already mentioned, minikube should be installed in order that the <code class="language-plaintext highlighter-rouge">local</code> profile be
effective and, the action with ID <code class="language-plaintext highlighter-rouge">start-minikube</code> simply executes the <code class="language-plaintext highlighter-rouge">start</code> command.</p>

<p>Once minikube started, the action with ID <code class="language-plaintext highlighter-rouge">deploy-to-minikube</code> executes the <code class="language-plaintext highlighter-rouge">deploy-to-minikube.sh</code> script, shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
set -e

echo "&gt;&gt;&gt; Loading image..."
docker save nicolasduminil/customers-api:1.0-SNAPSHOT | minikube image load -

echo "&gt;&gt;&gt; Creating namespace..."
kubectl create namespace customer-service --dry-run=client -o yaml | kubectl apply -f -

echo "&gt;&gt;&gt; Deploying PostgreSQL and Redis..."
kubectl apply -f src/test/resources/k8s/postgres-redis.yaml

echo "&gt;&gt;&gt; Waiting for database..."
for i in {1..5}; do
  if kubectl get pod -l app=postgres -n customer-service 2&gt;/dev/null | grep -q postgres; then
    break
  fi
  echo "Waiting for postgres pod to be created... ($i/5)"
  sleep 5
done

kubectl wait --for=condition=ready pod -l app=postgres -n customer-service --timeout=60s

echo "&gt;&gt;&gt; Deploying application..."
kubectl apply -f target/kubernetes/minikube.yml

echo "&gt;&gt;&gt; Waiting for application..."
for i in {1..5}; do
  if kubectl get pod -l app.kubernetes.io/name=customer-service-api -n customer-service 2&gt;/dev/null | grep -q customer-service; then
    break
  fi
  echo "Waiting for app pod to be created... ($i/5)"
  sleep 5
done
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=customer-service-api -n customer-service --timeout=120s

echo "&gt;&gt;&gt; Final status:"
kubectl get all -n customer-service

echo "&gt;&gt;&gt; Starting port-forward..."
kubectl port-forward -n customer-service service/customer-service-api 9090:80 &gt; /dev/null 2&gt;&amp;1 &amp;
echo "Port-forward started (PID: $!)"
sleep 2
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">deploy-to-minikube.sh</code> script above is structured for performing several operations. First, the Docker image
<code class="language-plaintext highlighter-rouge">nicolasduminil/customers-api:1.0-SNAPSHOT</code> built during the previous step (the shared components) is loaded to minikube
via the command <code class="language-plaintext highlighter-rouge">image load</code>. Then, the <code class="language-plaintext highlighter-rouge">kubectl</code> tool, which is another prerequisite, is used to create the customized
namespace <code class="language-plaintext highlighter-rouge">customer-service</code> and to apply the two manifests: <code class="language-plaintext highlighter-rouge">postgresql-manifest.yaml</code> and <code class="language-plaintext highlighter-rouge">minikube.yml</code>. Last but not
least, after having waited that all the services be on, the same <code class="language-plaintext highlighter-rouge">kubectl</code> is used to start the port-forward process.</p>

<p>At that point we’re able to test our API locally deployed on minikube using the Swagger UI. Fire your preferred browser
at http://localhost:9090/q/swagger-ui to take advantage of the 80 to 9090 port-forward. You’re ready to test the API.</p>

<p>Please notice that the <code class="language-plaintext highlighter-rouge">minikube.yml</code> manifest file mentioned above is automatically generated by the JIB extension for
Quarkus, while the <code class="language-plaintext highlighter-rouge">postgres-redis.yaml</code> was written on the purpose, to define the Kubernetes deployment and service
controllers associated to the PostgreSQL databse and Redis cache. Don’t hesitate to have a look at this file and make sure
you understand what everything is about there.</p>

<h3 id="building-deploying-and-testing-in-prod-mode">Building, deploying and testing in prod mode</h3>

<p>While the Maven building process is the same and consists in running</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mvn -Pe2e -DskipTests clean install
</code></pre></div></div>

<p>deploying is, this time, a much longer and heavier operation as it targets real AWS infrastructure. Look at the <code class="language-plaintext highlighter-rouge">e2e</code>
Maven profile below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;profile&gt;
  &lt;id&gt;e2e&lt;/id&gt;
  &lt;build&gt;
    &lt;plugins&gt;
      &lt;plugin&gt;
        &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
        &lt;artifactId&gt;exec-maven-plugin&lt;/artifactId&gt;
        &lt;executions&gt;
          &lt;execution&gt;
            &lt;id&gt;deploy-to-aws&lt;/id&gt;
            &lt;phase&gt;pre-integration-test&lt;/phase&gt;
            &lt;goals&gt;
              &lt;goal&gt;exec&lt;/goal&gt;
            &lt;/goals&gt;
            &lt;configuration&gt;
              &lt;executable&gt;bash&lt;/executable&gt;
              &lt;arguments&gt;
                &lt;argument&gt;./src/main/resources/scripts/deploy-to-aws.sh&lt;/argument&gt;
              &lt;/arguments&gt;
              &lt;workingDirectory&gt;${project.basedir}&lt;/workingDirectory&gt;
              &lt;environmentVariables&gt;
                &lt;CDK_DEFAULT_ACCOUNT&gt;${CDK_DEFAULT_ACCOUNT}&lt;/CDK_DEFAULT_ACCOUNT&gt;
                &lt;CDK_DEFAULT_REGION&gt;${CDK_DEFAULT_REGION}&lt;/CDK_DEFAULT_REGION&gt;
                &lt;CDK_DEFAULT_USER&gt;${CDK_DEFAULT_USER}&lt;/CDK_DEFAULT_USER&gt;
                &lt;CONTAINER_IMAGE_GROUP&gt;${CONTAINER_IMAGE_GROUP}&lt;/CONTAINER_IMAGE_GROUP&gt;
                &lt;CONTAINER_IMAGE_NAME&gt;${CONTAINER_IMAGE_NAME}&lt;/CONTAINER_IMAGE_NAME&gt;
                &lt;CONTAINER_PORT&gt;${CONTAINER_PORT}&lt;/CONTAINER_PORT&gt;
              &lt;/environmentVariables&gt;
            &lt;/configuration&gt;
          &lt;/execution&gt;
        &lt;/executions&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
  &lt;/build&gt;
&lt;/profile&gt;
</code></pre></div></div>

<p>What this profile is doing is simply running the <code class="language-plaintext highlighter-rouge">deploy-to-aws.sh</code> script via the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
set -e
...
../customer-service-cdk-common/src/main/resources/scripts/deploy-ecr.sh

echo "&gt;&gt;&gt; Updating kubeconfig..."
aws eks update-kubeconfig --region $CDK_DEFAULT_REGION --name customer-service-cluster

echo "&gt;&gt;&gt; Checking EKS access..."
USER_ARN=$(aws sts get-caller-identity --query 'Arn' --output text)
grant_eks_access "$USER_ARN" "current user"

echo "&gt;&gt;&gt; Granting EKS access to CodeBuild deploy role..."
DEPLOY_ROLE_ARN=$(aws iam list-roles --query 'Roles[?contains(RoleName, `CustomerServiceDeployRole`)].Arn' --output text --region $CDK_DEFAULT_REGION)
if [ -n "$DEPLOY_ROLE_ARN" ]; then
  grant_eks_access "$DEPLOY_ROLE_ARN" "deploy role"
else
  echo "&gt;&gt;&gt; Deploy role not found (pipeline not deployed yet)"
fi

echo "&gt;&gt;&gt; Retrieving database password from Secrets Manager..."
SECRET_ARN=$(jq -r '.DatabaseStack.DatabaseSecretArn' cdk-outputs.json)
DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN --region $CDK_DEFAULT_REGION --query SecretString --output text | jq -r .password)

echo "&gt;&gt;&gt; Creating Kubernetes secret with database password..."
kubectl create secret generic db-credentials \
  --from-literal=QUARKUS_DATASOURCE_PASSWORD="$DB_PASSWORD" \
  -n customer-service --dry-run=client -o yaml | kubectl apply -f -

echo "&gt;&gt;&gt; Waiting for pods to be ready..."
kubectl wait --for=condition=ready pod -l app=customer-service-api -n customer-service --timeout=300s || true

echo "&gt;&gt;&gt; Deployment complete!"
echo "&gt;&gt;&gt; To access the API locally, run:"
echo "&gt;&gt;&gt;   ./src/main/resources/scripts/test-api.sh"
echo "&gt;&gt;&gt; Then test with:"
echo "&gt;&gt;&gt;   curl http://localhost:8080/q/health"
</code></pre></div></div>

<p>The script above contains several distinct sections. First, it runs the shared script <code class="language-plaintext highlighter-rouge">deploy-ecr.sh</code>, present in the
<code class="language-plaintext highlighter-rouge">customer-service-cdk-common</code> module, which deploys to ECR (<em>Elastic Container Registry</em>) the image
<code class="language-plaintext highlighter-rouge">nicoladuminil/customer-service-api::1.0-SNAPSHOT</code>, built previously, before running the <code class="language-plaintext highlighter-rouge">cdk deploy</code> command, which
deploys to AWS all the CloudFormation stacks. This process is very complex and long and, depending on your network speed
, it may take 15 - 20 minutes.</p>

<p>Then the script updates the <code class="language-plaintext highlighter-rouge">.kube/config</code> file with the EKS cluster required parameters, such that it could be handled
further by <code class="language-plaintext highlighter-rouge">kubectl</code>. Next it grants the <code class="language-plaintext highlighter-rouge">AmazonEKSClusterAdminPolicy</code> to the current user and the deployer user,
identified by the <code class="language-plaintext highlighter-rouge">CustomerServiceDeployRole</code>. Then it gets the AWS secret containing the PostgreSQL database user
password and creates a Kubernetes secret to be used by the associated pod. Once that all the pods started and are healthy,
the script displays instructions of how to proceed further for testing purposes.</p>

<p>Several tests are available, once that the deployment process has succeeded. First, an e2e test, named <code class="language-plaintext highlighter-rouge">CustomerServiceE2EIT</code>
can be run as folowws:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mvn -Pe2e failsafe:integration-test
</code></pre></div></div>

<p>Here is the listing:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class CustomerServiceE2EIT extends AbstractCustomerServiceE2E
{
  private static Process portForwardProcess;

  @BeforeAll
  static void setup() throws Exception
  {
    startPortForward();
    configureEndpoint("localhost:8080");
    waitForServiceReady();
  }

  @AfterAll
  static void teardown()
  {
    if (portForwardProcess != null &amp;&amp; portForwardProcess.isAlive())
    {
      portForwardProcess.destroy();
      System.out.println("&gt;&gt;&gt; Port-forward stopped");
    }
  }

  private static void startPortForward() throws Exception
  {
    System.out.println("&gt;&gt;&gt; Waiting for deployment to be ready...");
    Process waitProcess = new ProcessBuilder(
      "kubectl", "wait", "--for=condition=Available",
      "deployment/customer-service-api-deployment",
      "-n", "customer-service",
      "--timeout=300s"
      ).start();

    if (waitProcess.waitFor() != 0)
      throw new RuntimeException("### Deployment not available");

    System.out.println("&gt;&gt;&gt; Starting port-forward...");
    portForwardProcess = new ProcessBuilder(
      "kubectl", "port-forward",
      "deployment/customer-service-api-deployment",
      "8080:8080",
      "-n", "customer-service"
    ).start();

    Thread.sleep(3000);
    System.out.println("&gt;&gt;&gt; Port-forward established on localhost:8080");
  }
}
</code></pre></div></div>

<p>As you can see, the test extends the <code class="language-plaintext highlighter-rouge">AbstractCustomerServiceE2E</code> present in the shared module <code class="language-plaintext highlighter-rouge">customer-service-cdk-common</code>.
This abstract class defines the test case to be run as they are the same whatever the cloud runtime is, be it ECS or EKS.
The only operation specific to the cloud runtime is the port-forward process start, implemented by the method
<code class="language-plaintext highlighter-rouge">startPortForward()</code>.</p>

<p>Of course, you can test your API using the Swagger UI, as you did before, in dev mode. The only thing you need to do is
to start the port-forward and, for this, the script <code class="language-plaintext highlighter-rouge">test-api.sh</code>, here below, comes very handy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
echo "&gt;&gt;&gt; Starting port-forward to access API locally..."
echo "&gt;&gt;&gt; API will be available at http://localhost:8080"

nohup kubectl port-forward svc/customer-service-api-service -n customer-service 8080:80 2&gt;/dev/null &amp;
</code></pre></div></div>

<p>Then fire your preferred browser, as usual, at http://localhost:8080/q/swagger-ui. Other test scripts, like
<code class="language-plaintext highlighter-rouge">load-distribution-demo.sh</code>, <code class="language-plaintext highlighter-rouge">perf-demo.sh</code>, <code class="language-plaintext highlighter-rouge">pods-monitoring.sh</code>, <code class="language-plaintext highlighter-rouge">scaling-demo.sh</code>, are available as well,
just run them.</p>

<p>Once you finished testing, please cancel the port-forwarding by running:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ pkill -f "kubectl port-forward"
</code></pre></div></div>

<p>And don’t forget to clean up your cloud by running:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd aws-cdk-quarkus/customer-service/eks
$ cdk destroy --all --force
</code></pre></div></div>

<p>A script named <code class="language-plaintext highlighter-rouge">destroy-all.sh</code> is also available for a more atomic destruction.</p>

<h4 id="troubleshooting-in-prod-mode">Troubleshooting in prod mode</h4>

<p>Working in prod mode, i.e. running the API and the associated tests against real AWS infrastructure, is challenging.
The environment is very complex and, at any step, dozens of issues might prevent things to happen as expected. Hence, the
necessity to be able to visualize the cloud infrastructure current status and the most recent events.</p>

<p>While the AWS Console is a very usefull tool, designed on the purpose to optimize the visualization of the cloud
infrastructure status, using the <code class="language-plaintext highlighter-rouge">kubectl</code> utility represents the Kubernetes most traditional way to check the cluster
health. Also, AWS CLI is able to perform all the AWS Console functions, in a less intuitive way, which requires a deep
knowledge, but which might be more practical, less repetitive as scriptable, and less error-prone.Accordingly, a <code class="language-plaintext highlighter-rouge">kubectl</code>
and AWS CLI commands breviary could be helpful in order to fix issues.</p>

<h5 id="verifying-the-eks-cluster-deployment">Verifying the EKS cluster deployment</h5>

<p>The following <code class="language-plaintext highlighter-rouge">kubectl</code> commands can be used to verify the EKS cluster deployment:</p>

<ol>
  <li>Check the cluster:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get nodes -n customer-service
</code></pre></div></div>

<ol>
  <li>Check the pods:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl get pods -n customer-service
</code></pre></div></div>

<ol>
  <li>Check the services:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get services -n customer-service
</code></pre></div></div>

<ol>
  <li>View all logs from pods:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl logs -f -l app=customer-service-api -n customer-service
</code></pre></div></div>

<ol>
  <li>Check rollout status:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl rollout status deployment/customer-service-api-deployment -n customer-service
</code></pre></div></div>

<ol>
  <li>View deployment details:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe deployment customer-service-api-deployment -n customer-service
</code></pre></div></div>

<ol>
  <li>Verify ECR repository exists:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws ecr describe-repositories --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Check IAM permissions</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws iam get-role-policy --role-name &lt;build-role-name&gt; --policy-name &lt;policy-name&gt;
</code></pre></div></div>

<ol>
  <li>Verify EKS cluster access</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws eks describe-cluster --name customer-service-cluster --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Check pod events</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ kubectl describe pod &lt;pod-name&gt; -n customer-service
</code></pre></div></div>

<ol>
  <li>Verify RDS endpoint</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws rds describe-db-instances --region eu-west-3
</code></pre></div></div>

<h5 id="verifying-th-cicd-pipeline-deployment">Verifying th CI/CD Pipeline deployment</h5>

<ol>
  <li>Get the webhook URL:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws codepipeline list-webhooks --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Check pipeline execution status:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ PIPELINE=$(aws codepipeline list-pipelines --region eu-west-3 --query 'pipelines[?starts_with(name, `CiCdPipelineStack`)].name' --output text)
$ aws codepipeline get-pipeline-state --name $PIPELINE --region eu-west-3
</code></pre></div></div>

<ol>
  <li>List recent pipeline executions:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws codepipeline list-pipeline-executions --pipeline-name $PIPELINE --region eu-west-3 --max-items 5
</code></pre></div></div>

<ol>
  <li>Get CodeBuild project names:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws codebuild list-projects --region eu-west-3 --query 'projects[?contains(@, `CustomerService`)]'
</code></pre></div></div>

<ol>
  <li>Check build project status:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ BUILD_PROJECT=$(aws codebuild list-projects --region eu-west-3 --query 'projects[?contains(@, `CustomerServiceBuild`)]' --output text)
$ aws codebuild batch-get-projects --names $BUILD_PROJECT --region eu-west-3
</code></pre></div></div>

<ol>
  <li>List recent builds:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws codebuild list-builds-for-project --project-name $BUILD_PROJECT --region eu-west-3 --max-items 5
</code></pre></div></div>

<ol>
  <li>Get detailed build information:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ BUILD_ID=$(aws codebuild list-builds-for-project --project-name $BUILD_PROJECT --region eu-west-3 --query 'ids[0]' --output text)
$ aws codebuild batch-get-builds --ids $BUILD_ID --region eu-west-3
</code></pre></div></div>

<ol>
  <li>View build logs:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws logs tail /aws/codebuild/$BUILD_PROJECT --since 30m --follow --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Check deploy project status:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ DEPLOY_PROJECT=$(aws codebuild list-projects --region eu-west-3 --query 'projects[?contains(@, `CustomerServiceDeploy`)]' --output text)
$ aws codebuild list-builds-for-project --project-name $DEPLOY_PROJECT --region eu-west-3 --max-items 5
</code></pre></div></div>

<ol>
  <li>View deploy logs:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws logs tail /aws/codebuild/$DEPLOY_PROJECT --since 30m --follow --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Verify GitHub OAuth token secret:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws secretsmanager describe-secret --secret-id github-oauth-token --region eu-west-3
</code></pre></div></div>

<ol>
  <li>Check CodeBuild service role permissions:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ BUILD_ROLE=$(aws iam list-roles --query 'Roles[?contains(RoleName, `CustomerServiceBuildRole`)].RoleName' --output text)
$ aws iam list-attached-role-policies --role-name $BUILD_ROLE
$ aws iam list-role-policies --role-name $BUILD_ROLE
</code></pre></div></div>

<ol>
  <li>Check deploy role EKS access:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ DEPLOY_ROLE_ARN=$(aws iam list-roles --query 'Roles[?contains(RoleName, `CustomerServiceDeployRole`)].Arn' --output text)
$ aws eks list-access-entries --cluster-name customer-service-cluster --region eu-west-3
</code></pre></div></div>

<h5 id="verifying-the-monitor-pipeline">Verifying the Monitor Pipeline</h5>

<ol>
  <li>List the existent pipeleines</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws codepipeline list-pipelines --region eu-west-3 --query 'pipelines[?starts_with(name, `CiCdPipelineStack`)].name' --output text
CiCdPipelineStack-CustomerServicePipelineB3195C39-t9UMJeMAQlDN
</code></pre></div></div>

<ol>
  <li>Get the monitoring pipeline status</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ aws codepipeline get-pipeline --name $PIPELINE --region eu-west-3 --query 'pipeline.stages[?name==`Build`].actions[0].configuration.ProjectName' --output text
CustomerServiceBuild0A9B7C3-YIk2RDA0JP1B
</code></pre></div></div>

<ol>
  <li>View the monitoring pipeline log file</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws logs tail /aws/codebuild/CustomerServiceBuild0A9B7C3-YIk2RDA0JP1B --since 30m --follow --region eu-west-3
</code></pre></div></div>

<h2 id="conclusions">Conclusions</h2>

<p>This project demonstrates a production-ready approach to deploying containerized Quarkus applications on AWS EKS with Fargate, leveraging Infrastructure as Code through the AWS CDK. By combining Kubernetes orchestration with serverless compute, we achieve operational simplicity without sacrificing the flexibility and portability that Kubernetes provides.</p>

<p>The automated CI/CD pipeline ensures consistent deployments from code commit to production, while the comprehensive monitoring and troubleshooting capabilities enable reliable operations at scale. Whether you’re migrating from ECS to EKS or building cloud-native applications from scratch, this architecture provides a solid foundation for modern microservices deployment on AWS.</p>

<p><a href="https://github.com/nicolasduminil/aws-cdk-quarkus.git">Source code</a></p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Quarkus" /><category term="AWS" /><category term="EKS" /><category term="CodeBuild" /><category term="CodeDeploy" /><category term="ECR" /><category term="Fargate" /><category term="DZone" /><summary type="html"><![CDATA[In a recent post, I have demonstrated the benefits of using AWS ECS (Elastic Container Service), with Quarkus and the CDK (Cloud Development Kit), in order to implement an API for the customer management.]]></summary></entry><entry><title type="html">Building a Containerized Quarkus API on AWS ECS/Fargate with CDK</title><link href="https://nicolasduminil.github.io/posts-archive/customer-service-ecs/" rel="alternate" type="text/html" title="Building a Containerized Quarkus API on AWS ECS/Fargate with CDK" /><published>2025-11-16T00:00:00+00:00</published><updated>2025-11-16T13:05:34+00:00</updated><id>https://nicolasduminil.github.io/posts-archive/customer-service-ecs</id><content type="html" xml:base="https://nicolasduminil.github.io/posts-archive/customer-service-ecs/"><![CDATA[<p>In a three articles series published recently on this site (<a href="https://dzone.com/articles/aws-cdk-infrastructure-as-abstract-data-types">Part 1</a>,
<a href="https://dzone.com/articles/aws-cdk-infrastructure-as-abstract-data-types-pt-2">Part 2</a>, <a href="https://dzone.com/articles/aws-cdk-infrastructure-as-abstract-data-types-3">Part 3</a>),
I’ve been demonstrating the power of the AWS Cloud Development Kit (CDK) in the
Infrastructure as Code (IaC) area, especially when coupled with the ubiquitous
Java and its supersonic / subatomic cloud-native stack: Quarkus.</p>

<p>While focusing on the CDK fundamentals in Java, like <code class="language-plaintext highlighter-rouge">Stack</code> and <code class="language-plaintext highlighter-rouge">Construct</code>,
together with their Quarkus implementations, this series was a bit frugal as far
as the infrastructure elements were concerned. Indeed, for the sake of the clarity
and simplification, the infrastructure used to illustrate how to use the CDK with
Java and Quarkus was inherently consensual. Hence, the idea of a new series, of
which this article is the first one, a series less concerned by CDK internals
and more dedicated to the infrastructure itself.</p>

<p>This first article demonstrates how to build and deploy a modern, cloud-native
customer management system using Quarkus, AWS CDK, and ECS/Fargate. It covers
the complete journey from application development to infrastructure as code,
containerization and comprehensive testing strategies. Once again, it doesn’t
emphasise on the exposed API and its possible business value, but rather on the
infrastructure elements required in order to provide the global solution in practice.</p>

<h2 id="architecture-overview">Architecture Overview</h2>

<p>The diagram below shows an overview of the project’s architecture:</p>

<p><img src="/assets/images/architecture.png" alt="Architecture Diagram" /></p>

<p>This presented solution implements the following architecture layers:</p>
<ul>
  <li>Presentation Layer : A Quarkus REST API exposing, as an example, a couple of simple customer management endpoints.</li>
  <li>Application Layer : A Quarkus main application running on ECS Fargate</li>
  <li>Data Layer : PostgreSQL (RDS) for persistence, Redis (ElastiCache) for caching</li>
  <li>Infrastructure Layer (Iaas): The AWS CDK-managed cloud infrastructure implemented in Quarkus</li>
</ul>

<p>Let’s try now to look in more details at these layers.</p>

<h3 id="the-presentation-layer">The Presentation Layer</h3>

<p>This layer is a Quarkus REST API which exposes a couple os simple endpoints to
CRUD customers. More than a real business API, this one is an example allowing
to illustrate how containerized applications could be deployed and hosted in an
AWS ECS (Elastic Container Service).</p>

<p>In order to separate concerns, our Maven project is structured in two modules:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">customer-service-ecs-api</code> module which implements the Quarkus REST API to be deployed and executed as a Docker image in the AWS ECS service;</li>
  <li>the <code class="language-plaintext highlighter-rouge">customer-service-exce-cdk</code> module which bootstraps the CDK and creates the required elements in order to implement the cloud infrastructure presented in the figure above.</li>
</ul>

<p>The Presentation Layer is contained in the <code class="language-plaintext highlighter-rouge">customer-service-ecs-api</code> module.
The exposed REST API is simple and consists in the following endpoints to CRUD
<code class="language-plaintext highlighter-rouge">Customer</code> entities:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GET /customers</code>: returns a response containing the list of the currently existent customers;</li>
  <li><code class="language-plaintext highlighter-rouge">POST /customers</code>: creates a new customer by persisting the entity passed in the request’s body;</li>
  <li><code class="language-plaintext highlighter-rouge">PUT /customers/{id}</code>: updates the existent customer having the ID equal to the one passed as the <code class="language-plaintext highlighter-rouge">id</code> parameter. If such a customer doesn’t exist then HTTP 404 is returned.</li>
  <li><code class="language-plaintext highlighter-rouge">GET /customers/{id}</code>: returns a response containing the customer having the ID equal to the one passed as the <code class="language-plaintext highlighter-rouge">id</code> parameter. If such a customer doesn’t exist then HTTP 404 is returned.</li>
  <li><code class="language-plaintext highlighter-rouge">DELETE /customers/{id}</code>: deletes the customer having the ID equal to the one passed as the <code class="language-plaintext highlighter-rouge">id</code> parameter. If such a customer doesn’t exist then HTTP 404 is returned.</li>
</ul>

<p>The endpoints above are implemented in the class <code class="language-plaintext highlighter-rouge">CustomerResource</code> which is a
CDI (Context Dependency Injection) bean, annotated with <code class="language-plaintext highlighter-rouge">@ApplicationScoped</code>. This
is a very realistic example of using CDI in AWS deployed infrastructure elements.</p>

<h3 id="the-application-layer">The Application Layer</h3>

<p>This layer is the “brain” of the system, the place where the actual customer
management business logic resides, separated from how it is exposed by the
presentation layer. In our project it is included in the module <code class="language-plaintext highlighter-rouge">customer-service
-ecs-api</code> as well and it consists in:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">Customer</code> entity which is the domain model representing the business object;</li>
  <li>the <code class="language-plaintext highlighter-rouge">CustomerService</code> class containing the core business logic to CRUD operations;</li>
  <li>the caching strategies using Redis;</li>
  <li>the transaction management;</li>
  <li>the business rules and validation logic;</li>
</ul>

<p>We mentioned precedently that the <code class="language-plaintext highlighter-rouge">CustomerResource</code> class, as the pilar of the
presentation layer, is a CDI bean and, as such, it injects another CDI bean, the
<code class="language-plaintext highlighter-rouge">CustomerService</code> class, which performs the effective CRUD operations on <code class="language-plaintext highlighter-rouge">Customer</code>
business objects, using Quarkus Panache. The listing below shows the <code class="language-plaintext highlighter-rouge">Customer</code>
entity:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Entity
@Table(name = "customers")
public class Customer extends PanacheEntity
{
  @NotBlank
  public String firstName;
  @NotBlank
  public String lastName;
  @Email
  @NotBlank
  public String email;
  public String phone;
  public String address;

  public Customer(){}
  ...
}
</code></pre></div></div>

<p>As you can see, the validation rules are expressed using Jakarta Validation
constraints.</p>

<p>Given this very simplified representation of a customer, the <code class="language-plaintext highlighter-rouge">CustomerService</code>
class uses the <code class="language-plaintext highlighter-rouge">PanacheEntity</code> methods to CRUD customers, as shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@ApplicationScoped
public class CustomerService
{
  @Inject
  RedisDataSource redisDS;

  @Transactional
  public Customer create(Customer customer)
  {
    customer.persist();
    invalidateCache("customers:all");
    return customer;
  }

  public List&lt;Customer&gt; findAll()
  {
    return Customer.listAll();
  }

  public Customer findById(Long id)
  {
    ValueCommands&lt;String, Customer&gt; cache = redisDS.value(Customer.class);
    Customer cached = cache.get("customer:" + id);
    return Optional.ofNullable(cached).orElseGet(() -&gt; {
      Customer customer = Customer.findById(id);
      if (customer != null)
        cache.setex("customer:" + id, 300, customer);
      return customer;
    });
  }

  @Transactional
  public Customer update(Long id, Customer updates)
  {
    return Optional.ofNullable((Customer) Customer.findById(id))
      .map(customer -&gt;
      {
        customer.updateFrom(updates);
        invalidateCache("customer:" + id);
        invalidateCache("customers:all");
        return customer;
      })
      .orElse(null);
  }

  @Transactional
  public boolean delete(Long id)
  {
    boolean deleted = Customer.deleteById(id);
    if (deleted)
    {
      invalidateCache("customer:" + id);
      invalidateCache("customers:all");
    }
    return deleted;
  }

  private void invalidateCache(String key)
  {
    redisDS.key().del(key);
  }
}
</code></pre></div></div>

<p>Nothing very spectacular here, just an usual Quarkus Panache service to CRUD
customers. As you can see, the transaction management that we mentioned previously
are implemented on the behalf of the <code class="language-plaintext highlighter-rouge">@Transactional</code> annotation provided by
the Jakarta Transaction specification, implemented by Quarkus.</p>

<p>The application layer isn’t directly invoked but through the API endpoints, in the
<code class="language-plaintext highlighter-rouge">CustomerResource</code> class, for example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  ...
  @POST
  public Response create(@Valid Customer customer)
  {
    return Response.status(Response.Status.CREATED)
      .entity(customerService.create(customer)).build();
  }
  ...
</code></pre></div></div>

<p>The endpoint above is invoked through HTTP by a REST client and, in turn, it calls
<code class="language-plaintext highlighter-rouge">CustomerService</code>. And talking about REST clients, we also provide a MicroProfile
(MP) REST Client, which aims at facilitating the integration, by giving the API
consumers an easy and practical way to invoke it. Look at the interface
<code class="language-plaintext highlighter-rouge">CustomerClient</code> below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@RegisterRestClient(configKey = "customers-api")
@Path("/customers")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public interface CustomerClient
{
  @POST
  Response createCustomer(Customer customer);
  @GET
  @Path("/{id}")
  Response getCustomer(@PathParam("id") Long id);
  @GET
  public Response getCustomers();
  @PUT
  @Path("/{id}")
  Response updateCustomer(@PathParam("id") Long id, @Valid Customer customer);
  @DELETE
  @Path("/{id}")
  Response delete(@PathParam("id") Long id);
}
</code></pre></div></div>

<p>For those not yet familiar with the MP REST Client specification and its Quarkus
implementation, this interface is all you need in order to probe your API. I’ll
come back later to it when we’ll discuss testing.</p>

<p>Let’s look now at the infrastructure layer.</p>

<h3 id="the-infrastructure-layer">The Infrastructure Layer</h3>

<p>This layer makes the object of the 2nd project’s module: <code class="language-plaintext highlighter-rouge">customer-service-ecs-cdk</code>.
It consists of a Quarkus main class, named <code class="language-plaintext highlighter-rouge">CustomerManagementMain</code>, shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@QuarkusMain
public class CustomerManagementMain
{
  public static void main(String... args)
  {
    Quarkus.run(CustomerManagementApp.class, args);
  }
}
</code></pre></div></div>

<p>This class is the entry point class that bootstraps the Quarkus CDK application.
It uses <code class="language-plaintext highlighter-rouge">@QuarkusMain</code> to define the main method and delegates to Quarkus runtime
to run the <code class="language-plaintext highlighter-rouge">CustomerManagementApp</code> class, shown below;</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@ApplicationScoped
public class CustomerManagementApp implements QuarkusApplication
{
  private CustomerManagementStack customerManagementStack;
  private App app;

  @Inject
  public CustomerManagementApp (App app, CustomerManagementStack customerManagementStack)
  {
    this.app = app;
    this.customerManagementStack = customerManagementStack;
  }

  @Override
  public int run(String... args) throws Exception
  {
    Tags.of(app).add("project", "Containerized Customer Management Application on ECS/Fargate");
    Tags.of(app).add("environment", "development");
    Tags.of(app).add("application", "CustomerManagementApp");
    customerManagementStack.initStack();
    app.synth();
    return 0;
  }
}
</code></pre></div></div>

<p>This class is the main application class, as opposed to the Quarkus main class.
It orchestrates the CDK stack creation by:</p>

<p>The main application class implementing QuarkusApplication. It orchestrates the CDK stack creation by:</p>

<ul>
  <li>injecting the CDK App and CustomerManagementStack via CDI;</li>
  <li>adding global tags to the CDK app for project identification;</li>
  <li>initializing the stack infrastructure;</li>
  <li>synthesizing the CloudFormation templates;</li>
</ul>

<p>The class <code class="language-plaintext highlighter-rouge">CustomerManagementStack</code>, too long to be reproduced here, defines the
CDK stack to be deployed. This stack consists in the following AWS infrastructure:</p>

<ul>
  <li>a VPC (<em>Virtual Private Cloud</em>) with a public and a private subnet across multiple AZs (<em>Availability Zone</em>);</li>
  <li>a NAT (<em>Network Address Translation</em>) gateway to outbound the internet access for private resources;</li>
  <li>an RDS (<em>Relational Database Service</em>) with a PostgreSQL database with automated backups and secrets’ management;</li>
  <li>a Redis cluster using AWS ElastiCache for in-memory caching and performance optimization;</li>
  <li>an ECS (<em>Elastic Container Service</em>) Fargate serverless container hosting platform;</li>
  <li>an ALB (<em>Application Load Balancer</em>) for the traffic distribution and health checking;</li>
  <li>a Secrets Manager for the secure credential store and rotation;</li>
  <li>all the required security groups and network level access control;</li>
  <li>a CloudWatch log group for monitoring;</li>
  <li>the required IAM (<em>Identity and Access Management</em>) roles for the fine-grained permission management;</li>
</ul>

<p>The Java CDK provides the familiar pattern Builder which makes easy to instantiate
complex structures and class hierarchies. The code excerpt below provides an example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    ApplicationLoadBalancedFargateService fargateService =
      ApplicationLoadBalancedFargateService.Builder.create(this, "CustomerService")
        .cluster(cluster)
        .cpu(config.ecs().cpu())
        .memoryLimitMiB(config.ecs().memoryLimitMiB())
        .desiredCount(config.ecs().desiredCount())
        .taskImageOptions(ApplicationLoadBalancedTaskImageOptions.builder()
          .image(ContainerImage.fromRegistry(imageName))
          .containerPort(containerPort)
          .logDriver(LogDriver.awsLogs(AwsLogDriverProps.builder()
            .logGroup(logGroup)
            .streamPrefix(config.logging().streamPrefix())
            .build()))
          .environment(Map.of(
            "QUARKUS_DATASOURCE_JDBC_URL",
              "jdbc:postgresql://" + database.getInstanceEndpoint().getHostname() +
              ":5432/" + config.database().databaseName(),
            "QUARKUS_REDIS_HOSTS", "redis://" + redis.getPrimaryEndpoint() + ":6379"
          ))
          .secrets(Map.of(
            "QUARKUS_DATASOURCE_USERNAME",
              Secret.fromSecretsManager(database.getSecret(), "username"),
            "QUARKUS_DATASOURCE_PASSWORD",
              Secret.fromSecretsManager(database.getSecret(), "password")
          ))
          .build())
        .publicLoadBalancer(true)
        .healthCheckGracePeriod(Duration.seconds(config.ecs().healthCheckGracePeriodSeconds()))
        .serviceName(config.ecs().serviceName())
        .minHealthyPercent(100)
        .build();
</code></pre></div></div>

<p>This code sequence uses different builders in order to instantiate a full ECS
Fargate serverless hosting platform. Given the high number of parameters that
this process requires, the <code class="language-plaintext highlighter-rouge">InfrastructureConfig</code> interface, here below, provides
a type-safe Quarkus <code class="language-plaintext highlighter-rouge">@ConfigMap</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@ConfigMapping(prefix = "cdk.infrastructure")
public interface InfrastructureConfig
{
  VpcConfig vpc();
  EcsConfig ecs();
  DatabaseConfig database();
  RedisConfig redis();
  LoggingConfig logging();
  interface VpcConfig
  {
    @WithDefault("2")
    int maxAzs();
    @WithDefault("1")
    int natGateways();
  }
  interface EcsConfig
  {
    @WithDefault("256")
    int cpu();
    @WithDefault("512")
    int memoryLimitMiB();
    @WithDefault("2")
    int desiredCount();
    @WithDefault("60")
    int healthCheckGracePeriodSeconds();
    @WithDefault("customer-service")
    String serviceName();
  }
  interface DatabaseConfig
  {
    @WithDefault("BURSTABLE3")
    String instanceClass();
    @WithDefault("MICRO")
    String instanceSize();
    @WithDefault("customers")
    String databaseName();
    @WithDefault("postgres")
    String secretUsername();
    @WithDefault("false")
    boolean deletionProtection();
  }
  interface RedisConfig
  {
    @WithDefault("cache.t3.micro")
    String nodeType();
    @WithDefault("1")
    int numNodes();
    @WithDefault("customer-cache")
    String clusterId();
    @WithDefault("Redis cache for customer service")
    String description();
  }
  interface LoggingConfig
  {
    @WithDefault("/ecs/customer-service")
    String logGroupName();
    @WithDefault("ONE_WEEK")
    String retentionDays();
    @WithDefault("ecs")
    String streamPrefix();
  }
}
</code></pre></div></div>

<p>This <code class="language-plaintext highlighter-rouge">@ConfigMap</code> defines nested configuration structures for different
infrastructure components and <code class="language-plaintext highlighter-rouge">@WithDefault</code> annotations for default values and
provides compile-time configuration validation while organizing settings into
logical groups like VPC, ECS, database, Redis, and logging.</p>

<h2 id="cdk-configuration-and-deployment">CDK Configuration and Deployment</h2>

<p>AWS CDK uses the <code class="language-plaintext highlighter-rouge">cdk.json</code> file as its primary configuration mechanism to define
how the CDK application should be executed and deployed. This file serves as the
entry point that tells the CDK toolkit how to run the infrastructure application.</p>

<p>Here below is the file <code class="language-plaintext highlighter-rouge">cdk.json</code> used for this project:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "app": "java -jar target/quarkus-app/quarkus-run.jar",
  "context": {
    "aws-cdk:enableDiffNoFail": true,
    "@aws-cdk/aws-ecs:minHealthyPercent": true
  },
  "requireApproval": "never",
  "output": "cdk.out",
  "progress": "bar",
  "ci": true,
  "verbose": false,
  "acknowledgements": {
    "@aws-cdk/aws-ecs:ecrImageRequiresPolicy": true,
    "@aws-cdk/aws-ecs:minHealthyPercent": true,
    "34892": true
  },
  "notices": false
}
</code></pre></div></div>

<p>Looking at this file, several categories of key aspects are to be brought into
focus:</p>

<ul>
  <li>The <code class="language-plaintext highlighter-rouge">app</code> element: defines the command which executes the application. Our application being a Quarkus one, the <code class="language-plaintext highlighter-rouge">app</code> element reflects that by defining the standard way to run a Quarkus JVM application.</li>
  <li>The <code class="language-plaintext highlighter-rouge">context</code> element: stores environment specific settings. In our case:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">"aws-cdk:enableDiffNoFail": true</code> controls the behavior of the <code class="language-plaintext highlighter-rouge">cdk diff</code> command such that to continue the execution even if it encounters errors, for example missing permissions to describe resources, etc.</li>
      <li><code class="language-plaintext highlighter-rouge">"@aws-cdk/aws-ecs:minHealthyPercent": true</code> is an ECS specific flag that enables the <code class="language-plaintext highlighter-rouge">minHealthyPercent</code> property for ECS services. Here, it allows setting the minimum percentage of healthy tasks during deployments (e.g., 50% for rolling updates).</li>
    </ul>
  </li>
  <li>The feature flags: control CDK behavior and enables/disables specific features. In our case:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">"requireApproval": "never"</code> says that the CDK will never prompt for manual approval during <code class="language-plaintext highlighter-rouge">cdk deploy</code> operations.</li>
      <li><code class="language-plaintext highlighter-rouge">"output": "cdk.out"</code> sets the directory where the CloudFormation templates, generated by the <code class="language-plaintext highlighter-rouge">cdk synth</code> command, will be stored.</li>
      <li><code class="language-plaintext highlighter-rouge">"progress": "bar"</code> shows progress bar during CDK operations instead of detailed logs.</li>
      <li><code class="language-plaintext highlighter-rouge">"ci": true</code> optimizes output for CI/CD environments (less interactive, more structured).</li>
      <li><code class="language-plaintext highlighter-rouge">"verbose": false</code> suppresses detailed debug information during execution.</li>
      <li><code class="language-plaintext highlighter-rouge">"notices": false</code> disables CDK notices about new features or deprecations.</li>
    </ul>
  </li>
  <li>The acknowledgements:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">"@aws-cdk/aws-ecs:ecrImageRequiresPolicy": true</code> acknowledges that ECR images require IAM policies for access;</li>
      <li><code class="language-plaintext highlighter-rouge">"@aws-cdk/aws-ecs:minHealthyPercent": true</code> confirms understanding of ECS health check behavior</li>
      <li><code class="language-plaintext highlighter-rouge">"34892": true</code> acknowledges specific CDK issue/warning (likely related to a GitHub issue number)</li>
    </ul>
  </li>
</ul>

<p>This <code class="language-plaintext highlighter-rouge">cdk.json</code> file is used by the CDK toolkit to:</p>

<ul>
  <li>Synthesis: execute the app command to generate CloudFormation templates in <code class="language-plaintext highlighter-rouge">cdk.out</code>.</li>
  <li>Deployment: use the synthesized templates to deploy infrastructure to AWS.</li>
  <li>Context Management: cache AWS account/region specific information for consistent deployments.</li>
</ul>

<h2 id="running-and-testing">Running and testing</h2>

<p>There are several test categories that come with the project, as follows:</p>

<ul>
  <li>integration tests;</li>
  <li>system integration tests;</li>
  <li>Open API / Swagger tests;</li>
  <li>end-to-end tests.</li>
</ul>

<p>As you can see, we don’t provide unit tests because we think that this category
of tests is completely useless. But this is another topic which doesn’t belong
to the scope of this article.</p>

<h3 id="the-integration-tests">The Integration tests</h3>

<p>These tests aim at testing the complete REST API layer with the Quarkus runtime.
They use the test infrastructure automatically provide by the Quarkus Dev Services
with an in-memory H2 database, in order to validate the API contracts, the
requests/responses and the business logic integration.</p>

<p>The class <code class="language-plaintext highlighter-rouge">CustomerResourceTest</code> is one test in this category. It is executed
by the <code class="language-plaintext highlighter-rouge">maven-surefire-plugin</code> in the Maven test phase, hence its naming
convention: <code class="language-plaintext highlighter-rouge">*Test</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@QuarkusTest
public class CustomerResourceTest
{
  @Test
  void testCreateCustomer()
  {
    given()
      .contentType(ContentType.JSON)
      .body("""
        {
          "firstName": "John",
          "lastName": "Doe",
          "email": "john@example.com"
        }
      """)
      .when()
      .post("/customers")
      .then()
      .statusCode(201)
      .body("firstName", equalTo("John"));
  }
  ...
}
</code></pre></div></div>

<p>We reproduced here only one test method, the one creating new customers. Feel
free to extensively look at this class which uses the RESTassured library as
REST client.</p>

<p>Another integration test is the class <code class="language-plaintext highlighter-rouge">CloudFormationTemplateIT</code>. As opposed to
the previous one, this class is executed by the <code class="language-plaintext highlighter-rouge">maven-failsafe-plugin</code> in the
Maven <code class="language-plaintext highlighter-rouge">verify</code> phase. The reason is that it needs to be run after the <code class="language-plaintext highlighter-rouge">cdk synth</code>
command, executed by the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code>. This command synthesizes the required
AWS infrastructure in the form of a CloudFormation template, stored in the directory
<code class="language-plaintext highlighter-rouge">cdk.out</code>. Then the test class checks the files in this directory for the presence
and the validity of these AWS infrastructure elements.</p>

<p>In order to perform the integration test:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd aws-cdk-quarkus
$ mvn clean verify
</code></pre></div></div>

<p>The Maven command above will execute, in addition to the integration tests, the
system integration tests.</p>

<h3 id="the-system-integration-tests">The System Integration Tests</h3>

<p>These tests are a more realistic version of the integration ones. As opposed to
the former ones, which were relying on a Quarkus runtime related infrastructure,
these tests are more realistic in the sense that they are performed against a
local, while production similar, containerized infrastructure. During the Maven
<code class="language-plaintext highlighter-rouge">verify</code> phase, the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> executes the <code class="language-plaintext highlighter-rouge">docker-compose.yaml</code> file against
the currently running Docker daemon and starts all the required services, as follows:</p>

<ul>
  <li>a PostgreSQL database;</li>
  <li>the <code class="language-plaintext highlighter-rouge">adminer</code> tool to administrate the database;</li>
  <li>a Redis node for in-memory caching purposes;</li>
  <li>the <code class="language-plaintext highlighter-rouge">redis-insight</code> tool to administrate the local Redis instance;</li>
  <li>the customer management API as a Quarkus application.</li>
</ul>

<p>Once all this infrastructure is started, the <code class="language-plaintext highlighter-rouge">CustomerResourceIT</code> class uses the
<code class="language-plaintext highlighter-rouge">CustomerClient</code> to test the API running locally. The usage of the MP REST Client
isn’t mandatory, of course, other REST clients, like RESTassured or simply Jakarta
REST Client, can be used. However, the MP REST Client is, in my opinion, the
simplest and the most effective solution.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@QuarkusTest
@TestProfile(IntegrationTestProfile.class)
public class CustomerResourceIT
{
  @Inject
  @RestClient
  CustomerClient customerClient;

  @Test
  void testCreateCustomer()
  {
    Customer customer = new Customer("John", "Doe", "john@example.com",
      "000000000000", "123 Main St");
    Response response = customerClient.createCustomer(customer);
    assertThat(response.getStatus()).isEqualTo(201);
    customer = response.readEntity(Customer.class);
    assertThat(customer.firstName).isEqualTo("John");
    assertThat(customer.lastName).isEqualTo("Doe");
    assertThat(customer.email).isEqualTo("john@example.com");
  }
  ...
}
</code></pre></div></div>

<p>As you can see, the test is using a Quarkus customized profile, named
<code class="language-plaintext highlighter-rouge">IntegationTestProfile</code>, shown below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class IntegrationTestProfile implements QuarkusTestProfile
{
  @Override
  public Map&lt;String, String&gt; getConfigOverrides()
  {
    return Map.of(
      "quarkus.datasource.db-kind", "postgresql",
      "quarkus.datasource.username", "nicolas",
      "quarkus.datasource.password", "dev123",
      "quarkus.datasource.jdbc.url", "jdbc:postgresql://localhost:5432/customers",
      "quarkus.datasource.devservices.enabled", "false",
      "quarkus.redis.hosts", "redis://localhost:6379",
      "quarkus.redis.devservices.enabled", "false"
    );
  }
}
</code></pre></div></div>

<h3 id="the-swagger-tests">The Swagger tests</h3>

<p>The module <code class="language-plaintext highlighter-rouge">customer-service-ecs-api</code> exposes a Swagger interface that you can
use to manually test the API. Just fire your preferred browser at
http://localhost:8080/q/swagger-ui and you’ll be presented with that:</p>

<p><img src="/assets/images/swagger.png" alt="swagger" /></p>

<p>This will allow you to probe your API.</p>

<h3 id="the-e2e-tests">The E2E Tests</h3>

<p>The last test category is the end-to-end one. These tests have the particularity
to be performed against the real AWS services (ECS, RDS, ElasticCache, etc.).
The class CustomerServiceE2EIT is such a test. It’s similar to the integration
test <code class="language-plaintext highlighter-rouge">CustomerResourceTest</code> in the sense that it uses RESTassured to probe the
API but, instead of invoking local endpoints, it invokes endpoints onto the real
API deployed on the AWS Fargate platform and executed as a Docker image.</p>

<p>Everything happens in the code sequence below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  @BeforeAll
  static void setup()
  {
    cfClient = CloudFormationClient.builder()
     .region(Region.EU_WEST_3)
     .build();
    String loadBalancerUrl = getStackOutput("QuarkusCustomerManagementStack", "CustomerServiceLoadBalancerDNS");
    RestAssured.baseURI = "http://" + loadBalancerUrl;
    RestAssured.port = 80;
    System.out.println("&gt;&gt;&gt; Connecting to: " + RestAssured.baseURI + ":" + RestAssured.port);
    waitForServiceReady();
  }
</code></pre></div></div>

<p>This method orchestrates the connection to teh AWS environment. It creates a
CloudFormation client configured for the EU-WEST-3 region where the infrastructure
is deployed. Then it queries the deployed CloudFormation stack to retrieve the
ALB DNS name, eliminating hardcoded URLs and ensuring tests always connect to
the correct deployed instance. The RESTassured client is configured with the
dynamically discovered ALB URL and the standard HTTP port (80). The remaining
is very similar to what we did in <code class="language-plaintext highlighter-rouge">CustomerResourceTest</code> and <code class="language-plaintext highlighter-rouge">CustomerResourceIT</code>.
The method <code class="language-plaintext highlighter-rouge">waitForServiceReady()</code> is, however, new and ensure the the ECS service
is fully operational before running the tests, preventing this way false failures
due to deployment timing.</p>

<p>To run the E2E tests execute the following Maven command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd aws-cdk-quarkus
$ mvn -Pe2e clean verify
</code></pre></div></div>

<p>The workflow is as follows:</p>

<ol>
  <li>The module’s <code class="language-plaintext highlighter-rouge">pom.xml</code> file uses the <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> to run, in the Maven’s <code class="language-plaintext highlighter-rouge">pre-integration-test</code> phase, the command <code class="language-plaintext highlighter-rouge">cdk synth</code>. This command creates the associated CloudFormation template in the directory <code class="language-plaintext highlighter-rouge">cdk.out</code>.</li>
  <li>Then the same <code class="language-plaintext highlighter-rouge">exec-maven-plugin</code> runs, in the same Maven’s <code class="language-plaintext highlighter-rouge">pre-integration-test</code> phase, the script <code class="language-plaintext highlighter-rouge">deploy-ecr.sh</code> which automates the complete container deployment workflow.</li>
  <li>This script creates the ECR (<em>Elastic Container Repository</em>) repository if it doesn’t exist, using environment variables for dynamic naming and region configuration.</li>
  <li>Then it authenticates with ECR, retags the local Docker image with the ECR registry URL, and pushes it to the remote repository.</li>
  <li>If the stack already exists, due to a previous execution, then it is updated, otherwise the complete infrastructure is created from scratch.</li>
</ol>

<p>Here is the script:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
registry=$CDK_DEFAULT_ACCOUNT.dkr.ecr.$CDK_DEFAULT_REGION.amazonaws.com
echo "&gt;&gt;&gt; Creating ECR registry $registry"
aws ecr create-repository --repository-name $CONTAINER_IMAGE_GROUP/$CONTAINER_IMAGE_NAME  --region $CDK_DEFAULT_REGION 2&gt;/dev/null || echo "### Repository already exists"
echo "&gt;&gt;&gt; Logging into ECR..."
aws ecr get-login-password --region $CDK_DEFAULT_REGION | docker login --username AWS --password-stdin $registry
echo "&gt;&gt;&gt; Tagging and pushing existing image..."
docker tag $CONTAINER_IMAGE_GROUP/$CONTAINER_IMAGE_NAME:1.0-SNAPSHOT $registry/$CONTAINER_IMAGE_GROUP/$CONTAINER_IMAGE_NAME:latest
docker push $registry/$CONTAINER_IMAGE_GROUP/$CONTAINER_IMAGE_NAME:latest
echo "&gt;&gt;&gt; Checking if stack exists..."
if aws cloudformation describe-stacks --stack-name QuarkusCustomerManagementStack --region $CDK_DEFAULT_REGION &gt;/dev/null 2&gt;&amp;1; then
  echo "&gt;&gt;&gt; Stack exists - updating ECS service ..."
  CLUSTER_NAME=$(aws cloudformation describe-stack-resources --stack-name QuarkusCustomerManagementStack \
    --query 'StackResources[?ResourceType==`AWS::ECS::Cluster`].PhysicalResourceId' \
    --output text --region $CDK_DEFAULT_REGION)
  if [ -n "$CLUSTER_NAME" ]; then
    echo "&gt;&gt;&gt; Found cluster: $CLUSTER_NAME - updating ECS service..."
    aws ecs update-service \
      --cluster $CLUSTER_NAME \
      --service customer-service \
      --force-new-deployment \
      --region $CDK_DEFAULT_REGION
    echo "&gt;&gt;&gt; Waiting for service update to complete..."
    aws ecs wait services-stable \
      --cluster $CLUSTER_NAME \
      --services customer-service \
      --region $CDK_DEFAULT_REGION
    echo "&gt;&gt;&gt; Service update complete!"
    exit 0
  fi
fi
echo "&gt;&gt;&gt; Deploying full infrastructure..."
cdk deploy --all --require-approval never
echo "&gt;&gt;&gt; Deployment finished !"
</code></pre></div></div>

<p>Beware that the deployment operation is a long-running process which can take
more than 15 minutes. Also, once deployed and running, you’ll be invoiced for
the cost of the associated infrastructure.</p>

<p>During the script execution, you can check the progression by running scripts
like <code class="language-plaintext highlighter-rouge">describe-services.sh</code>, <code class="language-plaintext highlighter-rouge">describe-events.sh</code>, <code class="language-plaintext highlighter-rouge">describe-stacks.sh</code>, etc.,
or simply using in the AWS Console to look for possible error messages in the
CloudWatch log groups. In order to run scripts you need to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cd aws-cdk-quarkus/customer-service-ecs
$ ./customer-service-ecs-cdk/src/main/resources/scripts/&lt;script-name&gt;
</code></pre></div></div>

<p>Once the deployed successful and after having exercised your infrastructure,
don’t forget to run the <code class="language-plaintext highlighter-rouge">delete-stack.sh</code> script, which will remove everything
you deployed, avoiding you this way to be invoiced by AWS.</p>

<p>Enjoy !</p>]]></content><author><name>Nicolas DUMINIL</name></author><category term="Java" /><category term="Quarkus" /><category term="AWS" /><category term="ECS" /><category term="Fargate" /><category term="DZone" /><summary type="html"><![CDATA[In a three articles series published recently on this site (Part 1, Part 2, Part 3), I’ve been demonstrating the power of the AWS Cloud Development Kit (CDK) in the Infrastructure as Code (IaC) area, especially when coupled with the ubiquitous Java and its supersonic / subatomic cloud-native stack: Quarkus.]]></summary></entry></feed>