<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Juan Carlos González Cabrero</title>
    <description>The latest articles on Forem by Juan Carlos González Cabrero (@malkomich).</description>
    <link>https://forem.com/malkomich</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F633777%2F5bc75064-09ed-4ed5-ab35-49232da2802c.jpeg</url>
      <title>Forem: Juan Carlos González Cabrero</title>
      <link>https://forem.com/malkomich</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/malkomich"/>
    <language>en</language>
    <item>
      <title>Decoupling the Core: An Introduction to Hexagonal Architecture</title>
      <dc:creator>Juan Carlos González Cabrero</dc:creator>
      <pubDate>Fri, 20 Mar 2026 12:58:56 +0000</pubDate>
      <link>https://forem.com/malkomich/decoupling-the-core-an-introduction-to-hexagonal-architecture-222l</link>
      <guid>https://forem.com/malkomich/decoupling-the-core-an-introduction-to-hexagonal-architecture-222l</guid>
      <description>&lt;h2&gt;
  
  
  When Layered Architecture Stops Scaling
&lt;/h2&gt;

&lt;p&gt;Layered architecture usually looks fine in the first month. The trouble starts when the service stops being a CRUD API and begins coordinating external providers, scheduled jobs, cache layers, persistence rules, and business constraints that are no longer trivial. At that point, the classic controller-service-repository stack tends to accumulate responsibilities in the wrong places.&lt;/p&gt;

&lt;p&gt;That is where Hexagonal Architecture starts to make sense. Not because the diagram is elegant, but because it gives the design a rule that is easy to reason about: business logic stays in the center, infrastructure stays at the edges, and dependencies point inward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why classic layering breaks down
&lt;/h2&gt;

&lt;p&gt;Traditional layered systems are not wrong. They are just easier to get wrong as the application grows.&lt;/p&gt;

&lt;p&gt;The usual failure mode is predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controllers start orchestrating use cases&lt;/li&gt;
&lt;li&gt;services become transaction scripts with framework knowledge&lt;/li&gt;
&lt;li&gt;repositories stop being persistence abstractions and begin to carry business decisions&lt;/li&gt;
&lt;li&gt;domain objects degrade into DTOs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real problem is not the number of layers. The real problem is dependency direction. Once domain logic depends on JPA entities, Spring annotations, HTTP DTOs, or provider payloads, changing any outer concern becomes more expensive than it should be.&lt;/p&gt;

&lt;p&gt;That cost shows up in very practical ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tests become slow because business rules need full application context&lt;/li&gt;
&lt;li&gt;switching storage technology becomes invasive&lt;/li&gt;
&lt;li&gt;adding a second delivery mechanism, such as messaging or scheduling, duplicates orchestration logic&lt;/li&gt;
&lt;li&gt;external integration concerns leak into the core model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hexagonal Architecture is a response to that coupling problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The central idea of Hexagonal Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmatjjnbumqj862gce2d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmatjjnbumqj862gce2d.png" alt="A hexagonal diagram showing the application core in the center with labeled ports (inbound and outbound) around the edges, connected to external adapters (REST Controller, Database, Message Queue, External API, etc.) on the outside. Should clearly show dependency arrows pointing inward toward the core." width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is simpler than the terminology sometimes suggests.&lt;/p&gt;

&lt;p&gt;A hexagonal application has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a core with domain and use cases&lt;/li&gt;
&lt;li&gt;inbound ports, which define what the application can do&lt;/li&gt;
&lt;li&gt;outbound ports, which define what the application needs&lt;/li&gt;
&lt;li&gt;adapters, which connect those ports to HTTP, databases, queues, schedulers, external APIs, and any other technical detail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not to create more interfaces for the sake of it. The point is to isolate the places where change is likely and costly.&lt;/p&gt;

&lt;p&gt;Here is the idea in one sentence: &lt;strong&gt;the application core should not know how data arrives, how it is stored, or which library is doing the work outside the boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inbound and outbound ports
&lt;/h3&gt;

&lt;p&gt;Inbound ports model use cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SearchSlotsUseCase&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;searchPublishedSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OffsetDateTime&lt;/span&gt; &lt;span class="n"&gt;opensAfter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OffsetDateTime&lt;/span&gt; &lt;span class="n"&gt;closesBefore&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;RefreshSlotCatalogUseCase&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refreshCatalog&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outbound ports model dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SlotQueryRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findPublishedSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SlotSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SlotCommandRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mergeSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PartnerCatalogClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchSlots&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those interfaces are already saying something important. The application depends on business capabilities, not on &lt;code&gt;JpaRepository&lt;/code&gt;, &lt;code&gt;RestTemplate&lt;/code&gt;, Redis commands, or XML parsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hexagonal Architecture and DDD fit naturally together
&lt;/h2&gt;

&lt;p&gt;Hexagonal Architecture and Domain-Driven Design solve different problems, but they work well together.&lt;/p&gt;

&lt;p&gt;Hexagonal Architecture answers: &lt;strong&gt;where should dependencies point?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DDD answers: &lt;strong&gt;how should the domain be modeled?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice, the combination is useful because a clean architecture without a meaningful domain model quickly becomes ceremony, and a rich domain model without clear boundaries tends to get contaminated by infrastructure concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What DDD looks like in a service like this
&lt;/h3&gt;

&lt;p&gt;You do not need the full DDD catalogue to get value. A few tactical choices are enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name domain concepts in business language&lt;/li&gt;
&lt;li&gt;model small value objects when identity or meaning matters&lt;/li&gt;
&lt;li&gt;keep invariants close to the model&lt;/li&gt;
&lt;li&gt;represent domain errors explicitly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if a slot coming from a partner catalog is identified by two related ids, that pair deserves its own type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;PartnerSlotId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;seasonCode&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;slotNumber&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PartnerSlotId&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seasonCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;seasonCode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;slotNumber&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DomainValidationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Partner slot id is required"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a better model than passing two unrelated &lt;code&gt;Long&lt;/code&gt; values through every layer and hoping nobody swaps their order.&lt;/p&gt;

&lt;p&gt;The same applies to the aggregate root or main entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;UUID&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;PartnerSlotId&lt;/span&gt; &lt;span class="n"&gt;partnerSlotId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;opensAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;closesAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;bookingStartsAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;bookingEndsAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;SalesChannel&lt;/span&gt; &lt;span class="n"&gt;salesChannel&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;soldOut&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SeatBand&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;seatBands&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headline&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DomainValidationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"headline is required"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opensAt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;closesAt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DomainValidationException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slot dates are required"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opensAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closesAt&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidDateRangeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"opensAt cannot be after closesAt"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookingStartsAt&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bookingEndsAt&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bookingStartsAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookingEndsAt&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidDateRangeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bookingStartsAt cannot be after bookingEndsAt"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;seatBands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seatBands&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seatBands&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where DDD pulls its weight. The model is not just data storage. It protects meaning and rejects invalid states.&lt;/p&gt;

&lt;h3&gt;
  
  
  A pragmatic warning about DDD
&lt;/h3&gt;

&lt;p&gt;This is also where many teams overdo it.&lt;/p&gt;

&lt;p&gt;Not every object needs to become a rich domain object. Not every service needs domain events, factories, specifications, and anti-corruption layers from day one. If the domain is modest, forcing every DDD pattern into the design usually makes the code harder to follow.&lt;/p&gt;

&lt;p&gt;The useful test is simple: does this abstraction make the business model clearer and safer, or is it only there because the pattern exists?&lt;/p&gt;

&lt;h2&gt;
  
  
  A Spring Boot structure that stays readable
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzx9so2yjmct32voma8zj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzx9so2yjmct32voma8zj.png" alt="A detailed component interaction diagram showing: Payment (domain entity) in the center, ProcessPaymentUseCase interface above it, SavePaymentPort interface below it, PaymentService (implementing ProcessPaymentUseCase) in the application layer, PaymentController (inbound adapter) connecting via use case, and PaymentPersistenceAdapter (outbound adapter) implementing the SavePaymentPort with PaymentJpaEntity. Use color coding and arrows to show interface implementations and dependencies." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical structure for a hexagonal Spring Boot service usually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/main/java/com/example/catalog
├── domain
│   ├── exception
│   ├── model
│   └── port
├── application
│   ├── port/in
│   └── service
└── infrastructure
    ├── config
    ├── inbound
    └── outbound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package names matter less than the rule behind them. Domain and application should not depend on infrastructure. Infrastructure is allowed to depend on the core.&lt;/p&gt;

&lt;p&gt;I also think it is worth being slightly strict here: if your domain package imports JPA annotations or Spring MVC types, the architecture is already drifting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The application layer should orchestrate, not absorb infrastructure
&lt;/h2&gt;

&lt;p&gt;The read side can stay very small:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SlotQueryRepository&lt;/span&gt; &lt;span class="n"&gt;slotQueryRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;searchPublishedSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OffsetDateTime&lt;/span&gt; &lt;span class="n"&gt;opensAfter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OffsetDateTime&lt;/span&gt; &lt;span class="n"&gt;closesBefore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SlotSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SlotSearchCriteria&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;opensAfter&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opensAfter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLocalDateTime&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;closesBefore&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;closesBefore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLocalDateTime&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;slotQueryRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findPublishedSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the right level of responsibility for an application service. It coordinates the use case, translates raw input into a domain concept, and delegates the actual query to an outbound port.&lt;/p&gt;

&lt;p&gt;The write side usually shows the architecture more clearly:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PartnerCatalogClient&lt;/span&gt; &lt;span class="n"&gt;partnerCatalogClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SlotCommandRepository&lt;/span&gt; &lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refreshCatalog&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partnerCatalogClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchSlots&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mergeSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three details here that are easy to miss and worth keeping:&lt;/p&gt;

&lt;p&gt;First, &lt;code&gt;Clock&lt;/code&gt; is injected directly. I prefer that over inventing a &lt;code&gt;ClockPort&lt;/code&gt; unless time itself is a business capability. Not every technical dependency deserves its own architectural boundary.&lt;/p&gt;

&lt;p&gt;Second, the write port exposes business operations, not generic CRUD verbs. &lt;code&gt;mergeSnapshot&lt;/code&gt; and &lt;code&gt;disableMissingSlots&lt;/code&gt; describe a synchronization process much better than &lt;code&gt;save()&lt;/code&gt; ever could.&lt;/p&gt;

&lt;p&gt;Third, the service has no idea whether the provider uses XML, JSON, HTTP, gRPC, or a message queue. That uncertainty stays outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapters should translate, not think
&lt;/h2&gt;

&lt;p&gt;This is one of the most useful practical rules in a hexagonal system.&lt;/p&gt;

&lt;p&gt;Adapters are translators. They convert transport models into domain objects and domain objects into transport or persistence models. They should be as boring as possible.&lt;/p&gt;

&lt;p&gt;A persistence adapter can map between the domain model and JPA entities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JpaSlotCommandRepositoryAdapter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;SlotCommandRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SpringDataSlotRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SlotEntityMapper&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mergeSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SlotEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A provider adapter can isolate HTTP and parsing concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="nd"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PartnerCatalogClientAdapter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PartnerCatalogClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PartnerFeedGateway&lt;/span&gt; &lt;span class="n"&gt;partnerFeedGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PartnerSlotFeedParser&lt;/span&gt; &lt;span class="n"&gt;partnerSlotFeedParser&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchSlots&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;partnerFeedGateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchPayload&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;partnerSlotFeedParser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This split matters because retry policy, HTTP timeouts, payload validation, caching, and fetch strategy are not domain concerns. They change for different reasons and should live in different adapters or configuration classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  CQRS often appears naturally here
&lt;/h2&gt;

&lt;p&gt;A lot of articles treat CQRS like a dramatic architectural leap. In many Java backends, it appears in a much smaller and more useful form.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/img/uploads/hexagonal-read-write-flow.svg" class="article-body-image-wrapper"&gt;&lt;img src="/assets/img/uploads/hexagonal-read-write-flow.svg" alt="Read and write flows in a hexagonal backend"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If reads and writes already have different behavior, different performance requirements, and different models, it is often enough to separate query and command ports.&lt;/p&gt;

&lt;p&gt;That is already a CQRS-style decision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SlotQueryRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findPublishedSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SlotSearchCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SlotCommandRepository&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mergeSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerformanceSlot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not full CQRS. There is no separate read database, no asynchronous projection system, and no event-driven choreography. But the separation is still valuable because it acknowledges that reads and writes have different semantics.&lt;/p&gt;

&lt;p&gt;In practice, this buys you a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the read side can optimize for filtering, sorting, pagination, and caching&lt;/li&gt;
&lt;li&gt;the write side can optimize for consistency, idempotency, and synchronization logic&lt;/li&gt;
&lt;li&gt;tests become more focused because the contracts are narrower&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of those places where I think pragmatism matters more than purity. You do not need the full CQRS stack to benefit from the idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  SOLID and related patterns still matter
&lt;/h2&gt;

&lt;p&gt;Hexagonal Architecture does not replace SOLID. It gives some of those principles a clearer architectural shape.&lt;/p&gt;

&lt;p&gt;The most visible one is Dependency Inversion. Application services depend on abstractions such as &lt;code&gt;PartnerCatalogClient&lt;/code&gt; or &lt;code&gt;SlotQueryRepository&lt;/code&gt;, not on concrete adapters.&lt;/p&gt;

