<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: skaca8</title>
    <description>The latest articles on DEV Community by skaca8 (@skaca8).</description>
    <link>https://dev.to/skaca8</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%2F3941139%2F6f92a6cc-50e1-4611-9e93-4c874d605f20.png</url>
      <title>DEV Community: skaca8</title>
      <link>https://dev.to/skaca8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/skaca8"/>
    <language>en</language>
    <item>
      <title>A YAML-Driven Multi-Channel RestClient — The Story of Building mido-client</title>
      <dc:creator>skaca8</dc:creator>
      <pubDate>Wed, 20 May 2026 01:47:53 +0000</pubDate>
      <link>https://dev.to/skaca8/a-yaml-driven-multi-channel-restclient-the-story-of-building-mido-client-4c0j</link>
      <guid>https://dev.to/skaca8/a-yaml-driven-multi-channel-restclient-the-story-of-building-mido-client-4c0j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I built a Spring Boot 3.2+ open-source library that lets you declare and manage external API channels entirely from YAML.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  It started in the travel industry
&lt;/h2&gt;

&lt;p&gt;The thing is, every OTA has different API specs. One pattern that shows up often is when &lt;strong&gt;the lookup host and the booking/payment host are separated&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://api-search.someota.com&lt;/code&gt; → product lookup, price check&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;https://api-booking.someota.com&lt;/code&gt; → actual booking and payment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same OTA, but two endpoints — different auth tokens, different timeout policies.&lt;br&gt;
Once you have 10 or 20 such OTAs, the problem starts in earnest.&lt;br&gt;
Similar-looking &lt;code&gt;@Bean&lt;/code&gt; declarations grow with the channel count — and double again for any channel with dual endpoints — honestly piling up.&lt;/p&gt;
&lt;h2&gt;
  
  
  The limits of RestClient + &lt;a class="mentioned-user" href="https://dev.to/bean"&gt;@bean&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;My first attempt was the obvious one: register &lt;code&gt;RestClient&lt;/code&gt; (introduced in Spring Boot 3.2) as &lt;code&gt;@Bean&lt;/code&gt;s. It was boilerplate hell.&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;@Configuration&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;OtaClientConfig&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="s"&gt;"abcOtaSearchClient"&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;RestClient&lt;/span&gt; &lt;span class="nf"&gt;abcOtaSearchClient&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="nf"&gt;buildClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"https://api-search.someota.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABC_SEARCH_TOKEN"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcOtaBookingClient"&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;RestClient&lt;/span&gt; &lt;span class="nf"&gt;abcOtaBookingClient&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="nf"&gt;buildClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"https://api-booking.someota.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ABC_BOOKING_TOKEN"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="c1"&gt;// Two more beans for XYZ OTA&lt;/span&gt;
    &lt;span class="c1"&gt;// Two more beans for DEF OTA&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 10 channels means 20 beans&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;RestClient&lt;/span&gt; &lt;span class="nf"&gt;buildClient&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;baseUrl&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;Duration&lt;/span&gt; &lt;span class="n"&gt;readTimeout&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Duration&lt;/span&gt; &lt;span class="n"&gt;connectTimeout&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// factory, interceptor, logging setup...&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;Sure, you can extract a &lt;code&gt;buildClient&lt;/code&gt; helper. But the more channels you add, the more &lt;code&gt;@Bean&lt;/code&gt; methods pile up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not OpenFeign?
&lt;/h2&gt;

&lt;p&gt;OpenFeign was on the table, of course. Clean abstraction — just interfaces and annotations. But when I actually tried it, things kept getting in the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. It doesn't sit on top of Spring Boot 3.2's &lt;code&gt;RestClient&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;
OpenFeign runs on Feign internally. If your org wants a unified HTTP client policy built on &lt;code&gt;RestClient&lt;/code&gt;, that consistency breaks the moment you adopt Feign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Mapping two endpoints to one service is awkward.&lt;/strong&gt;&lt;br&gt;
The dual-host OTA pattern — lookup and booking on different URLs — forces you to split into two Feign interfaces. "It's the same OTA, why am I splitting it into two classes?" That nagging feeling never goes away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Customization is shallow.&lt;/strong&gt;&lt;br&gt;
Shaping request/response logging into a specific format (URL + body + response time + status) means bringing in a plugin or reconfiguring &lt;code&gt;Logger.Level&lt;/code&gt;. Details like gzip compression or decompression-bomb defense? You write those yourself. I spent days just matching the log format ops required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The code grows back anyway.&lt;/strong&gt;&lt;br&gt;
Interfaces, fallbacks, configuration classes, &lt;code&gt;ErrorDecoder&lt;/code&gt;, &lt;code&gt;RequestInterceptor&lt;/code&gt; — what the annotations seemed to save up front, you end up paying back elsewhere.&lt;/p&gt;