&lt;p&gt;Single Responsibility also becomes easier to enforce when boundaries are explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controllers deal with HTTP&lt;/li&gt;
&lt;li&gt;schedulers trigger use cases&lt;/li&gt;
&lt;li&gt;adapters handle translation&lt;/li&gt;
&lt;li&gt;application services orchestrate&lt;/li&gt;
&lt;li&gt;domain objects protect business rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clean Architecture and Onion Architecture belong to the same family of ideas. The naming differs, but the core intention is similar: keep business rules in the center and push technology outward.&lt;/p&gt;

&lt;p&gt;The useful takeaway is not which label you choose. The useful takeaway is whether your code actually follows the dependency rule you claim to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing is where Hexagonal Architecture usually proves itself
&lt;/h2&gt;

&lt;p&gt;The strongest argument for the pattern is rarely the diagram. It is the test suite.&lt;/p&gt;

&lt;p&gt;A service designed around ports is much easier to test in layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;domain tests for invariants&lt;/li&gt;
&lt;li&gt;application service tests with mocked ports&lt;/li&gt;
&lt;li&gt;adapter integration tests for persistence or external clients&lt;/li&gt;
&lt;li&gt;controller tests for request and response mapping&lt;/li&gt;
&lt;li&gt;architecture tests to enforce dependency rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A unit test for the synchronization flow can stay fast and precise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ExtendWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockitoExtension&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RefreshSlotCatalogServiceTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Mock&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;PartnerCatalogClient&lt;/span&gt; &lt;span class="n"&gt;partnerCatalogClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Mock&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;SlotCommandRepository&lt;/span&gt; &lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Mock&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Clock&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@InjectMocks&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;RefreshSlotCatalogService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refreshUsesTheSameTimestampForMergeAndDisable&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-01-01T00:00:00Z"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instant&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partnerCatalogClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchSlots&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;thenReturn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refreshCatalog&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;mergeSnapshot&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slotCommandRepository&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;disableMissingSlots&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importedAt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That test is quick, deterministic, and tied to business behavior instead of framework wiring.&lt;/p&gt;

&lt;p&gt;I also strongly recommend architecture tests once the project has a few contributors. If the team says the domain must not depend on infrastructure, that rule should be executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ArchTest&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ArchRule&lt;/span&gt; &lt;span class="n"&gt;domainMustNotDependOnInfrastructure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;noClasses&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;resideInAPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..domain.."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;should&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;dependOnClassesThat&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resideInAnyPackage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"..application.."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"..infrastructure.."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without those checks, many projects keep the folder names and lose the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes that are worth avoiding early
&lt;/h2&gt;

&lt;p&gt;There are a few mistakes I keep seeing in Java teams that adopt hexagonal structure too literally.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/img/uploads/architect.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/img/uploads/architect.png" alt="Matrix architect talking about mistakes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ports everywhere
&lt;/h3&gt;

&lt;p&gt;If a dependency is not a real boundary, do not force it into a port. A &lt;code&gt;LoggerPort&lt;/code&gt; is almost always a smell. A &lt;code&gt;ClockPort&lt;/code&gt; often is too. Architectural boundaries should represent things that may vary independently or that materially affect testability and coupling.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Anemic domain with fancy packaging
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;domain&lt;/code&gt; package full of records or Lombok DTOs with no invariants is not a domain model. It is just data moved to a different folder.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Business logic hidden in adapters
&lt;/h3&gt;

&lt;p&gt;As soon as adapters start deciding what is valid, what should be retried, or how core workflows should behave, the architecture is being bypassed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pretending folder names equal architecture
&lt;/h3&gt;

&lt;p&gt;Folders help, but they do not enforce anything by themselves. Dependency rules, code review discipline, and a small amount of architectural testing do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Hexagonal Architecture is not a silver bullet, and it is not the right answer for every application. For a small CRUD service with minimal business logic and few external dependencies, it might add more complexity than value. But for growing backends, especially in fast-moving industries like fintech, healthtech, or any domain with complex rules and frequent pivots, it is often the most solid long-term investment.&lt;/p&gt;

&lt;p&gt;I've worked on systems where we've replaced entire infrastructure stacks, migrating from monoliths to microservices, switching message queues, or integrating new database systems, with minimal changes to core logic. That stability in the center while everything around it evolves is what makes the pattern powerful.&lt;/p&gt;

&lt;p&gt;My advice: Start small and don't over-engineer. Apply the pattern to domains where volatility and growth risk are highest. If you're building a payment processing engine or a complex workflow system, hexagonal architecture makes sense from day one. If you're building a simple backend for a mobile app with CRUD operations, maybe start traditionally and refactor toward hexagonal boundaries as complexity grows. You can introduce ports and adapters incrementally, they're just interfaces and implementations, after all.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cleancode</category>
      <category>java</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Self-Hosting n8n on Raspberry Pi: HTTPS, DDNS &amp; Automated Backups</title>
      <dc:creator>Juan Carlos González Cabrero</dc:creator>
      <pubDate>Thu, 19 Feb 2026 13:38:34 +0000</pubDate>
      <link>https://forem.com/malkomich/self-hosting-n8n-on-raspberry-pi-https-ddns-automated-backups-i2</link>
      <guid>https://forem.com/malkomich/self-hosting-n8n-on-raspberry-pi-https-ddns-automated-backups-i2</guid>
      <description>&lt;h2&gt;
  
  
  1. Why I Choose to Self-Host n8n on a Raspberry Pi
&lt;/h2&gt;

&lt;p&gt;When I first started working with automation tools, n8n's cloud offering seemed like the best choice. But after a few months, I found myself increasingly concerned about where the data lives, especially when handling customer information and internal business logic. The costs were also scaling faster than I'd anticipated, and I kept running into those limitations that come with any SaaS platform: can't &lt;strong&gt;customize the infrastructure&lt;/strong&gt;, can't control update timing, can't peek under the hood when something goes sideways.&lt;/p&gt;

&lt;p&gt;As a backend engineer with curiosity and passion for challenges, the idea of self-hosting n8n on a Raspberry Pi started making more sense. I wanted complete control over my automation stack without the recurring charges or vendor dependencies. More importantly, I wanted to understand every layer of the system, from the basic setup to the reverse proxy to the backup strategy. This wasn't just about getting n8n running locally on a publicly exposed port and calling it done. I needed a setup that could handle production workloads, stay accessible 24/7 from anywhere, and recover gracefully from failures.&lt;/p&gt;

&lt;p&gt;What I ended up building is what I'm sharing with you here: a &lt;strong&gt;fully Dockerized n8n stack with PostgreSQL, secure HTTPS endpoints, dynamic DNS to handle my home ISP's changing IP addresses, and automated cloud backups&lt;/strong&gt;. It's been running reliably for some weeks now, handling everything from complex workflows to internal notification systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stack Architecture and Core Configuration
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8u512wwo0fl63ndlhbv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8u512wwo0fl63ndlhbv.webp" alt="Container architecture diagram showing the main components (n8n, PostgreSQL, Nginx Proxy Manager) with arrows indicating data flow, dependencies, and volume mounts. Should visualize how requests flow from the customer through Nginx to n8n, and how n8n connects to PostgreSQL." width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This setup uses Docker Compose to isolate each component, n8n for workflow automation, PostgreSQL as the database, pgAdmin for the PostgreSQL management, and Nginx Proxy Manager for TLS, reverse proxy, and Let's Encrypt certificate management. Finally, the persistent volumes for each service are stored under &lt;code&gt;./data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All sensitive values are stored in a single &lt;code&gt;.env&lt;/code&gt; file in the project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POSTGRES_USER=n8nprod
POSTGRES_PASSWORD=supersecretpassword
POSTGRES_DB=n8n
PGADMIN_EMAIL=admin@example.com
PGADMIN_PASSWORD=change_me
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=automation
N8N_BASIC_AUTH_PASSWORD=change_me_later
N8N_ENCRYPTION_KEY=&amp;lt;32 char random string&amp;gt;
PUBLIC_DOMAIN=&amp;lt;YOUR_DOMAIN&amp;gt;
N8N_PROTOCOL=https
N8N_PORT=5678
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important: in &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;PUBLIC_DOMAIN=&amp;lt;YOUR_DOMAIN&amp;gt;&lt;/code&gt; must be the public domain you want to publish your n8n server, for example &lt;code&gt;n8n.example.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first thing I do after creating this &lt;code&gt;.env&lt;/code&gt; file is lock down its permissions. Only the user running Docker should be able to read it. This prevents other users on the system (or compromised services) from accessing the secrets. You could use Docker secrets, or even HashiCorp Vault, but for a small setup, strict file permissions and regular password rotation are already a strong baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;Docker Compose&lt;/strong&gt; file where everything comes together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres_db&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/postgres:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_network&lt;/span&gt;

  &lt;span class="na"&gt;pgadmin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dpage/pgadmin4&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pgadmin&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_DEFAULT_EMAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PGADMIN_EMAIL}&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_DEFAULT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PGADMIN_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_CONFIG_SERVER_MODE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;True'&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;False'&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/pgadmin:/var/lib/pgadmin&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_network&lt;/span&gt;

  &lt;span class="na"&gt;n8n&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8nio/n8n:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_TYPE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresdb&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${N8N_ENCRYPTION_KEY}&lt;/span&gt;
      &lt;span class="na"&gt;N8N_SECURE_COOKIE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;N8N_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5678&lt;/span&gt;
      &lt;span class="na"&gt;N8N_PROTOCOL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
      &lt;span class="na"&gt;N8N_EDITOR_BASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://${PUBLIC_DOMAIN}/&lt;/span&gt;
      &lt;span class="na"&gt;WEBHOOK_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://${PUBLIC_DOMAIN}/&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/n8n:/home/node/.n8n&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_network&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jc21/nginx-proxy-manager:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx_proxy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;81:81"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/npm/data:/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data/npm/letsencrypt:/etc/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;n8n&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgadmin&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_network&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The volume mounts deserve special attention. For PostgreSQL, I'm mounting &lt;code&gt;./data/postgres&lt;/code&gt; to persist the database across container restarts. For n8n, the &lt;code&gt;./data/n8n&lt;/code&gt; mount stores workflow data and encrypted credentials. Nginx Proxy Manager stores config and certificates in &lt;code&gt;./data/npm&lt;/code&gt;, and pgAdmin persists its state in &lt;code&gt;./data/pgadmin&lt;/code&gt;. This path structure keeps backups simple because every critical state lives under &lt;code&gt;./data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I use &lt;code&gt;restart: unless-stopped&lt;/code&gt; for all services rather than &lt;code&gt;restart: always&lt;/code&gt;. The distinction matters: if I manually stop a container for maintenance, I don't want it automatically restarting. But if the Pi reboots or Docker daemon restarts, everything should come back up without manual intervention.&lt;/p&gt;

&lt;p&gt;After the first &lt;code&gt;docker compose up -d&lt;/code&gt;, run a quick service sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 n8n
docker compose logs &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50 nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;n8n&lt;/code&gt; is restarting in loop, the first thing to verify is &lt;code&gt;.env&lt;/code&gt; consistency, especially &lt;code&gt;POSTGRES_*&lt;/code&gt;, &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;, and &lt;code&gt;PUBLIC_DOMAIN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Exposing n8n Securely with Nginx Proxy Manager
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwnxfqxit777y695ym9h.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwnxfqxit777y695ym9h.webp" alt="Network flow diagram showing: external traffic on port 80/443 → Nginx Proxy Manager (TLS termination) → internal n8n service on port 5678 over plain HTTP. Should illustrate the reverse proxy pattern and where SSL/TLS encryption happens." width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running n8n directly on port 5678 with HTTP is fine for localhost testing, but completely unacceptable for production use. You need HTTPS for security, you need proper hostname routing, and you need automatic certificate renewal. I used to configure all of this manually with raw nginx configs and certbot, but Nginx Proxy Manager (NPM) has simplified my life considerably.&lt;/p&gt;

&lt;p&gt;NPM provides a clean web UI, usually accessible on port 81, where you can configure proxy hosts, SSL certificates, and access controls without touching nginx config files.&lt;/p&gt;

&lt;p&gt;The SSL configuration is remarkably straightforward. In Nginx Proxy Manager, create a proxy host for &lt;code&gt;&amp;lt;YOUR_DOMAIN&amp;gt;&lt;/code&gt;, forward to &lt;code&gt;n8n:5678&lt;/code&gt;, enable &lt;code&gt;Force SSL&lt;/code&gt;, and request a Let's Encrypt certificate. NPM handles the ACME challenge automatically, as long as ports 80 and 443 are properly forwarded from your router. The certificates renew automatically every 60 days, and I've never had to intervene manually. This is the kind of automation that makes self-hosting sustainable.&lt;/p&gt;

&lt;p&gt;What I particularly appreciate about this architecture is that all &lt;strong&gt;TLS termination happens at the proxy layer&lt;/strong&gt;. n8n itself runs plain HTTP internally, which simplifies its configuration and reduces the attack surface. Only NPM needs access to private keys, and only NPM faces the public internet directly. This separation of concerns makes security auditing much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Domain config with Dynamic DNS (DDNS) and Router Setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz4xtlwcngmsq48w1blb.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzz4xtlwcngmsq48w1blb.jpeg" alt="Timeline/sequence diagram showing: ISP changes home IP → DDNS script detects change → DNS record change → Cloudflare propagates → external services resolve to new IP. Should include the 15-minute check interval and TTL expiration windows." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Domain and DDNS flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register domain at your registrar.&lt;/li&gt;
&lt;li&gt;Delegate nameservers to Cloudflare.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create DNS record for your n8n host if needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In my case I created an &lt;code&gt;A&lt;/code&gt; record for a subdomain I reserved for my n8n instance, and that record is the one my DDNS process updates automatically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep DNS updated with &lt;a href="https://github.com/qdm12/ddns-updater" rel="noopener noreferrer"&gt;&lt;code&gt;ddns-updater&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Install DDNS updater:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/qdm12/ddns-updater/cmd/ddns-updater@latest
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/ddns/data
&lt;span class="nb"&gt;mv&lt;/span&gt; ~/go/bin/ddns-updater ~/ddns/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the configuration file &lt;code&gt;config.json&lt;/code&gt; you need to create under &lt;code&gt;~/ddns/data&lt;/code&gt; directory, following the &lt;a href="https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md" rel="noopener noreferrer"&gt;tool documentation&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudflare"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;YOUR_DOMAIN&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;CLOUDFLARE_API_TOKEN&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"zone_identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;CLOUDFLARE_ZONE_ID&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep the updater alive 24/7, I created a systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/systemd/system/ddns-updater.service
&lt;/span&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;DDNS Updater&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your_user&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/&amp;lt;your_user&amp;gt;/ddns&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/home/&amp;lt;your_user&amp;gt;/ddns/ddns-updater -datadir ./data&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; ddns-updater.service
systemctl status ddns-updater.service
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; ddns-updater.service &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your router needs two settings, a fixed local IP for the Raspberry Pi, and port forwarding for 80 and 443 to that same IP.&lt;/p&gt;

&lt;p&gt;First, get the network interface MAC address from the Raspberry Pi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ifconfig &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your router DHCP section, bind that MAC to a fixed LAN IP you choose. It can be any valid free IP in your subnet, for example &lt;code&gt;192.168.1.100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, create port forwarding rules pointing to that same fixed IP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TCP &lt;code&gt;80&lt;/code&gt; -&amp;gt; &lt;code&gt;&amp;lt;fixed_lan_ip&amp;gt;:80&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;TCP &lt;code&gt;443&lt;/code&gt; -&amp;gt; &lt;code&gt;&amp;lt;fixed_lan_ip&amp;gt;:443&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the forwarded IP and the DHCP reservation IP do not match, HTTPS issuance and public access will fail.&lt;/p&gt;

&lt;p&gt;Once DNS and forwarding are configured, validate the public path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig +short &amp;lt;YOUR_DOMAIN&amp;gt; @1.1.1.1
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://&amp;lt;YOUR_DOMAIN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result, &lt;code&gt;dig&lt;/code&gt; returns your public IP (or Cloudflare edge IPs if proxied), and &lt;code&gt;curl&lt;/code&gt; returns an HTTP response over TLS.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Automated Backups with rclone
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bubc8sud42lshg46gmr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bubc8sud42lshg46gmr.jpg" alt="rclone automation flow, pushing backups to Google Drive." width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Backups include both PostgreSQL data and n8n runtime data (&lt;code&gt;data/n8n&lt;/code&gt;). &lt;strong&gt;rclone&lt;/strong&gt; is reliable for unattended uploads, supports retries, and keeps the process lightweight on Raspberry Pi.&lt;/p&gt;

&lt;p&gt;My backup script runs nightly via cron. It loads &lt;code&gt;.env&lt;/code&gt;, dumps the database from the running PostgreSQL container, archives n8n data, uploads both files to a timestamped Google Drive folder, and enforces local and remote retention policies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;ENV_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/.env"&lt;/span&gt;
&lt;span class="nv"&gt;BACKUP_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.n8n/backups"&lt;/span&gt;
&lt;span class="nv"&gt;RCLONE_REMOTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gdrive:n8n-backups"&lt;/span&gt;
&lt;span class="nv"&gt;LOCAL_RETENTION_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;14
&lt;span class="nv"&gt;REMOTE_RETENTION_DAYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;90
&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%d_%H-%M-%S"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ENV_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; +a

&lt;span class="nv"&gt;PG_DUMP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/postgres_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.sql"&lt;/span&gt;
&lt;span class="nv"&gt;N8N_TAR_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;/n8n_data_&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres &lt;span class="se"&gt;\&lt;/span&gt;
  pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PG_DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-czf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$N8N_TAR_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; data/n8n

&lt;span class="nv"&gt;REMOTE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RCLONE_REMOTE&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$TS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
rclone &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
rclone copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PG_DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
rclone copy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$N8N_TAR_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMOTE_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKUP_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-mtime&lt;/span&gt; +&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCAL_RETENTION_DAYS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-delete&lt;/span&gt;
rclone delete &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RCLONE_REMOTE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--min-age&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REMOTE_RETENTION_DAYS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;d"&lt;/span&gt; &lt;span class="nt"&gt;--rmdirs&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backup OK: &lt;/span&gt;&lt;span class="nv"&gt;$TS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reliability depends on automation that does not require manual intervention. I use cron for backups, and a long running systemd service for DDNS.&lt;/p&gt;

&lt;p&gt;This backup script runs via cron at 3 AM daily. The cron entry pipes output to a log file, so I have a record of every backup attempt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 3 * * * cd /home/pi/n8n-server &amp;amp;&amp;amp; /bin/bash ./n8n-backup.sh &amp;gt;&amp;gt; /var/log/n8n-backup.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operational check, verify new backup folders are being created in Google Drive and validate local log history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone lsd gdrive:n8n-backups | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50 /var/log/n8n-backup.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a quick restore drill, create a temporary database and import one dump:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'CREATE DATABASE n8n_restore_test;'&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; postgres psql &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; n8n_restore_test &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.n8n/backups/postgres_YYYY-MM-DD_HH-MM-SS.sql"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this works and n8n data archive can be extracted without errors, your backup pipeline is operational, not just successful on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Security and Operations Baseline
&lt;/h2&gt;

&lt;p&gt;Minimal baseline for a n8n service facing public exposure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Restrict inbound ports with UFW firewall, allowing only 80, 443, and local SSH.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80,443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 192.168.1.0/24 proto tcp to any port 22
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enforce HTTPS end to end at the public boundary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nginx Proxy Manager should have &lt;code&gt;Force SSL&lt;/code&gt; enabled for the n8n host, and n8n must keep secure URL settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N8N_PROTOCOL=https
N8N_SECURE_COOKIE=true
N8N_EDITOR_BASE_URL=https://${PUBLIC_DOMAIN}/
WEBHOOK_URL=https://${PUBLIC_DOMAIN}/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids mixed HTTP/HTTPS behavior and ensures cookies and webhook callbacks stay in secure mode.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rotate credentials and minimize secret exposure.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rotate n8n basic auth credentials, PostgreSQL credentials, and Cloudflare API token on a fixed schedule. Use a Cloudflare token scoped only to the required zone and DNS permissions, never your global API key. Keep &lt;code&gt;.env&lt;/code&gt; and DDNS config files outside any version control system.&lt;/p&gt;

&lt;p&gt;Most critical detail for n8n recovery, keep &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; backed up in a secure location. Without that key, stored n8n credentials cannot be decrypted after restore.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Harden SSH access.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Disable password authentication and root login, use SSH keys only, and keep SSH reachable only from your private network range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^#\\?PasswordAuthentication.*/PasswordAuthentication no/'&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^#\\?PermitRootLogin.*/PermitRootLogin no/'&lt;/span&gt; /etc/ssh/sshd_config
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Validate backup restorations, not only backup creation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A successful backup command does not guarantee a valid restore. Test restores periodically in an isolated environment and verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL dump can be restored without errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data/n8n&lt;/code&gt; archive is complete.&lt;/li&gt;
&lt;li&gt;n8n boots correctly with the original &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Critical workflows and credentials load correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Keep management surfaces private.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not expose admin ports publicly. In practice, only forward &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt; in your router. Keep &lt;code&gt;81&lt;/code&gt; (NPM admin), &lt;code&gt;8080&lt;/code&gt; (pgAdmin), and &lt;code&gt;5678&lt;/code&gt; (direct n8n) private to LAN or behind VPN.&lt;/p&gt;

&lt;p&gt;A simple verification is checking exposed listeners from the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tulpen&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'(:80|:81|:443|:5678|:8080)\\b'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Public exposure should be limited to 80/443 through your router rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0015cficwwxx87j4qmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0015cficwwxx87j4qmr.png" alt="n8n login page after successful setup" width="800" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything is configured correctly, opening &lt;code&gt;https://&amp;lt;YOUR_DOMAIN&amp;gt;&lt;/code&gt; should display the n8n login page. This is the expected result after completing the setup and routing public traffic from your domain to the Raspberry Pi server.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusions and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;This setup covers the full path from local deployment to stable public exposure, Docker based service isolation, HTTPS termination, DDNS automation, router alignment with DHCP reservation, and backup automation with restore oriented validation.&lt;/p&gt;

&lt;p&gt;The most important lessons were operational, not tooling specific:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reliability comes from consistent automation, cron, systemd, and health checks.&lt;/li&gt;
&lt;li&gt;Security comes from reducing exposed surface, strict secret handling, and access hardening.&lt;/li&gt;
&lt;li&gt;Backups are useful only when restore is tested and repeatable.&lt;/li&gt;
&lt;li&gt;Portability improves when domain, credentials, and runtime paths are centralized in environment variables.&lt;/li&gt;
&lt;li&gt;The same architecture can be reproduced on Raspberry Pi, or cloud VMs with minimal changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you follow these controls from day one, you get a setup that is easier to operate, easier to recover, and safer to expose publicly.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>docker</category>
      <category>raspberrypi</category>
      <category>automation</category>
    </item>
    <item>
      <title>API-First with OpenAPI Generator: From Spec to REST API and Type-Safe SDKs</title>
      <dc:creator>Juan Carlos González Cabrero</dc:creator>
      <pubDate>Thu, 22 Jan 2026 13:23:37 +0000</pubDate>
      <link>https://forem.com/malkomich/api-first-with-openapi-generator-from-spec-to-rest-api-and-type-safe-sdks-4gl2</link>
      <guid>https://forem.com/malkomich/api-first-with-openapi-generator-from-spec-to-rest-api-and-type-safe-sdks-4gl2</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction &amp;amp; Contract-First Approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij5rmrxb7565m9u00cvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fij5rmrxb7565m9u00cvq.png" alt="A comparison diagram showing two development workflows side-by-side: Traditional approach (Implementation → Documentation → Client Integration) vs Contract-First approach (OpenAPI Spec → Server Implementation + Client SDK, with bidirectional arrows showing synchronization). Should highlight how contract-first prevents drift and mismatches." width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;APIs are the arteries of modern software, powering everything from mobile apps to distributed cloud microservices. Building those APIs, however, is rarely as straightforward as writing a few controller methods. If you've worked on any enough complex backend, you've probably wrestled with inconsistent request/response payloads, mismatched client/server contracts, or ambiguous endpoint documentation. I certainly have, and the pain points are always the same: a frontend developer working from outdated documentation, a client blocked because the API shape changed unexpectedly, or integration tests failing because nobody synchronized the contract.&lt;/p&gt;

&lt;p&gt;That's why I advocate for the "contract-first," or API-first, approach. Instead of treating the OpenAPI specification as an afterthought—something you generate from annotations or write to satisfy a documentation requirement—you define your API contract &lt;em&gt;before&lt;/em&gt; implementing it. This inverts the traditional workflow in a way that fundamentally changes how teams collaborate. Your OpenAPI spec becomes the single source of truth that both server and client implementations derive from, ensuring they can never drift apart. The spec drives automatic, always-current documentation. It aligns product managers, frontend engineers, backend developers, and external partners around a shared understanding before a single line of implementation code is written.&lt;/p&gt;

&lt;p&gt;With tools like the OpenAPI Generator and Maven, you can turn a single OpenAPI spec into a production-grade Java REST backend &lt;em&gt;and&lt;/em&gt; type-safe SDK clients in multiple languages. I've seen this approach cut integration time from weeks to days in microservice architectures and eliminate entire categories of bugs related to contract mismatches. Today, I'll walk you through building a real workflow for this: from designing the OpenAPI 3.0 spec, to generating Spring Boot controllers and Java client SDKs, to handling API evolution gracefully. I'll also share the gotchas I've learned the hard way in production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. OpenAPI Specification Setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtwmer4aqbwghegahvn5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvtwmer4aqbwghegahvn5.jpeg" alt="A visual hierarchy diagram showing how the OpenAPI spec components map to generated artifacts. Show the YAML/JSON spec at the top, with arrows flowing down to: Spring Boot interfaces, Model classes, Validation rules, Client SDKs, and Documentation. Include annotations showing where constraints like 'minLength' and 'required' flow through to generated code." width="501" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The foundation of everything we're building starts with the OpenAPI spec itself. In a contract-first workflow, your OpenAPI YAML or JSON file isn't just documentation—it's an executable contract that drives code generation, testing, and deployment. Every detail matters because ambiguity in the spec translates directly to ambiguity in generated code. I've learned to be obsessively precise here, because time invested in a well-crafted spec pays exponential dividends downstream.&lt;/p&gt;

&lt;p&gt;Consider a basic bookstore API. Before writing any Java controllers or setting up Spring Boot, we define exactly how this API should behave. Here's what that looks like in OpenAPI 3.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.0.3&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bookstore API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
    &lt;span class="s"&gt;APIs to manage books and orders in a bookstore.&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/api&lt;/span&gt;