&lt;p&gt;What I wanted was simple: &lt;strong&gt;"if I look at one YAML, I can see exactly how this system talks to the outside world."&lt;/strong&gt; OpenFeign doesn't get you there.&lt;/p&gt;
&lt;h2&gt;
  
  
  So I built it myself — mido-client
&lt;/h2&gt;

&lt;p&gt;I searched existing open-source options for a while. A few came close in concept, but none of them ticked every box. So I built it myself. Its name is &lt;strong&gt;mido-client&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/skaca8/mido-client" rel="noopener noreferrer"&gt;https://github.com/skaca8/mido-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central: &lt;a href="https://central.sonatype.com/artifact/io.github.skaca8/mido-client" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.skaca8/mido-client&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core idea is simple.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Declare external API channels in YAML. Let code focus on business logic.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RestClient (vanilla)&lt;/th&gt;
&lt;th&gt;OpenFeign&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;mido-client&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Configuration style&lt;/td&gt;
&lt;td&gt;Java &lt;code&gt;@Bean&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Interface + annotations&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;YAML only&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-channel setup&lt;/td&gt;
&lt;td&gt;Manual per bean&lt;/td&gt;
&lt;td&gt;Manual per interface&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual endpoint per service&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in (primary / secondary)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request/response logging&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Plugin required&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in (4 levels)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client caching&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Managed by framework&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Built-in (ConcurrentHashMap)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Based on&lt;/td&gt;
&lt;td&gt;Spring Boot 3.2+&lt;/td&gt;
&lt;td&gt;Feign&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Spring Boot 3.2+ RestClient&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  How the code shrinks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; — 4 beans + a config class.&lt;br&gt;
&lt;strong&gt;After&lt;/strong&gt; — one block in &lt;code&gt;application.yml&lt;/code&gt;.&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;mido-client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&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;channels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;abcOta&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ABC&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;OTA"&lt;/span&gt;
      &lt;span class="na"&gt;charset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UTF-8&lt;/span&gt;
      &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                            &lt;span class="c1"&gt;# lookup endpoint&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;https://api-search.someota.com&lt;/span&gt;
        &lt;span class="na"&gt;read-timeout-seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
        &lt;span class="na"&gt;connect-timeout-seconds&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;authorization&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;bearer&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ABC_SEARCH_TOKEN}&lt;/span&gt;
        &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;console&lt;/span&gt;
      &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;                          &lt;span class="c1"&gt;# booking endpoint&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;https://api-booking.someota.com&lt;/span&gt;
        &lt;span class="na"&gt;read-timeout-seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
        &lt;span class="na"&gt;authorization&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;bearer&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${ABC_BOOKING_TOKEN}&lt;/span&gt;
        &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;                          &lt;span class="c1"&gt;# console + file&lt;/span&gt;
    &lt;span class="na"&gt;xyzOta&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;primary&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;https://api.xyzota.com&lt;/span&gt;
        &lt;span class="na"&gt;authorization&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;bearer&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${XYZ_TOKEN}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AbcOtaService&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseExternalApi&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;RestClient&lt;/span&gt; &lt;span class="n"&gt;searchClient&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;RestClient&lt;/span&gt; &lt;span class="n"&gt;bookingClient&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;AbcOtaService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MidoClientFactory&lt;/span&gt; &lt;span class="n"&gt;midoClientFactory&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;searchClient&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;midoClientFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrCreateClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcOta"&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;bookingClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;midoClientFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrCreateClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcOta"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EndpointType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECOND&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;protected&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getChannelName&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="s"&gt;"abcOta"&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;HotelList&lt;/span&gt; &lt;span class="nf"&gt;searchHotels&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchCondition&lt;/span&gt; &lt;span class="n"&gt;cond&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="nf"&gt;withDefaultChannelAction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchHotels"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;searchClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&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="s"&gt;"/hotels/search"&lt;/span&gt;&lt;span class="o"&gt;)&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;cond&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HotelList&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="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;BookingResult&lt;/span&gt; &lt;span class="nf"&gt;book&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BookingRequest&lt;/span&gt; &lt;span class="n"&gt;req&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="nf"&gt;withDefaultChannelAction&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"book"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;bookingClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&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="s"&gt;"/bookings"&lt;/span&gt;&lt;span class="o"&gt;)&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;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retrieve&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BookingResult&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="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;@Bean&lt;/code&gt; methods. No factory classes. No proliferating interfaces. Want to add another channel? Add one more block in YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Details worth showing off
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Four-level logging — ops will thank you
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;       &lt;span class="c1"&gt;# off&lt;/span&gt;
&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;console&lt;/span&gt;   &lt;span class="c1"&gt;# console only&lt;/span&gt;
&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;      &lt;span class="c1"&gt;# file only (MidoClientFileLog logger)&lt;/span&gt;
&lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;       &lt;span class="c1"&gt;# console + file simultaneously&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each log line includes the request URL, method, request/response body, response time, and status. Whenever ops asks "trace this OTA request and tell me where it came from," you no longer need to touch the logging code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ChannelContext + MDC — distributed log tracing
&lt;/h3&gt;