&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;/books&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List all books&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;200'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A list of books&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;array&lt;/span&gt;
                &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Book'&lt;/span&gt;
    &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add a new book&lt;/span&gt;
      &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/BookRequest'&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Book created&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#/components/schemas/Book'&lt;/span&gt;
&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schemas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Book&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;title&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;author&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uuid&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;number&lt;/span&gt;
        &lt;span class="na"&gt;inStock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
    &lt;span class="na"&gt;BookRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;title&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;author&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
        &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;number&lt;/span&gt;
          &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;inStock&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
          &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the level of specificity. I'm not just saying "there's a title field"—I'm declaring it's required, it's a string, and it must have at least one character. The price must be a number with a minimum value of one. The Book model requires an ID, title, and author, while the BookRequest model (what clients send when creating a book) has slightly different requirements. This separation between request and response models is deliberate. In production, you rarely want clients sending IDs for new resources—those should be server-generated. By defining distinct schemas, the generated code enforces these business rules at compile time.&lt;/p&gt;

&lt;p&gt;The validation constraints embedded here—&lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;—aren't just documentation. When we generate code in the next section, these become actual Hibernate Validator annotations in your Java models. Client SDKs gain the same type safety. A TypeScript client will know that &lt;code&gt;price&lt;/code&gt; is a number, not a string. A Go client will have required fields that can't be omitted. This is the power of treating the contract as code: you're not just describing the API, you're programming the behavior of every system that interacts with it.&lt;/p&gt;

&lt;p&gt;For project organization, I recommend keeping your OpenAPI spec in the resources directory in your repository. In a Maven project, the structure typically looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project-root/
  src/
    main/
      java/...
      resources/
        bookstore-api.yaml
        ...
    test/
      java/...
  pom.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation also enables smooth integration with CI/CD tools. Your pipeline can lint the spec, generate documentation, run contract tests, and publish versioned artifacts—all independent of your Java compilation. The spec becomes the input to multiple downstream processes, not just a sidecar to your server implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Server-Side Generation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzim99ew6kqozn79ghqr5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzim99ew6kqozn79ghqr5.gif" alt="An architecture diagram showing the separation of concerns in generated server code. Display: OpenAPI Spec → OpenAPI Generator Plugin → Generated Interfaces (immutable) and Model Classes → User-Written Controller Implementations (extends interfaces). Use different colors to distinguish generated vs hand-written code, with arrows showing how they interact." width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With a solid spec in hand, we're ready to generate our Spring Boot server code. The OpenAPI Generator Maven plugin is the engine that transforms our declarative YAML into concrete Java interfaces and model classes. Configuring this plugin correctly is crucial because it determines not just what code gets generated, but how that code integrates with your handwritten business logic.&lt;/p&gt;

&lt;p&gt;Here's the plugin configuration in your &lt;code&gt;pom.xml&lt;/code&gt; that I've refined across multiple production projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openapitools&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;openapi-generator-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;7.12.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;code-generate-spring&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/bookstore-api.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;generatorName&amp;gt;&lt;/span&gt;spring&lt;span class="nt"&gt;&amp;lt;/generatorName&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;apiPackage&amp;gt;&lt;/span&gt;com.bookstore.api&lt;span class="nt"&gt;&amp;lt;/apiPackage&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;modelPackage&amp;gt;&lt;/span&gt;com.bookstore.model&lt;span class="nt"&gt;&amp;lt;/modelPackage&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;invokerPackage&amp;gt;&lt;/span&gt;com.bookstore.invoker&lt;span class="nt"&gt;&amp;lt;/invokerPackage&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;library&amp;gt;&lt;/span&gt;spring-boot&lt;span class="nt"&gt;&amp;lt;/library&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;configOptions&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;reactive&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/reactive&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;delegatePattern&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/delegatePattern&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;interfaceOnly&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/interfaceOnly&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;useTags&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/useTags&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;dateLibrary&amp;gt;&lt;/span&gt;java8&lt;span class="nt"&gt;&amp;lt;/dateLibrary&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;useBeanValidation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/useBeanValidation&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/configOptions&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;interfaceOnly&lt;/code&gt; option is the key architectural decision here. When set to true, the generator creates Java interfaces for each API path—not concrete implementations. This gives you the perfect separation of concerns: the generated code defines the contract (method signatures, parameter types, return types), while your handwritten code provides the implementation. I cannot overstate how valuable this separation becomes as APIs evolve. When you modify your OpenAPI spec and regenerate, your IDE immediately shows you which controller methods need updates because the interface changed. No grep'ing through logs, no runtime surprises—just compile-time feedback.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;useBeanValidation&lt;/code&gt; flag enables Hibernate Validator annotations on generated model classes. Remember those &lt;code&gt;minLength&lt;/code&gt; and &lt;code&gt;minimum&lt;/code&gt; constraints in our spec? They become &lt;code&gt;@NotNull&lt;/code&gt;, &lt;code&gt;@Min&lt;/code&gt;, and &lt;code&gt;@Size&lt;/code&gt; annotations in the generated Java code. Spring Boot's validation framework automatically enforces these when requests arrive, rejecting invalid data before it reaches your business logic.&lt;/p&gt;

&lt;p&gt;After running &lt;code&gt;mvn clean generate-sources&lt;/code&gt;, you'll find generated code under &lt;code&gt;target/generated-sources/openapi&lt;/code&gt;. I've seen teams make the mistake of modifying this generated code directly. Don't. Treat generated sources as immutable build artifacts, like compiled &lt;code&gt;.class&lt;/code&gt; files. Any changes you make will be overwritten on the next build. Instead, your implementations live in your main source directory and reference the generated interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.controller&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.api.BooksApi&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.model.Book&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.model.BookRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.http.ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.RestController&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.List&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BooksController&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;BooksApi&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BookRepository&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BookService&lt;/span&gt; &lt;span class="n"&gt;bookService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BooksController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BookRepository&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BookService&lt;/span&gt; &lt;span class="n"&gt;bookService&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bookRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bookService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getBooks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;addBook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BookRequest&lt;/span&gt; &lt;span class="n"&gt;bookRequest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Book&lt;/span&gt; &lt;span class="n"&gt;createdBook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bookService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookRequest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;createdBook&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern feels natural in practice. You're implementing an interface, just like any other Java development. The difference is that this interface was derived from your API contract, so you get compile-time guarantees about correctness. If you add a new required parameter to an endpoint in your OpenAPI spec, the generated interface changes, and your controller won't compile until you update the implementation. This catches integration bugs at build time rather than in staging or—worse—production.&lt;/p&gt;

&lt;p&gt;The validation annotations on generated models work automatically with Spring's &lt;code&gt;@Valid&lt;/code&gt; annotation, but surfacing errors to clients in a user-friendly format requires a bit of plumbing. In production, I always implement a global exception handler to transform validation failures into structured error responses:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@ExceptionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleValidationError&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MethodArgumentNotValidException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ApiError&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Validation failed"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setErrors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBindingResult&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFieldErrors&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getField&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultMessage&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;badRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that when a client sends a book with a negative price or an empty title, they receive a clear, actionable error message rather than a generic 500 or a stack trace. The validation rules specified once in your OpenAPI contract now flow all the way through to the error responses your clients see.&lt;/p&gt;

&lt;p&gt;One pitfall I've encountered repeatedly: teams sometimes struggle with the generated code being "in the way" during development. They're tempted to edit it for quick fixes or to add custom annotations. The solution is to use extension points. If you need custom behavior, extend or wrap the generated classes in your own source tree. Use &lt;code&gt;.openapi-generator-ignore&lt;/code&gt; to prevent the generator from overwriting specific files if you absolutely must customize generated code, but be cautious—you're opting out of automatic contract enforcement for those files. In most cases, composition beats modification: write adapter classes that delegate to generated code while adding your customizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Client SDK Generation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaglu9jdy3r6symh5rfn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhaglu9jdy3r6symh5rfn.jpg" alt="A multi-language SDK generation diagram showing one OpenAPI spec generating client SDKs in multiple languages (Java, TypeScript, Python, Go). Display the spec in the center with arrows pointing outward to each language/framework combination. Include example method signatures to show type-safety in each language (e.g., Java's  raw `List&amp;lt;Book&amp;gt;` endraw , TypeScript's typed promise return, Python's typed response)." width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The contract-first approach truly shines when you realize your OpenAPI spec can generate not just server code, but type-safe client SDKs in virtually any language. This is transformative for microservice architectures and external API consumers. Instead of each client team hand-rolling HTTP requests and parsing JSON, they get a strongly-typed SDK that's automatically synchronized with your server implementation—because both derive from the same contract.&lt;/p&gt;

&lt;p&gt;Generating a Java client SDK uses the same plugin with a different generator configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;generate-java-client&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/bookstore-api.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;generatorName&amp;gt;&lt;/span&gt;java&lt;span class="nt"&gt;&amp;lt;/generatorName&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;library&amp;gt;&lt;/span&gt;resttemplate&lt;span class="nt"&gt;&amp;lt;/library&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;apiPackage&amp;gt;&lt;/span&gt;com.bookstore.client.api&lt;span class="nt"&gt;&amp;lt;/apiPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelPackage&amp;gt;&lt;/span&gt;com.bookstore.client.model&lt;span class="nt"&gt;&amp;lt;/modelPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;output&amp;gt;&lt;/span&gt;${project.build.directory}/generated-sources/java-client&lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running &lt;code&gt;mvn clean generate-sources&lt;/code&gt;, you have a fully functional Java client SDK with the same strong typing as your server. Here's what using that SDK looks like in a separate microservice or integration test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.client.api.BooksApi&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.client.model.Book&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.bookstore.client.model.BookRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.client.RestTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BookInventoryFetcher&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;BooksApi&lt;/span&gt; &lt;span class="n"&gt;booksApi&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BookInventoryFetcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiUrl&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;RestTemplate&lt;/span&gt; &lt;span class="n"&gt;restTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RestTemplate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;booksApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BooksApi&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;booksApi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;setBasePath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiUrl&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Book&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fetchBooks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;booksApi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBooks&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Book&lt;/span&gt; &lt;span class="nf"&gt;createBook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;BookRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BookRequest&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTitle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAuthor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPrice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;booksApi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBook&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's happening here. The client code has no raw HTTP calls, no JSON parsing, no string concatenation of URLs. The &lt;code&gt;BooksApi&lt;/code&gt; class provides type-safe methods like &lt;code&gt;getBooks()&lt;/code&gt; that return &lt;code&gt;List&amp;lt;Book&amp;gt;&lt;/code&gt;. The &lt;code&gt;Book&lt;/code&gt; and &lt;code&gt;BookRequest&lt;/code&gt; models have the same fields, types, and validation constraints as the server. If the server API changes—say, you rename &lt;code&gt;inStock&lt;/code&gt; to &lt;code&gt;available&lt;/code&gt;—the client SDK regenerates with that change, and any code using the old field name stops compiling. This is contract enforcement at its finest.&lt;/p&gt;

&lt;p&gt;The same workflow applies to other languages. Want a TypeScript client for your web frontend? Change &lt;code&gt;generatorName&lt;/code&gt; to &lt;code&gt;typescript-axios&lt;/code&gt;. Need a Python SDK for a data pipeline? Use &lt;code&gt;python&lt;/code&gt;. Go for infrastructure tooling? &lt;code&gt;go&lt;/code&gt;. The OpenAPI Generator supports dozens of languages, each with multiple library options. In one project, I maintained server code in Java, a TypeScript SDK for the React frontend, a Python SDK for ML engineers, and a Go SDK for infrastructure automation—all generated from the same OpenAPI spec. When the API evolved, I regenerated all four SDKs in a single build step. The alternative—manually maintaining four client implementations—would have been a coordination nightmare.&lt;/p&gt;

&lt;p&gt;Publishing these SDKs is straightforward. Package the Java client as a Maven artifact and deploy it to your internal repository. Use npm for TypeScript, PyPI for Python, and so on. Version the SDKs to match your API version, and consuming teams can depend on them like any other library. This transforms your internal APIs into first-class, versioned products rather than endpoints that teams access via curl and prayer.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Advanced Patterns
&lt;/h2&gt;

&lt;p&gt;In production, APIs don't stay static. Requirements change, new features emerge, and you discover design flaws that need correction. The contract-first approach, combined with generated code, provides elegant patterns for handling this evolution while maintaining stability for existing consumers.&lt;/p&gt;

&lt;p&gt;Maintaining multiple API versions side-by-side is surprisingly straightforward. Keep separate spec files—&lt;code&gt;bookstore-v1.yaml&lt;/code&gt; and &lt;code&gt;bookstore-v2.yaml&lt;/code&gt;—and configure separate plugin executions for each. Your server can implement both &lt;code&gt;BooksApiV1&lt;/code&gt; and &lt;code&gt;BooksApiV2&lt;/code&gt; interfaces, routing requests based on a version header or URL path segment like &lt;code&gt;/api/v1/books&lt;/code&gt; versus &lt;code&gt;/api/v2/books&lt;/code&gt;. I've used this pattern to keep legacy endpoints alive for mobile apps that can't update immediately while rolling out breaking changes to web clients. The generated code keeps each version's contract enforced independently, preventing accidental cross-contamination of v1 and v2 logic.&lt;/p&gt;

&lt;p&gt;Integrating code generation into your CI/CD pipeline automates contract enforcement across your entire development workflow. In GitHub Actions or Jenkins, add a build step that runs &lt;code&gt;mvn clean generate-sources&lt;/code&gt; and fails if generated code doesn't match what's committed. This catches developers who modified the spec locally but forgot to regenerate code, or vice versa. Here's a snippet from a GitHub Actions workflow that I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API Contract Validation&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;17'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate OpenAPI code&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mvn clean generate-sources&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check for uncommitted changes&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git diff --exit-code target/generated-sources/openapi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If generated code differs from what's in the repository, the build fails. This simple check has saved me from countless "it works on my machine" bugs related to contract drift.&lt;/p&gt;

&lt;p&gt;Contract testing takes this further by validating that both server and client implementations actually conform to the spec, not just that code was generated from it. Tools like Schemathesis or Spring Cloud Contract can execute tests against the running server using the OpenAPI spec as the test definition. These tests send requests covering every endpoint and parameter combination defined in the spec, then validate responses match the schema. I've caught bugs where business logic returned nullable fields that the spec declared non-nullable, or where enum values in practice diverged from the contract. Traditional unit tests rarely catch these issues because they test specific scenarios, not the entire contract surface.&lt;/p&gt;

&lt;p&gt;Documentation generation is almost trivial when your API is defined in OpenAPI. Adding Springdoc-OpenAPI to your dependencies automatically serves interactive Swagger UI documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;springdoc.api-docs.path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/api-docs&lt;/span&gt;
&lt;span class="py"&gt;springdoc.swagger-ui.path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/swagger-ui.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the server running, navigate to &lt;code&gt;/swagger-ui.html&lt;/code&gt; and you have live, interactive API documentation that stays in sync with your implementation because it's reading directly from the runtime application. For static documentation—say, for embedding in a developer portal—generate ReDoc HTML as part of your build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx redoc-cli bundle src/main/resources/bookstore-api.yaml &lt;span class="nt"&gt;-o&lt;/span&gt; docs/api-documentation.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've seen teams struggle with documentation staleness for years before adopting this approach. Once your spec is the source of truth, documentation becomes a free byproduct rather than a maintenance burden.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Real-World Example: Bookstore Microservice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfnfi2ax9wr4qq6s0m1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfnfi2ax9wr4qq6s0m1t.png" alt="A microservices interaction diagram showing Product Service and Inventory Service, both with the OpenAPI spec as their shared contract. Show: Product Service implementing server interfaces from spec, Inventory Service using generated Java client, and Frontend using generated TypeScript client. Use arrows to indicate contract-driven dependencies and regeneration points. Include model objects (Book, BookRequest) in the center showing they're identical across all services." width="539" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To see how all these pieces fit together in practice, consider a microservices architecture where Product and Inventory services need to coordinate. The Product service manages book metadata and handles customer queries. The Inventory service tracks stock levels and processes reservations. Both need to share a common understanding of what a "book" is and how stock checks work.&lt;/p&gt;

&lt;p&gt;Start by defining a shared OpenAPI contract in &lt;code&gt;bookstore-api.yaml&lt;/code&gt; that includes endpoints for querying available books and reserving inventory. In the Product service's &lt;code&gt;pom.xml&lt;/code&gt;, configure the generator to create Spring server interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;code-generate-spring&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/bookstore-api.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;generatorName&amp;gt;&lt;/span&gt;spring&lt;span class="nt"&gt;&amp;lt;/generatorName&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;apiPackage&amp;gt;&lt;/span&gt;com.company.bookstore.product.api&lt;span class="nt"&gt;&amp;lt;/apiPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelPackage&amp;gt;&lt;/span&gt;com.company.bookstore.model&lt;span class="nt"&gt;&amp;lt;/modelPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;configOptions&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;interfaceOnly&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/interfaceOnly&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;useBeanValidation&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/useBeanValidation&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/configOptions&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Product service implements these interfaces to expose REST endpoints. Meanwhile, in the Inventory service's &lt;code&gt;pom.xml&lt;/code&gt;, configure the generator to create a Java client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;generate-java-client&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/bookstore-api.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;generatorName&amp;gt;&lt;/span&gt;java&lt;span class="nt"&gt;&amp;lt;/generatorName&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;library&amp;gt;&lt;/span&gt;resttemplate&lt;span class="nt"&gt;&amp;lt;/library&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;apiPackage&amp;gt;&lt;/span&gt;com.company.bookstore.inventory.client&lt;span class="nt"&gt;&amp;lt;/apiPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelPackage&amp;gt;&lt;/span&gt;com.company.bookstore.model&lt;span class="nt"&gt;&amp;lt;/modelPackage&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;output&amp;gt;&lt;/span&gt;${project.build.directory}/generated-sources/java-client&lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the Inventory service can call Product service endpoints using a type-safe SDK instead of raw HTTP. Both services share identical model definitions—the &lt;code&gt;Book&lt;/code&gt; class is generated identically in both codebases because it comes from the same spec. If you add a new field to Book in the contract, both services regenerate with that field. The Product service's endpoints automatically accept and return the new field, and the Inventory service's client SDK includes it. There's no way for the services to drift apart because the contract binds them together.&lt;/p&gt;

&lt;p&gt;For frontend integration, add a TypeScript client generation step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;generate-typescript-client&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;generate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/bookstore-api.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;generatorName&amp;gt;&lt;/span&gt;typescript-axios&lt;span class="nt"&gt;&amp;lt;/generatorName&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;output&amp;gt;&lt;/span&gt;${project.build.directory}/generated-sources/typescript-client&lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package the TypeScript SDK as an npm module, publish it to your registry, and frontend developers import it like any library. When they call &lt;code&gt;booksApi.getBooks()&lt;/code&gt;, TypeScript's compiler enforces that they handle the response correctly—it knows the shape of the Book object, which fields are optional, and what types they have. The entire stack, from database to UI, is now bound by a single contract.&lt;/p&gt;

&lt;p&gt;In practice, I've seen this approach cut feature development time dramatically. A new "add review" feature that touches frontend, Product service, and Inventory service used to require careful coordination between three teams to ensure everyone's JSON payloads matched. With generated code, we updated the OpenAPI spec with a new &lt;code&gt;/books/{id}/reviews&lt;/code&gt; endpoint, regenerated all artifacts, and each team implemented against their generated interfaces. Integration happened in hours, not days, because mismatches were caught at compile time.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Pitfalls &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;p&gt;I've deployed this pattern across fintech platforms, e-commerce marketplaces, and SaaS products. It's not a silver bullet—there are scenarios where it struggles and mistakes that can undermine the benefits. Here's what I've learned the hard way.&lt;/p&gt;

&lt;p&gt;Code generation makes the most sense when your API contract is relatively stable. If your spec is in flux—changing daily as you explore a new feature space—you'll spend more time regenerating and updating implementations than you save. In early-stage projects or when prototyping, I sometimes write controllers by hand first, iterate until the design feels right, then reverse-engineer an OpenAPI spec and switch to generation for the production implementation. Trying to spec-first before you understand the problem space leads to churn and frustration.&lt;/p&gt;

&lt;p&gt;Breaking changes are the eternal challenge of API evolution. Even with perfect tooling, removing a field or changing a data type breaks clients. Versioning is the answer, but it requires discipline. When introducing breaking changes, bump the API version in your spec (e.g., from 1.0.0 to 2.0.0), generate new endpoints under &lt;code&gt;/v2&lt;/code&gt;, and keep &lt;code&gt;/v1&lt;/code&gt; alive until all consumers migrate. Set deprecation timelines and communicate them clearly. The generated code can help here—run both v1 and v2 servers simultaneously in the same Spring Boot app, each with their own generated interfaces, until v1 traffic drops to zero.&lt;/p&gt;

&lt;p&gt;Managing generated code across repository boundaries requires careful dependency management. If you're generating and publishing client SDKs, version them carefully. Pin SDK versions in consuming projects to avoid surprise breakages. I've seen teams publish SDK patches that silently changed behavior because they forgot to bump the version number. Treat SDKs as public APIs themselves, with semantic versioning and changelogs. &lt;/p&gt;

&lt;p&gt;Never, ever modify generated source files directly. It's tempting when you're in a hurry—just add that one annotation, tweak that method signature—but you've now created a time bomb. The next developer runs a build, the generator overwrites your change, and hours of debugging ensue. Use &lt;code&gt;.openapi-generator-ignore&lt;/code&gt; sparingly and only for files you truly want to manage manually, like README files or example configurations. For code customizations, extend generated classes or use adapter patterns in your source tree.&lt;/p&gt;

&lt;p&gt;Dependency conflicts between generated code and your application can be subtle. The generated client might pull in a version of Jackson or Spring that conflicts with your main application. Manage this with dependency exclusions in your POM and careful choice of generator libraries. I typically generate clients in separate Maven modules to isolate their dependencies from the main application.&lt;/p&gt;

&lt;p&gt;Test automation is non-negotiable. Just because code is generated doesn't mean it's correct. Your OpenAPI spec might have a typo, or your business logic might not match the contract. Write integration tests that exercise the full request/response cycle. Run contract tests that validate the server against the spec. Test client SDKs against the running server. The tooling gives you compile-time safety for the interface, but runtime correctness is still your responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Conclusions and Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The contract-first approach—defining your OpenAPI spec before writing code—fundamentally changes how teams build and maintain APIs. By using OpenAPI Generator and Maven, you transform a declarative YAML file into a comprehensive suite of server interfaces, data models, client SDKs, and documentation. This inversion of the traditional workflow has profound effects: integration issues surface at compile time rather than runtime, client and server implementations can't drift apart, and documentation stays current because it's generated from the same source of truth.&lt;/p&gt;

&lt;p&gt;From real-world experience across multiple production systems, the productivity gains are substantial. Features that once required careful coordination between backend and frontend teams—with inevitable integration delays when reality didn't match assumptions—now proceed in parallel with confidence. The contract provides a fence that both sides can develop against independently, meeting in the middle with generated code ensuring compatibility.&lt;/p&gt;

&lt;p&gt;The approach scales particularly well as systems grow. Adding a new microservice that needs to call existing APIs? Generate a client SDK and you're working with type-safe methods, not HTTP libraries and JSON parsing. Exposing APIs to external partners? Publish SDKs in their language of choice, all guaranteed to match your server implementation. Supporting mobile apps that update slowly? Keep old API versions alive with separate generated interfaces until usage drops to zero.&lt;/p&gt;

&lt;p&gt;The benefits compound with API maturity. Initial setup has friction—learning the generator options, deciding on project structure, establishing workflows—but once established, the patterns become second nature. Each new endpoint added to the spec automatically propagates to server code, client SDKs, and documentation. Each field added to a model updates everywhere simultaneously. The maintenance burden of keeping disparate systems synchronized largely disappears.&lt;/p&gt;

&lt;p&gt;This isn't a magic solution to all API challenges. You still need to design good APIs, with sensible resource models and clear semantics. You still need to manage versions and communicate breaking changes. You still need comprehensive testing. But the contract-first approach with code generation removes an entire category of problems—the tedious, error-prone work of keeping implementations synchronized with contracts. It lets you focus on the hard problems: what the API should do, not whether all the pieces agree on how it's supposed to work.&lt;/p&gt;

&lt;p&gt;For teams building distributed systems, microservices, or public APIs, the investment in contract-first development with OpenAPI Generator pays off quickly. The upfront effort to learn the tooling and establish patterns is measured in days. The ongoing benefits—fewer integration bugs, faster development, automatic documentation, type-safe clients—accrue over the entire lifetime of the API. In an industry where API maintenance is a major cost driver, this approach provides rare leverage: do the work once in the contract, and propagate it automatically everywhere it's needed.&lt;/p&gt;

</description>
      <category>java</category>
      <category>openapi</category>
      <category>springboot</category>
      <category>api</category>
    </item>
    <item>
      <title>Building a Cloud-Native SaaS Backend on GCP</title>
      <dc:creator>Juan Carlos González Cabrero</dc:creator>
      <pubDate>Sat, 20 Dec 2025 20:31:05 +0000</pubDate>
      <link>https://forem.com/malkomich/building-a-cloud-native-saas-backend-on-gcp-4m16</link>
      <guid>https://forem.com/malkomich/building-a-cloud-native-saas-backend-on-gcp-4m16</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: The Invisible Engine Behind Modern SaaS
&lt;/h2&gt;

&lt;p&gt;When a user clicks 'Sign Up' on a SaaS product or requests some specific data, they expect real-time responsiveness and reliability. Behind this simple interaction runs a sophisticated backend, architected to scale, self-heal, and distribute load across a constellation of microservices. But as more startups embrace cloud-native designs and containerized services become the backbone, one challenge repeatedly emerges: how can we intelligently balance traffic so that it's not just spread evenly, but routed to the healthiest, fastest, and most reliable service endpoints?&lt;/p&gt;

&lt;p&gt;This is far more complex than classic round-robin routing. As anyone running production systems has learned, naive traffic distribution leads to cascading failures when one service goes unhealthy, or bottlenecks when new versions aren't production-ready. In this article, I'll share a detailed backend architecture for cloud-native SaaS on GCP, focusing on &lt;em&gt;intelligent&lt;/em&gt; load balancing for Dockerized Python microservices—using Cloud Load Balancing, GKE/Cloud Run, managed VPC, robust IAM, and native observability features.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Problem Context: Why Naive Load Balancing Fails in Production
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37o9p2csuelyees91vo5.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37o9p2csuelyees91vo5.jpeg" alt="Requests without Load Balancer Meme" width="503" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Picture your SaaS backend composed of User, Billing, and Notification microservices, each containerized with Python and running in GKE. Your API Gateway distributes traffic through Cloud Load Balancer to whichever pods are registered. Everything looks fine in staging. Then production happens.&lt;/p&gt;