&lt;p&gt;A single &lt;code&gt;withDefaultChannelAction("methodName", ...)&lt;/code&gt; call automatically writes &lt;code&gt;channelAction&lt;/code&gt; into SLF4J MDC.&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;pattern&amp;gt;&lt;/span&gt;%d [%X{channelAction}] %-5level %msg%n&lt;span class="nt"&gt;&amp;lt;/pattern&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a single log line, you can immediately see which channel and which action it broke at. Even when business logic throws an exception, &lt;code&gt;BaseExternalApi&lt;/code&gt; handles it so the context is automatically cleared in &lt;code&gt;finally&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not ScopedValue?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;ScopedValue&lt;/code&gt; (JEP 446 / JDK 21+) is the modern way to do context propagation, but mido-client targets &lt;strong&gt;Java 17 as its minimum&lt;/strong&gt; — so we went with &lt;code&gt;ThreadLocal&lt;/code&gt; for now. I personally run Java 25, but that's too bleeding-edge to force on library users. We plan to migrate to &lt;code&gt;ScopedValue&lt;/code&gt; when we bump the library's minimum Java version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Gzip — compression + decompression-bomb defense
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;gzip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;request&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;response&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;min-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;                   &lt;span class="c1"&gt;# skip compression for bodies &amp;lt; 1KB&lt;/span&gt;
  &lt;span class="na"&gt;max-decompressed-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10485760&lt;/span&gt;  &lt;span class="c1"&gt;# IOException if response &amp;gt; 10MB after decompression&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request compression, automatic response decompression, &lt;strong&gt;and a decompression-bomb defense cap&lt;/strong&gt; baked in. Some OTAs occasionally return abnormally inflated responses, so having a memory cap is genuinely reassuring.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Custom interceptors registered in YAML
&lt;/h3&gt;

&lt;p&gt;Want to plug in Resilience4j? Just put the class name in &lt;code&gt;interceptors:&lt;/code&gt;.&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;interceptors&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;com.yourapp.PaymentResilienceInterceptor"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mido-client deliberately does NOT bundle a resilience layer. Each team's preferred tool (Resilience4j, Sentinel, Spring Retry, Failsafe...) plugs in via this hook. The README ships with a full Resilience4j integration recipe.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Fail-fast config validation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@Validated&lt;/code&gt; validates &lt;code&gt;@ConfigurationProperties&lt;/code&gt;. If a URL is blank, a timeout is negative, or a required &lt;code&gt;primary&lt;/code&gt; endpoint is missing, it throws a runtime error.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. JSON / XML channel type
&lt;/h3&gt;

&lt;p&gt;If you have a legacy SOAP OTA still hanging around — yes, those happen — just declare &lt;code&gt;type: xml&lt;/code&gt; on the channel and &lt;code&gt;Content-Type: application/xml&lt;/code&gt; is attached automatically.&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;channels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;legacySoap&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;xml&lt;/span&gt;
    &lt;span class="na"&gt;primary&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;https://soap.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Multi-header support
&lt;/h3&gt;

&lt;p&gt;You can register multiple static headers per endpoint. Values that need to ride along on every request — auth tokens, tracing IDs, API versions — can all be collected in one place in YAML.&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;headers&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;X-AUTH-TOKEN&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${AUTH_TOKEN}&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;X-API-Version&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'io.github.skaca8:mido-client:1.0.8'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mido-client&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&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;channels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;yourChannel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;first&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;https://api.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two blocks are all you need. In addition to Maven Central, JitPack-based GitHub dependency is also supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code shouldn't grow just because channels grow.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open &lt;code&gt;application.yml&lt;/code&gt; once, and you can see — at a glance — where and how this service talks to the outside world.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honestly, I built this library to scratch my own itch, so I hope it helps others working in similar environments. If you've ever watched your bean count balloon as you added one more channel, or thought "OpenFeign feels a bit heavy for what I need," give it a try. Feedback, issues, and PRs are all welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/skaca8/mido-client" rel="noopener noreferrer"&gt;https://github.com/skaca8/mido-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central: &lt;a href="https://central.sonatype.com/artifact/io.github.skaca8/mido-client" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.skaca8/mido-client&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: Apache 2.0&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; The library's name &lt;code&gt;mido&lt;/code&gt; is the name of &lt;strong&gt;my Maltese, who has been living with me for 13 years&lt;/strong&gt;.&lt;br&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%2Fbzn6reiog7fjl8ns6s82.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%2Fbzn6reiog7fjl8ns6s82.jpeg" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>opensource</category>
      <category>restapi</category>
    </item>
  </channel>
</rss>