&lt;p&gt;A new Billing pod version deploys that takes 30 seconds to warm up its database connection pool. Or perhaps a pod gets bogged down handling a batch export task, spiking latency to 5x normal. Maybe there's a memory leak that slowly degrades performance over hours. Classic load balancers will continue routing users to these struggling pods because, technically, they're still responding to basic health checks. The result? Your P95 latency climbs, timeout errors cascade through dependent services, and customer support tickets flood in.&lt;/p&gt;

&lt;p&gt;Even with built-in Kubernetes readiness probes, the default GCP-managed load balancer doesn't always have granular-enough health data to avoid slow or failing endpoints instantly. The probe might check every 10 seconds, but a pod can fail spectacularly in the intervening time. What we need is intelligent load balancing driven by detailed health signals, readiness gates, real-time metrics, and rapid failure detection. The architecture I'm about to walk you through addresses exactly these challenges, drawn from years of running production SaaS platforms on Google Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Defining Intelligent Load Balancing: Key Requirements
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code or provisioning any infrastructure, I've learned it's critical to be precise about what 'intelligent' actually means in this context. Too often, teams jump straight to implementation without defining success criteria, only to discover months later that their load balancing strategy has subtle but critical gaps.&lt;/p&gt;

&lt;p&gt;Intelligent load balancing means the system only sends traffic to pods that are healthy, live, and genuinely responsive—not just pods that haven't crashed yet. It means distinguishing between containers that are technically running and those that are actually ready to handle production traffic. I've seen too many incidents where a pod passes its health check but is still initializing its database connections or warming up caches, leading to timeouts for the first users who hit it.&lt;/p&gt;

&lt;p&gt;Beyond simple health, intelligent routing must consider real-time performance characteristics. A pod might be healthy but currently experiencing high latency due to garbage collection or resource contention. The load balancer should prefer endpoints with lower, more stable response times. When a pod starts showing elevated error rates or slowdowns, the system needs a feedback loop to temporarily route around it, even if traditional health checks still show it as operational.&lt;/p&gt;

&lt;p&gt;The architecture also needs to play nicely with elastic scaling. As pods spin up and down in response to traffic patterns, the load balancer must smoothly integrate new capacity while draining traffic from pods scheduled for termination. And critically, all of this needs observability built in from day one. Without logs, traces, and metrics feeding back into routing decisions, you're flying blind. This is where GCP's integrated tooling becomes invaluable, providing the telemetry foundation that makes intelligent decisions possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Designing the Cloud-Native Backend Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfzvtcensk0mlbrjtq5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnfzvtcensk0mlbrjtq5g.png" alt="Complete system architecture diagram showing: GCP Cloud Load Balancer at entry point → Network Endpoint Groups (NEGs) → GKE cluster with multiple pods (billing-service, user-service, notification-service) across availability zones → Cloud SQL database and external APIs (Stripe, SendGrid). Should show health check flow from load balancer to pods, readiness/liveness probe endpoints, and the distinction between healthy, warming-up, and unhealthy pods with visual indicators." width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Microservices Design (Python, Docker)
&lt;/h3&gt;

&lt;p&gt;The foundation of intelligent load balancing starts with services that properly communicate their state. I've found that too many microservices treat health checks as an afterthought, implementing them with a simple "return 200 OK" that tells the load balancer nothing useful. Instead, your services need to expose granular information about their actual readiness and health.&lt;/p&gt;

&lt;p&gt;Here's a Python-based billing service that demonstrates the pattern I use in production. Notice how it separates health (is the process alive?) from readiness (is it prepared to serve traffic?):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# billing_service.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/healthz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Report healthy 95% of the time, failure 5%
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unhealthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/readyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Simulate readiness delay on startup
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;START_TIME&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not Ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Simulate payment processing latency
&lt;/span&gt;    &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;START_TIME&lt;/span&gt;
    &lt;span class="n"&gt;START_TIME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation between &lt;code&gt;/healthz&lt;/code&gt; and &lt;code&gt;/readyz&lt;/code&gt; mirrors what I've implemented across dozens of production services. The health endpoint tells Kubernetes whether the process should be restarted—maybe it's deadlocked or has exhausted file descriptors. The readiness endpoint gates whether the pod receives production traffic. During those critical first seconds after startup, while the service is establishing database connections, warming caches, or loading configuration from Secret Manager, readiness returns 503. The load balancer knows to wait.&lt;/p&gt;

&lt;p&gt;In real production code, your readiness check would verify actual dependencies. Can you ping the database? Is Redis responding? Have you loaded your ML model into memory? For the billing service specifically, you might check whether Stripe SDK initialization completed or whether fraud detection rules loaded successfully. The randomness in the health check here simulates intermittent failures you'll encounter in production—network blips, transient resource exhaustion, or external dependency hiccups.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Containerization: Dockerfile Example
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff12lrafycfi74dkx33vq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff12lrafycfi74dkx33vq.png" alt="Container lifecycle flow diagram showing: code → Docker build → image in Artifact Registry → pod deployment on GKE → startup sequence → health check failures during initialization → readiness transition → traffic routing begins. Should clearly show the timeline of initialization, the liveness/readiness probe checks at different stages, and when traffic begins flowing to the pod." width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once your service properly exposes its state, packaging it for cloud-native deployment becomes straightforward. I keep Dockerfiles deliberately simple and focused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; billing_service.py .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "billing_service.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, you'd want to enhance this with multi-stage builds to minimize image size, run as a non-root user for security, and potentially use a requirements.txt for dependency management. But the core pattern remains: a slim base image, minimal layers, clear entrypoint. I've found that optimizing container startup time is one of the highest-leverage improvements you can make for intelligent load balancing, since faster startups mean less time in "not ready" state and smoother scaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 GCP Resource Provisioning: Building and Deploying
&lt;/h3&gt;

&lt;p&gt;With your service containerized, the next step is getting it into GCP's artifact registry and onto your cluster. I typically structure this as a repeatable pipeline, but here's the manual workflow to understand what's happening under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build, tag, and push Docker image to GCP Artifact Registry&lt;/span&gt;
gcloud artifacts repositories create python-services &lt;span class="nt"&gt;--repository-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; us-central1-docker.pkg.dev/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/python-services/billing-service:v1 &lt;span class="nb"&gt;.&lt;/span&gt;

gcloud auth configure-docker us-central1-docker.pkg.dev

docker push us-central1-docker.pkg.dev/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/python-services/billing-service:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What matters here is that you're using Artifact Registry rather than Container Registry. Artifact Registry gives you vulnerability scanning out of the box, better IAM integration, and regional replication options that become critical when you're running multi-region services. I've migrated several production systems from Container Registry to Artifact Registry, and the improved security posture alone justified the effort.&lt;/p&gt;

&lt;p&gt;Now comes the deployment configuration, which is where intelligent load balancing really starts to take shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# k8s/billing-deployment.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-central1-docker.pkg.dev/YOUR_PROJECT/python-services/billing-service:v1&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/readyz&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the probe configuration. I'm checking health every 5 seconds, which in production might be too aggressive depending on your service characteristics. You'll need to tune these values based on actual behavior. If health checks themselves become a source of load, lengthen the period. If you need faster failure detection, shorten it—but be prepared for more false positives during transient issues.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;initialDelaySeconds&lt;/code&gt; setting is critical and often misconfigured. Set it too short, and your pods fail health checks during normal startup, creating a restart loop. Set it too long, and you waste time before traffic can flow to newly scaled pods. I typically start with a value 2x my observed startup time in development, then tune based on production metrics.&lt;/p&gt;

&lt;p&gt;Deploy the service and expose it with these commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/billing-deployment.yaml
kubectl expose deployment billing-service &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LoadBalancer &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="nt"&gt;--target-port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a GCP Load Balancer in front of your deployment automatically, which brings us to the next layer of intelligence.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 GCP Load Balancer with Intelligent Health Checks
&lt;/h3&gt;

&lt;p&gt;When you create a LoadBalancer-type Kubernetes Service, GCP provisions an HTTP(S) Load Balancer that integrates deeply with your GKE cluster. This isn't just forwarding traffic—it's actively monitoring backend health, respecting readiness states, and making routing decisions millisecond by millisecond.&lt;/p&gt;

&lt;p&gt;The real power comes from enabling container-native load balancing through Network Endpoint Groups (NEGs). This allows the GCP load balancer to route directly to pod IPs rather than going through kube-proxy and iptables, reducing latency and improving health check accuracy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# k8s/billing-service.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cloud.google.com/neg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"ingress":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;true}'&lt;/span&gt; &lt;span class="c1"&gt;# Enables container-native load balancing&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
    &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single annotation—&lt;code&gt;cloud.google.com/neg&lt;/code&gt;—transforms your load balancing architecture. I've measured 20-30% latency improvements in production just from enabling NEGs, because you're eliminating a network hop and iptables processing. More importantly for our purposes, it gives the GCP load balancer direct visibility into pod health. When a readiness probe fails, that backend is instantly removed from the load balancer's rotation. No eventual consistency, no delay waiting for endpoints to update.&lt;/p&gt;

&lt;p&gt;Once deployed, you can fine-tune health check behavior through the GCP Console or gcloud commands. In production, I typically adjust the health check interval to balance between rapid failure detection and overhead. I also configure the unhealthy threshold—how many consecutive failures before removing a backend—based on whether I prefer availability (tolerate transient failures) or reliability (fail fast). For a billing service handling payments, I lean toward aggressive failure detection since partial failures can mean dropped transactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Deploying for Readiness, Scaling, and Resilience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Enabling Horizontal Pod Autoscaling
&lt;/h3&gt;

&lt;p&gt;Intelligent load balancing doesn't just mean routing effectively to existing backends—it means ensuring you have the right number of healthy backends available at all times. This is where Kubernetes' Horizontal Pod Autoscaler becomes essential, working in concert with your load balancing strategy.&lt;/p&gt;

&lt;p&gt;The beauty of combining proper health checks with autoscaling is that new pods only enter the load balancer rotation once they're actually ready. There's no race condition where traffic hits a pod that's still initializing. Here's how I typically configure autoscaling for a service like billing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl autoscale deployment billing-service &lt;span class="nt"&gt;--cpu-percent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;70 &lt;span class="nt"&gt;--min&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 &lt;span class="nt"&gt;--max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've learned through painful experience that setting the minimum replica count is just as important as the maximum. Running with fewer than 3 replicas in production means any single pod failure or deployment represents a significant percentage of your capacity, leading to cascading overload. With 3 minimum replicas across multiple availability zones, you maintain headroom even during disruptions.&lt;/p&gt;

&lt;p&gt;The CPU threshold of 70% is conservative, which I prefer for services handling financial transactions. For less critical services, you might push to 80-85% to maximize resource efficiency. But here's what matters: combining autoscaling with readiness probes means traffic surges are handled gracefully. New pods spin up, initialize properly (blocked from traffic by readiness), then seamlessly join the load balancer pool once prepared.&lt;/p&gt;

&lt;p&gt;In more sophisticated setups, I've extended this to use custom metrics—scaling based on request queue depth or P95 latency rather than just CPU. GCP makes this possible through the Custom Metrics API, allowing your application to export business-logic-aware metrics that drive scaling decisions. For a billing service, you might scale based on pending payment jobs rather than generic CPU usage.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Fine-Grained Traffic Splitting for Safe Deployments
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfkl8hi70ygbooojia5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfkl8hi70ygbooojia5u.png" alt="Canary deployment traffic splitting visualization showing: stable deployment (3 pods v1) receiving 75% of traffic → canary deployment (1 pod v2) receiving 25% of traffic → gradual progression showing traffic percentage shift to canary (25% → 50% → 75% → 100%) → promotion to stable as health metrics improve. Should include monitoring panels showing error rates and latency metrics for each version, with clear decision points (scale up, rollback, promote)." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even with intelligent health checks and autoscaling, deploying new code remains the highest-risk operation in production. A bug that makes it past staging can take down your entire service if rolled out to all pods simultaneously. This is where traffic splitting and canary deployments become crucial, and where GKE's integration with GCP load balancing really shines.&lt;/p&gt;

&lt;p&gt;The pattern I use most frequently is a canary deployment with percentage-based traffic splitting. You deploy a new version to a small number of pods while maintaining your stable version, then gradually shift traffic based on observed health metrics. Here's a canary deployment configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# k8s/billing-deployment-canary.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service-canary&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;canary&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;canary&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-central1-docker.pkg.dev/YOUR_PROJECT/python-services/billing-service:v2&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
        &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
        &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/readyz&lt;/span&gt;
            &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
          &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Service selector includes both &lt;code&gt;stable&lt;/code&gt; and &lt;code&gt;canary&lt;/code&gt; version labels, so traffic flows to both. Initially with just 1 canary replica versus 3 stable replicas, roughly 25% of traffic hits the new version. You monitor error rates, latency, and business metrics. If everything looks healthy after an hour, you increase canary replicas to 2, then 3, then eventually promote it to stable while decommissioning the old version.&lt;/p&gt;

&lt;p&gt;What makes this powerful is how it interacts with health checks. If your canary version has a critical bug that causes it to fail readiness probes, it never receives production traffic in the first place. The deployment completes, the pod starts, but the load balancer keeps routing around it. You discover the issue through monitoring rather than customer impact.&lt;/p&gt;

&lt;p&gt;For even more sophisticated deployments, GCP's Traffic Director enables precise traffic splitting percentages, header-based routing for testing specific scenarios, and integration with service mesh capabilities. In one production system I worked on, we routed internal employee traffic to canary versions while keeping all customer traffic on stable, giving us real-world testing without customer risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Observability: Monitoring Health, Latency, and Failures
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ey959n6fm02as5zemd1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ey959n6fm02as5zemd1.webp" alt="Observability feedback loop diagram showing: pods generating metrics (request latency, error rates, custom business metrics) → exported to Cloud Monitoring/Cloud Logging → metrics trigger alerts and autoscaling decisions → autoscaling controller creates/terminates pods → load balancer receives health signals from NEGs → routing decisions adjusted. Should show the circular feedback loop and the role of each component (Prometheus metrics, Cloud Trace, logs) in informing load balancing decisions." width="727" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Logging and Monitoring with Cloud Operations Suite
&lt;/h3&gt;

&lt;p&gt;Here's the uncomfortable truth about load balancing: you can architect the most sophisticated routing logic in the world, but without observability, you're blind to whether it's actually working. Intelligent load balancing requires data—continuous, detailed data about pod health, request latency, error rates, and traffic distribution.&lt;/p&gt;

&lt;p&gt;This is where GCP's Cloud Operations Suite becomes indispensable. The integration with GKE is deep enough that you get pod-level metrics, container logs, and distributed traces with minimal configuration. But getting the most value requires instrumenting your services to export meaningful data that can drive routing decisions.&lt;/p&gt;

&lt;p&gt;For the billing service, I export several classes of metrics. First, the basics—request count, error rate, latency percentiles. These flow automatically through GCP's managed Prometheus if you expose them in the right format. Second, health check results over time, which helps identify patterns in failures. Is a pod failing health checks every morning at 2am during database maintenance? That's a signal to tune your health check logic or adjust maintenance windows.&lt;/p&gt;

&lt;p&gt;Third, and most importantly, custom business metrics that represent actual service health from a user perspective. For billing, that might be payment success rate, time to process refunds, or fraud detection latency. These metrics inform autoscaling, alerting, and ultimately load balancing decisions.&lt;/p&gt;

&lt;p&gt;Here's how to export custom metrics using OpenTelemetry from your Flask service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Export Flask metrics (latency, errors) using OpenTelemetry
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.exporter.cloud_monitoring&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CloudMonitoringMetricsExporter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MeterProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opentelemetry.sdk.metrics.export&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PeriodicExportingMetricReader&lt;/span&gt;

&lt;span class="n"&gt;exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CloudMonitoringMetricsExporter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;meter_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MeterProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;metric_readers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;PeriodicExportingMetricReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_interval_millis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_meter_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meter_provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;meter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_meter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;payment_latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_histogram&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing.payment.latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payment processing latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In your endpoint:
&lt;/span&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/pay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# ... process payment ...
&lt;/span&gt;    &lt;span class="n"&gt;duration_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;payment_latency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these metrics flowing to Cloud Monitoring, your SRE team can make informed decisions. When should you scale? When is a canary actually safer than the stable version? Which pods are consistently slower than their peers? I've built dashboards that show per-pod latency distributions, making it immediately obvious when a single pod is degraded. That visibility has prevented countless incidents by enabling preemptive action before customers notice problems.&lt;/p&gt;

&lt;p&gt;The other critical piece is tracing. Cloud Trace integration with GKE means you can follow a request from the load balancer through your billing service and into downstream calls to payment processors. When P95 latency spikes, you can pinpoint whether it's your code, database queries, or external API calls. This depth of visibility transforms troubleshooting from guesswork into data-driven investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Alerting on Failures and Degrading Latency
&lt;/h3&gt;

&lt;p&gt;Observability data is useless unless it drives action. I configure alert policies that treat different signal types appropriately—some require immediate pages, others just create tickets for investigation during business hours.&lt;/p&gt;

&lt;p&gt;For the billing service, critical alerts include error rate exceeding 1% sustained over 5 minutes, or any instance of payment processing failing for all attempts in a 2-minute window. These page whoever is on-call because they represent immediate customer impact. Medium-severity alerts might fire when P95 latency exceeds 1 second, or when a pod fails health checks more than 3 times in 10 minutes. These create tickets but don't page—they indicate degraded performance that needs investigation but isn't yet critical.&lt;/p&gt;

&lt;p&gt;The key is connecting alerts to automated responses where possible. When error rate spikes on canary pods, automatically roll back the deployment. When autoscaling maxes out capacity, notify the on-call engineer to investigate whether you need to increase limits or optimize performance. When a pod consistently fails health checks after startup, kill it and let Kubernetes reschedule—maybe it landed on a degraded node.&lt;/p&gt;

&lt;p&gt;I've built automation around these alerts using Cloud Functions triggered by Pub/Sub messages from Cloud Monitoring. The function can scale deployments, restart pods, or even drain traffic from an entire cluster if metrics indicate a zone-level failure. This closes the loop from observation to intelligent action without requiring human intervention for common scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Secure Networking, IAM, and Service Access
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 Restricting Internal Traffic with VPCs
&lt;/h3&gt;

&lt;p&gt;Intelligent load balancing isn't just about routing efficiency—it's also about security. Production SaaS systems need defense in depth, where compromising one service doesn't grant access to your entire infrastructure. This is where network policies and VPC configuration become part of your load balancing strategy.&lt;/p&gt;

&lt;p&gt;I deploy production GKE clusters as private clusters, meaning nodes don't have public IP addresses and can't be reached from the internet except through the load balancer. Within the cluster, I use Kubernetes NetworkPolicies to enforce which services can communicate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# k8s/network-policy.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-allow-internal&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-service&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy ensures that only pods labeled &lt;code&gt;app: api-gateway&lt;/code&gt; can initiate connections to billing service pods. If an attacker compromises your notification service, they can't directly access billing. They'd need to pivot through the gateway, which is more heavily monitored and locked down.&lt;/p&gt;

&lt;p&gt;I've seen incidents where network policies prevented lateral movement after a container escape vulnerability. The attacker got pod access but couldn't reach any valuable services because network policies blocked the traffic. It bought enough time to detect and respond before data was compromised.&lt;/p&gt;

&lt;p&gt;The policies also interact with intelligent load balancing in subtle ways. By restricting which services can reach your backends, you ensure all external traffic flows through the load balancer where it's subject to health checks, rate limiting, and observability. Internal service-to-service calls might bypass the load balancer for efficiency, but they're still subject to network policies and service mesh controls if you're running Istio or similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 IAM Controls: Least Privilege
&lt;/h3&gt;

&lt;p&gt;Network policies handle network-level access, but IAM controls what authenticated services can do. I configure every microservice with its own Kubernetes Service Account mapped to a specific GCP Service Account through Workload Identity. The billing service needs access to Cloud SQL for transaction records and Pub/Sub for publishing payment events, but nothing else.&lt;/p&gt;

&lt;p&gt;This principle of least privilege has saved me multiple times. In one incident, a vulnerability in a dependency allowed arbitrary code execution in the notification service. Because that service's IAM permissions were tightly scoped to only send emails via SendGrid, the attacker couldn't access customer payment data, couldn't modify infrastructure, couldn't even list what other services existed. The blast radius was contained to what we could tolerate.&lt;/p&gt;

&lt;p&gt;When combined with intelligent load balancing and health checks, IAM controls ensure that even if a compromised pod passes health checks and receives traffic, the damage it can do is minimized. You've created a system that degrades gracefully even under active attack, continuing to serve legitimate users while containing the compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Production Scenario: Handling a Real Failure
&lt;/h2&gt;

&lt;p&gt;Theory is satisfying, but what matters is how this architecture performs when things go wrong. Here's a scenario I've lived through, with names changed: You deploy a new version of billing-service v2.1.4 that includes an optimization for batch processing. The change looks good in staging. You roll it out as a canary to 10% of production traffic.&lt;/p&gt;

&lt;p&gt;Within minutes, P95 latency for requests hitting the canary pod jumps from 200ms to 3 seconds. Error rate climbs from 0.1% to 2%. In the old architecture, this would mean 10% of your users are having a terrible experience, and you'd be racing to roll back manually while your support team fields angry tickets.&lt;/p&gt;

&lt;p&gt;Instead, here's what happens with intelligent load balancing: The canary pod's readiness probe starts failing because you've configured it to check not just "is the process alive" but "are recent requests completing successfully." After 3 consecutive failures, Kubernetes marks the pod as not ready. The GCP load balancer immediately stops routing new traffic to it, even though the pod is still running. Your healthy stable pods absorb the additional load, and autoscaling spins up an extra stable pod to handle the increased traffic.&lt;/p&gt;

&lt;p&gt;Cloud Monitoring detects the pattern—canary pods failing health checks, latency spike isolated to v2.1.4. An alert fires to your Slack channel. Your automated rollback policy kicks in because the canary exceeded failure thresholds. Within 2 minutes of the initial deployment, the canary is removed, and you're back to running entirely on stable v2.1.3. Total customer impact: a few dozen requests saw elevated latency before the health check failed. No one noticed.&lt;/p&gt;

&lt;p&gt;Your on-call engineer investigates the next morning rather than at 2am. Looking at traces in Cloud Trace, they discover the optimization introduced a database query that locks tables during batch operations, blocking interactive requests. It's fixed in v2.1.5, which passes canary validation and rolls out smoothly.&lt;/p&gt;

&lt;p&gt;This is the promise of intelligent load balancing—not that systems never fail, but that they fail gracefully, contain the blast radius, and provide the visibility needed to fix problems without drama.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Common Pitfalls and Best Practices
&lt;/h2&gt;

&lt;p&gt;Even with the architecture I've described, there are failure modes I've encountered that are worth calling out explicitly. The most common mistake I see is teams implementing health and readiness probes that check the wrong things. Your probe might verify that Flask is responding, but not whether the database connection pool is exhausted. It might return 200 OK while background threads are deadlocked. Effective probes check whether the service can actually fulfill its purpose, not just whether the process is running.&lt;/p&gt;

&lt;p&gt;Another pitfall is tuning health check intervals without considering the full impact. Very aggressive checking (every second) can overwhelm your application with probe traffic, especially if the health check itself is expensive. But very conservative checking (every 30 seconds) means it can take over a minute to detect a failed pod and remove it from rotation. I've found that 5-10 second intervals strike a good balance for most services, but you need to measure in your own environment.&lt;/p&gt;

&lt;p&gt;The fail-open versus fail-closed decision is subtle but critical. When your load balancer has multiple unhealthy backends, should it continue routing to them (fail-open) or refuse traffic entirely (fail-closed)? The right answer depends on your service. For a billing system, I prefer fail-closed—better to return 503 and have clients retry than to process payments incorrectly. For a recommendation engine, fail-open might be better—showing slightly stale recommendations is preferable to showing nothing.&lt;/p&gt;

&lt;p&gt;I always advocate for testing failure scenarios in production with tools like chaos engineering. Use &lt;code&gt;kubectl delete pod&lt;/code&gt; to verify that traffic smoothly fails over to healthy pods. Use network policies to simulate latency or packet loss. Inject failures in your canary deployments intentionally to verify monitoring catches them. Every production service I run has regular chaos experiments scheduled because the confidence they provide is invaluable.&lt;/p&gt;

&lt;p&gt;Finally, load testing is non-negotiable. Use tools like Locust or k6 to simulate realistic traffic patterns and verify that autoscaling responds appropriately, that health checks remain reliable under load, and that your performance assumptions hold. I've discovered countless issues during load tests that never manifested in staging with synthetic traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Conclusions and Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The modern SaaS backend is both a distributed system and a living organism—adapting, self-healing, and scaling on demand. What I've described in this article isn't just theoretical architecture; it's the pattern I've refined across dozens of production systems, validated through incidents that ranged from minor hiccups to company-threatening outages.&lt;/p&gt;

&lt;p&gt;The real insight, which took me years to internalize, is that intelligent load balancing isn't a feature you add at the end. It's an emergent property of good architecture: services that honestly report their state, infrastructure that respects those signals, and observability that closes the feedback loop. When these pieces align, you get a system that routes traffic not based on naive heuristics, but on genuine understanding of backend health and capacity.&lt;/p&gt;

&lt;p&gt;GCP's managed services make this accessible in ways that weren't possible a decade ago. The deep integration between GKE, Cloud Load Balancing, and Cloud Operations means you're not duct-taping together disparate tools—you're working with a coherent platform where health checks flow naturally into routing decisions, where metrics inform autoscaling, and where the blast radius of failures is naturally contained.&lt;/p&gt;

&lt;p&gt;But the technology is only half the story. The teams that succeed with architectures like this are those who obsessively observe their systems in production, who treat every incident as a learning opportunity, and who iterate relentlessly on their traffic control strategies. The advice I've shared comes not from planning but from responding—to cascading failures at 3am, to traffic spikes during product launches, to subtle bugs that only manifest at scale.&lt;/p&gt;

&lt;p&gt;If you take one thing from this article, let it be this: intelligent load balancing is about building systems that fail gracefully and heal automatically, giving you the breathing room to fix problems thoughtfully rather than frantically. It's about creating that invisible engine—fast, resilient, secure, and ready for any growth you throw at it. And perhaps most importantly, it's about letting you sleep through the night while your infrastructure handles the inevitable chaos of production without human intervention.&lt;/p&gt;

&lt;p&gt;The patterns I've shared are battle-tested, but they're not prescriptive. Your SaaS will have different constraints, different failure modes, different business requirements. Adapt these concepts to your context, measure what matters for your services, and build the observability that lets you iterate with confidence. That's how you evolve from naive load balancing to truly intelligent traffic management—one production incident at a time.&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>cloud</category>
      <category>microservices</category>
      <category>python</category>
    </item>
    <item>
      <title>Authenticating REST services with OAuth2</title>
      <dc:creator>Juan Carlos González Cabrero</dc:creator>
      <pubDate>Fri, 06 Aug 2021 22:20:07 +0000</pubDate>
      <link>https://forem.com/malkomich/authenticating-rest-services-with-oauth2-54fp</link>
      <guid>https://forem.com/malkomich/authenticating-rest-services-with-oauth2-54fp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63d4sim72bjmb1q50ty1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63d4sim72bjmb1q50ty1.jpg" alt="Authorized Personnel Only" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;p&gt;When it comes to adding &lt;strong&gt;authorization to call secured services&lt;/strong&gt;, we realize not only that the configuration changes depending on which framework you are going to use, but that for each HTTP client you use, you must configure OAuth2 in a different way.&lt;/p&gt;

&lt;p&gt;For this reason, the simplest thing when implementing an authorization layer through OAuth2 to call those services, would be to outsource the generation of the tokens to a new personalized client. This way we would have a maintainable integration, isolated from the REST client we are using.&lt;/p&gt;

&lt;p&gt;This article guides you through the creation of a simple library which allows you to grant your HTTP requests with the required authorization token, and integrate in your services &lt;strong&gt;whatever client you may use&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;The authorization flow is described in the image above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authorization request is sent from client to OAuth server.&lt;/li&gt;
&lt;li&gt;Access token is returned to the client.&lt;/li&gt;
&lt;li&gt;Access token is then sent from client to the API service (acting as resource server) on each request for a protected resource access.&lt;/li&gt;
&lt;li&gt;Resource server checks the token with the OAuth server, to confirm the client is authorized to consume that resource.&lt;/li&gt;
&lt;li&gt;Server responds with requested protected resources.&lt;/li&gt;
&lt;/ol&gt;



&lt;h2&gt;
  
  
  2. Setting up the required dependencies
&lt;/h2&gt;

&lt;p&gt;We will need a few libraries to build our custom OAuth2 client.&lt;/p&gt;

&lt;p&gt;First of all, the &lt;strong&gt;Apache HTTP&lt;/strong&gt; client library, which will provide us with the HTTP client for the integration with the authorization server, as well as a toolset for the request building. So it would be the core library for our client.&lt;/p&gt;

&lt;p&gt;In the second one, we find another Apache library, called &lt;strong&gt;&lt;em&gt;cxf-rt-rs-security-oauth2&lt;/em&gt;&lt;/strong&gt;. In this case, this dependency would be optional, since we only need a set of predefined values in the OAuth2 Protocol definition, gathered in the &lt;code&gt;OAuthConstants&lt;/code&gt; class. We could also define those values by ourselves, to get rid of this dependency.&lt;/p&gt;

&lt;p&gt;Lastly, we include the &lt;strong&gt;json&lt;/strong&gt; library. This library is a helpful toolset when we are handling JSON data. It is really useful to parse and manipulate JSON in Java.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.httpcomponents&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;httpclient&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.5&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.cxf&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;cxf-rt-rs-security-oauth2&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.4.2&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.json&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;json&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;20160212&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  3. Building the OAuth2 request
&lt;/h2&gt;

&lt;p&gt;We have to build the request to the server which will authorize our service as a granted client.&lt;br&gt;
To achieve this, we need to define the OAuth2 configuration we are using, including the &lt;strong&gt;grant type&lt;/strong&gt;, the authorization &lt;strong&gt;server URL&lt;/strong&gt;, the &lt;strong&gt;credentials&lt;/strong&gt; for the given grant type, and the &lt;strong&gt;scope&lt;/strong&gt; for the resource we are requesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;grantType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientSecret&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accessTokenUri&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we have the configuration values initialized, we can use them to build the HTTP request for the authorization server.&lt;br&gt;
Typically, the HTTP method used to get the access token, will be a POST, as defined in the &lt;a href="https://tools.ietf.org/html/draft-ietf-oauth-v2-22" rel="noopener noreferrer"&gt;OAuth 2.0 Authorization Protocol specification&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The client MUST use the HTTP "POST" method when making access token requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Depending on the grant type we define, we must define different parameters on the POST request. We will use a list of &lt;code&gt;NameValuePair&lt;/code&gt; to gather all those needed parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;HttpUriRequest&lt;/span&gt; &lt;span class="nf"&gt;buildRequest&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NameValuePair&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
  &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;GRANT_TYPE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGrantType&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScope&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScope&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SCOPE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScope&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CLIENT_CREDENTIALS_GRANT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGrantType&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClientId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClientSecret&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESOURCE_OWNER_GRANT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGrantType&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESOURCE_OWNER_NAME&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BasicNameValuePair&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESOURCE_OWNER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPassword&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RequestBuilder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpPost&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;METHOD_NAME&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccessTokenUri&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UrlEncodedFormEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formData&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  4. Executing the OAuth2 request
&lt;/h2&gt;

&lt;p&gt;Since we are building an OAuth2 client as basic as possible, we will use the default HTTP client from &lt;strong&gt;&lt;em&gt;Apache HTTP&lt;/em&gt;&lt;/strong&gt; library, to send our request to the authorization server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CloseableHttpResponse&lt;/span&gt; &lt;span class="nf"&gt;doRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpUriRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;CloseableHttpClient&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClients&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDefault&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuth2ClientException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"An error occurred executing the request."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once we receive the response, we need to handle it, extracting the information we need for the access token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Response&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;HttpEntity&lt;/span&gt; &lt;span class="n"&gt;httpEntity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should check for errors before parsing the content to get the access token. We can consider here errors in the credentials we defined, a wrong or malformed URL, or any internal error from the authorization server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;OAuth2Response&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpUriRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;CloseableHttpResponse&lt;/span&gt; &lt;span class="n"&gt;httpResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="nc"&gt;HttpEntity&lt;/span&gt; &lt;span class="n"&gt;httpEntity&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getEntity&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;statusCode&lt;/span&gt;                     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusLine&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                                                   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatusCode&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuth2ClientException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpEntity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuth2Response&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpEntity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should not forget to close the &lt;code&gt;httpResponse&lt;/code&gt;, to avoid the memory leakage. But it is pretty important to wait until it is read properly, since it contains an InputStream which would become inaccessible once we have closed it.&lt;/p&gt;



&lt;p&gt;Typically, the response content will come on a JSON format, with the access token data in a key-value schema. However, we should consider a server handling the data in a different format, like XML or URL encoded.&lt;/p&gt;

&lt;p&gt;For the scope of this article, we will consider our authorization server giving us JSON formatted content. The &lt;strong&gt;&lt;em&gt;org.json:json&lt;/em&gt;&lt;/strong&gt; library we included earlier, will help us on the deserialization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt; &lt;span class="nf"&gt;handleResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpEntity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extractEntityContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofNullable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContentType&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                               &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Header:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                               &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMimeType&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JSONObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;extractEntityContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpEntity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;EntityUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuth2ClientException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"An error occurred while extracting entity content."&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Given the &lt;code&gt;JSONObject&lt;/code&gt;, it becomes much easier to handle the response, since we can retrieve instantly each value we are interested in.&lt;/p&gt;



&lt;h2&gt;
  
  
  5. Putting all together
&lt;/h2&gt;

&lt;p&gt;The goal here is to obtain an access token to call the secured services we need. However, sometimes we also need to know some additional data, like the timestamp when the token is going to expire, the token type we are receiving, or the refresh token in the case the grant type is defined so.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccessToken&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;expiresIn&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tokenType&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;refreshToken&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nc"&gt;AccessToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JSONObject&lt;/span&gt; &lt;span class="n"&gt;jsonObject&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;expiresIn&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;optLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ACCESS_TOKEN_EXPIRES_IN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;tokenType&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;optString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ACCESS_TOKEN_TYPE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;refreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;optString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;REFRESH_TOKEN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;accessToken&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonObject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;optString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;ACCESS_TOKEN&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we will get a client which will retrieve the access token data needed to grant our calls to the services, based on the configuration we defined.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AccessToken&lt;/span&gt; &lt;span class="nf"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;HttpUriRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildRequest&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="nc"&gt;OAuth2Response&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AccessToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handleResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHttpEntity&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h2&gt;
  
  
  6. Put into practice
&lt;/h2&gt;

&lt;p&gt;But, how could we integrate this custom client in our service?&lt;/p&gt;

&lt;p&gt;Well, as I mentioned at the beginning of the article, the idea of this custom OAuth2 client is to be &lt;strong&gt;isolated from the framework and/or the HTTP client&lt;/strong&gt; we are using to consume the secured services.&lt;/p&gt;

&lt;p&gt;So I will show you a few examples of how to integrate it in different service environments.&lt;/p&gt;



&lt;h3&gt;
  
  
  6.1. Spring Framework - WebClient
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebClientConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"securedWebClient"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;WebClient&lt;/span&gt; &lt;span class="nf"&gt;fetchWebClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${host}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                           &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="n"&gt;oAuth2Config&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt; &lt;span class="n"&gt;oAuth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Config&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OAuth2ExchangeFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;
  &lt;span class="nd"&gt;@ConfigurationProperties&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"security.oauth2.config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="nf"&gt;oAuth2Config&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuth2Config&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuth2ExchangeFilter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ExchangeFilterFunction&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt; &lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ClientResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ClientRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                       &lt;span class="nc"&gt;ExchangeFunction&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofNullable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AccessToken:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedException&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

      &lt;span class="nc"&gt;ClientRequest&lt;/span&gt; &lt;span class="n"&gt;newRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ClientRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHORIZATION&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newRequest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  6.2. Spring Framework - Feign Client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeignClientConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="nd"&gt;@Bean&lt;/span&gt;
  &lt;span class="nc"&gt;OAuthRequestInterceptor&lt;/span&gt; &lt;span class="nf"&gt;repositoryClientOAuth2Interceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Client&lt;/span&gt; &lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OAuthRequestInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OAuthRequestInterceptor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestInterceptor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt; &lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RequestTemplate&lt;/span&gt; &lt;span class="n"&gt;requestTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofNullable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                                 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AccessToken:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                                 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedException&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

      &lt;span class="n"&gt;requestTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHORIZATION&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  6.3. Vert.x - Web Client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProtectedResourceHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RoutingContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="n"&gt;oAuth2Config&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nc"&gt;ProtectedResourceHandler&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Resource handler initialization&lt;/span&gt;
    &lt;span class="n"&gt;oAuth2Config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth2Config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="nf"&gt;oauth2Config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonObject&lt;/span&gt; &lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grantType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"grantType"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessTokenUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"accessTokenUri"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clientId"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clientSecret"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RoutingContext&lt;/span&gt; &lt;span class="n"&gt;routingContext&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;WebClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routingContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vertx&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAbs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;putHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHORIZATION&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpResponse&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Successful response handler */&lt;/span&gt; &lt;span class="o"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onFailure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Error response handler */&lt;/span&gt; &lt;span class="o"&gt;});&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Config&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OAuth2Client:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AccessToken:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedException&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;h3&gt;
  
  
  6.4. Quarkus - RESTEasy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RegisterRestClient&lt;/span&gt;
&lt;span class="nd"&gt;@RegisterClientHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecurityHeaderFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;DocumentClient&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;// External endpoints definition&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityHeaderFactory&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ClientHeadersFactory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt; &lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@Inject&lt;/span&gt;
  &lt;span class="nc"&gt;SecurityHeaderFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Properties&lt;/span&gt; &lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;oAuth2Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Client&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oauth2Config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@Override&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;MultivaluedMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MultivaluedMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;incomingHeaders&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                               &lt;span class="nc"&gt;MultivaluedMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outgoingHeaders&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;outgoingHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpHeaders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTHORIZATION&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outgoingHeaders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;generateToken&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Client&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OAuth2Client:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;AccessToken:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAccessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AccessDeniedException&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt; &lt;span class="nf"&gt;oauth2Config&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OAuth2Properties&lt;/span&gt; &lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;OAuth2Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;grantType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGrantType&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accessTokenUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAccessTokenUri&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClientId&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClientSecret&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPassword&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oAuth2Properties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScope&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;p&gt;In this article, we have seen how we can set up a &lt;strong&gt;simple OAuth2 Client&lt;/strong&gt;, and how we can integrate it in your REST calls to retrieve a secured resource from an external service.&lt;/p&gt;

&lt;p&gt;You can check the code used for the OAuth2 Client, the repository is available over on &lt;strong&gt;&lt;a href="https://github.com/malkomich/oauth2-token-client" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>oauth2</category>
      <category>rest</category>
      <category>authorization</category>
      <category>microservices</category>
    </item>
  </channel>
</rss>
