<?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: lanyxp</title>
    <description>The latest articles on DEV Community by lanyxp (@showingdata).</description>
    <link>https://dev.to/showingdata</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3988089%2Fe0727f11-ddd9-439f-b7a8-c4b4fdbf5093.jpeg</url>
      <title>DEV Community: lanyxp</title>
      <link>https://dev.to/showingdata</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/showingdata"/>
    <language>en</language>
    <item>
      <title>Your circuit breaker stops at the service layer. Slow SQL needs one too.</title>
      <dc:creator>lanyxp</dc:creator>
      <pubDate>Wed, 17 Jun 2026 01:35:16 +0000</pubDate>
      <link>https://dev.to/showingdata/your-circuit-breaker-stops-at-the-service-layer-slow-sql-needs-one-too-3bee</link>
      <guid>https://dev.to/showingdata/your-circuit-breaker-stops-at-the-service-layer-slow-sql-needs-one-too-3bee</guid>
      <description>&lt;h1&gt;
  
  
  Your circuit breaker stops at the service layer. Slow SQL needs one too.
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://central.sonatype.com/artifact/io.github.showingdata.starter.framework/sql-circuit-breaker-spring-boot-starter" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fmaven-central%2Fv%2Fio.github.showingdata.starter.framework%2Fsql-circuit-breaker-spring-boot-starter.svg%3Flabel%3DMaven%2520Central" alt="Maven Central" width="134" height="20"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/showingdata/sql-circuit-breaker" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FGitHub-showingdata%252Fsql--circuit--breaker-181717%3Flogo%3Dgithub" alt="GitHub" width="251" height="20"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://gitee.com/LanyXP/sql-circuit-breaker" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FGitee-LanyXP%252Fsql--circuit--breaker-C71D23%3Flogo%3Dgitee" alt="Gitee" width="213" height="20"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.apache.org/licenses/LICENSE-2.0" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FLicense-Apache%25202.0-blue.svg" alt="License" width="124" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A single slow query can take down an entire service in seconds. This post starts with a production cascade failure and walks through how I built a Spring Boot Starter that does circuit breaking &lt;strong&gt;at the MyBatis interceptor layer&lt;/strong&gt;, keyed by SQL type + SQL fingerprint — plus a few design trade-offs worth talking about. The SDK is open source, on Maven Central, and takes one dependency + a bit of YAML to wire in.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/showingdata/sql-circuit-breaker" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://gitee.com/LanyXP/sql-circuit-breaker" rel="noopener noreferrer"&gt;Gitee&lt;/a&gt; (a Star ⭐ helps if you find it useful)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  1. A short war story: how one slow query took down a service
&lt;/h2&gt;

&lt;p&gt;One evening at peak traffic, the order service started timing out everywhere and alerts went off. The root cause was mundane: a list query had picked up a new filter condition that wasn't covered by an index, turned into a full table scan, and took 30+ seconds per call.&lt;/p&gt;

&lt;p&gt;The thing is, &lt;strong&gt;it doesn't stop at "slow once"&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One slow query holds a DB connection hostage for 30 seconds.&lt;/li&gt;
&lt;li&gt;Under load, more of the same request keep coming in, eating connections one by one.&lt;/li&gt;
&lt;li&gt;The connection pool drains → every &lt;em&gt;other&lt;/em&gt; query (including perfectly healthy ones) can't get a connection, queues up, times out.&lt;/li&gt;
&lt;li&gt;Upstream threads all block waiting for connections → endpoints time out → callers retry → it gets worse.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One slow query ends up dragging down &lt;strong&gt;the whole database, and the whole service&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the post-mortem the first reaction was: &lt;em&gt;"Don't we already have a circuit breaker?"&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Why the usual circuit breakers aren't enough
&lt;/h2&gt;

&lt;p&gt;Resilience4j, Hystrix, Sentinel — all mature. But they operate at the &lt;strong&gt;endpoint / RPC / method-call&lt;/strong&gt; level. They can tell you &lt;em&gt;"this endpoint's failure rate is high, trip it"&lt;/em&gt;, but they &lt;strong&gt;don't understand SQL&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They don't know &lt;em&gt;which&lt;/em&gt; query is slow — they can only trip the whole endpoint, even though 90% of the SQL behind it might be perfectly healthy.&lt;/li&gt;
&lt;li&gt;They aggregate by &lt;em&gt;endpoint&lt;/em&gt;, but the actual fault lives at the level of &lt;em&gt;one class of SQL&lt;/em&gt;. Within a single endpoint, &lt;code&gt;select * from order where ...&lt;/code&gt; being slow says nothing about whether the &lt;code&gt;insert&lt;/code&gt; is fine.&lt;/li&gt;
&lt;li&gt;They can't catch SQL that doesn't sit on an obvious boundary (scheduled jobs, Mapper calls buried deep in a call chain).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I actually wanted was a breaker whose granularity lands exactly at &lt;strong&gt;"SQL type + SQL shape"&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When one &lt;em&gt;class&lt;/em&gt; of SQL goes bad, fast-fail only that class — leave the rest alone. And the trip has to happen &lt;strong&gt;before the request is actually sent to the DB&lt;/strong&gt;, so the connection pool stays protected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The MyBatis / MyBatis-Plus &lt;code&gt;Interceptor&lt;/code&gt; (plugin) is exactly the right seam: every CRUD statement passes through it, you can read the SQL, measure the execution time, and intercept &lt;em&gt;before&lt;/em&gt; it runs. So I built this.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. What it looks like: two steps to wire in
&lt;/h2&gt;

&lt;p&gt;The punchline first; the rest is just the "why". Integration is &lt;strong&gt;zero-touch&lt;/strong&gt; for your business code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add the dependency&lt;/strong&gt; (Spring Boot 2.x; for 3.x use &lt;code&gt;-spring-boot3-starter&lt;/code&gt;):&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;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.showingdata.starter.framework&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;sql-circuit-breaker-spring-boot-starter&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;2.1.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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Configure the YAML&lt;/strong&gt; (per SQL type; all four are required):&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;sql-circuit-breaker&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;select&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;timeout-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;          &lt;span class="c1"&gt;# SELECT timeout threshold&lt;/span&gt;
    &lt;span class="na"&gt;failure-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;       &lt;span class="c1"&gt;# consecutive timeouts that trip the breaker&lt;/span&gt;
    &lt;span class="na"&gt;circuit-open-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;     &lt;span class="c1"&gt;# how long the breaker stays open (60s)&lt;/span&gt;
    &lt;span class="na"&gt;cache-max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;      &lt;span class="c1"&gt;# max entries in the breaker-state cache&lt;/span&gt;
  &lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;timeout-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failure-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;circuit-open-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache-max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;timeout-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failure-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;circuit-open-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache-max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;timeout-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failure-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;circuit-open-ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30000&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cache-max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;5000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart and you're done — &lt;strong&gt;not a single line of business code changes&lt;/strong&gt;. From then on, when a class of SQL times out enough times in a row, it trips: while open, that class of SQL &lt;strong&gt;fast-fails locally and never reaches the DB&lt;/strong&gt;, giving the database room to breathe; it recovers automatically when the window expires.&lt;/p&gt;

&lt;p&gt;Why split SELECT and DML config? Because their risk profiles are completely different. DML holds locks and has a wide blast radius — often "one timeout should trip it". SELECT comes in many shapes and is more tolerant — you can afford "three in a row before tripping". A single one-size-fits-all threshold just doesn't make sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The design, and the parts worth talking about
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 The matching unit: SQL fingerprint, not the full SQL
&lt;/h3&gt;

&lt;p&gt;If you key on the full SQL text, then &lt;code&gt;where user_id = 123&lt;/code&gt; and &lt;code&gt;where user_id = 456&lt;/code&gt; become two different statements with separate counters — which is wrong: they share the same shape, and they're slow because of the &lt;em&gt;query pattern&lt;/em&gt;, not the specific parameter.&lt;/p&gt;

&lt;p&gt;So the matching unit is the &lt;strong&gt;SQL fingerprint&lt;/strong&gt;: normalize the parameters away, keep the structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- raw (two calls, different params)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;456&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;-- fingerprint (identical)&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is simple: replace parameter placeholders (&lt;code&gt;?&lt;/code&gt; / &lt;code&gt;#{xxx}&lt;/code&gt;) with &lt;code&gt;?&lt;/code&gt;, collapse whitespace, lowercase, take the MD5 as the key.&lt;/p&gt;

&lt;p&gt;That way &lt;strong&gt;one trip protects the entire class of SQL&lt;/strong&gt;, instead of the count getting "diluted" across parameter values and never reaching the threshold.&lt;/p&gt;

&lt;p&gt;The final breaker key is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;datasource_id : sql_type : fingerprint_md5
e.g.  default:SELECT:a3f2c1...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The datasource prefix is for multi-datasource setups (more on that later), the middle is the SQL type, the tail is the fingerprint MD5 (to avoid absurdly long keys).&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 State machine: two states, no half-open
&lt;/h3&gt;

&lt;p&gt;Many circuit breakers are three-state: CLOSED → OPEN → HALF_OPEN (probing). I &lt;strong&gt;deliberately kept it to two&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            consecutive timeouts &amp;gt;= failureThreshold
  CLOSED ──────────────────────────────────────────→ OPEN
    ↑                                                  │
    └──────── auto-reset when circuitOpenMs elapses ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why drop half-open?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Half-open means managing "probe permits" (let a few requests through to test the waters) and reconciling those permits under concurrency — non-trivial complexity.&lt;/li&gt;
&lt;li&gt;And the SQL breaker's &lt;code&gt;failure-threshold&lt;/code&gt; is tiny to begin with (default 3, DML even 1). After the open window expires, it just fully reopens; &lt;strong&gt;even if the fault isn't fixed, it re-trips within a couple of failures&lt;/strong&gt;. The cost of "re-trip fast" is entirely acceptable here.&lt;/li&gt;
&lt;li&gt;Two states are simple to reason about and easy on ops: "either it's open or it isn't", no mysterious middle state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One engineering detail: while OPEN, requests that were &lt;em&gt;already in flight before the trip&lt;/em&gt; and only time out late do &lt;strong&gt;not&lt;/strong&gt; add to the count or refresh the window — so the open window stays exactly &lt;code&gt;circuit-open-ms&lt;/code&gt; and never gets silently extended by stragglers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 A misconfiguration I designed away: cache "expire-after-access" isn't configurable
&lt;/h3&gt;

&lt;p&gt;Each SQL fingerprint's breaker state lives in a Guava Cache (four independent caches, one per SQL type). Each cache has two eviction policies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LRU size cap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cache-max-size&lt;/code&gt; (per type)&lt;/td&gt;
&lt;td&gt;Hard memory ceiling, prevents unbounded growth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expire-after-access&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;derived from &lt;code&gt;circuit-open-ms&lt;/code&gt;&lt;/strong&gt; (20× and at least 5 min), &lt;em&gt;not&lt;/em&gt; configurable&lt;/td&gt;
&lt;td&gt;Cleans up long-idle SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is that second row: &lt;strong&gt;I deliberately don't let you configure expire-after-access.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because it has a hard constraint with the open window: the access-expiry must be &lt;strong&gt;significantly larger than&lt;/strong&gt; &lt;code&gt;circuit-open-ms&lt;/code&gt;. Otherwise you get a subtle bug — a query trips and is sitting in OPEN, but during that window no new requests come in (it's fast-failing, and callers may have backed off), so &lt;strong&gt;its state gets evicted by access-expiry prematurely&lt;/strong&gt;; the next request finds no state, treats it as brand-new CLOSED, and lets it through… and your protection just quietly weakened.&lt;/p&gt;

&lt;p&gt;By deriving it from &lt;code&gt;circuit-open-ms&lt;/code&gt;, the "two values configured backwards" mistake becomes impossible. The memory ceiling is still enforced by &lt;code&gt;cache-max-size&lt;/code&gt;; access-expiry only handles idle cleanup. &lt;strong&gt;If a constraint can eliminate a misconfiguration, don't leave it as a config knob.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 It trips on timeouts only, not on exceptions
&lt;/h3&gt;

&lt;p&gt;This is a deliberate boundary: exceptions thrown by SQL execution — connection errors, syntax errors, constraint violations — are &lt;strong&gt;never counted&lt;/strong&gt; toward the breaker. Only "execution time exceeded the threshold" counts as a failure.&lt;/p&gt;

&lt;p&gt;Why? Because the breaker protects against one specific failure mode: &lt;em&gt;slow SQL exhausting the connection pool&lt;/em&gt;. A syntax error is a code bug; a constraint violation is a data problem. Neither holds connections hostage or drags down the DB, and counting them would only cause &lt;strong&gt;false trips&lt;/strong&gt; (a temporarily-erroring query gets tripped, which actually masks the real business problem). Single responsibility keeps it predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5 A performance detail under load: the exception doesn't fill in its stack trace
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;SqlCircuitBreakerException&lt;/code&gt; thrown on fast-fail overrides &lt;code&gt;fillInStackTrace()&lt;/code&gt; to skip stack capture entirely.&lt;/p&gt;

&lt;p&gt;Because during a high-concurrency trip, this exception may be thrown thousands of times per second, and &lt;code&gt;fillInStackTrace&lt;/code&gt; is one of the more expensive things the JVM does (it walks the entire call stack). Skipping it saves a lot of CPU/memory.&lt;/p&gt;

&lt;p&gt;But that buys you a &lt;strong&gt;gotcha you must know about&lt;/strong&gt; (next section).&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6 Heads up: you can't catch this breaker exception
&lt;/h3&gt;

&lt;p&gt;Following from the above — because the exception carries no stack trace, &lt;em&gt;and&lt;/em&gt; MyBatis &lt;strong&gt;re-wraps&lt;/strong&gt; it in a &lt;code&gt;MyBatisSystemException&lt;/code&gt; on the way out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Writing this in your Service / Controller won't catch it!&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;orderMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queryByUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&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;SqlCircuitBreakerException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// the thrown type is MyBatisSystemException; instanceof doesn't match&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right way is to catch it centrally in a global exception handler — and, importantly, &lt;strong&gt;log using the wrapper &lt;code&gt;MyBatisSystemException&lt;/code&gt;&lt;/strong&gt;, because &lt;em&gt;its&lt;/em&gt; stack trace contains the full business call chain (Controller → Service → Mapper):&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;@RestControllerAdvice&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;GlobalExceptionHandler&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;MyBatisSystemException&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;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MyBatisSystemException&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;SqlCircuitBreakerException&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findCircuitBreaker&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="c1"&gt;// walk the cause chain&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;cb&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="c1"&gt;// key point: log with the wrapper `ex` — it has the business line numbers&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[SqlCircuitBreaker] fast-fail | key={} | business stack below"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCircuitKey&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="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;503&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="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way you get both the "no stack fill" performance &lt;em&gt;and&lt;/em&gt; the ability to pinpoint which Service/Controller triggered it. Best of both worlds — as long as you know the mechanism exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7 Config priority: from one-size-fits-all to fine-grained
&lt;/h3&gt;

&lt;p&gt;Four layers of config, higher overrides lower:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ThreadLocal (programmatic)  &amp;gt;  method annotation  &amp;gt;  interface annotation  &amp;gt;  global YAML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global YAML&lt;/strong&gt; is the only place you can configure &lt;em&gt;per SQL type&lt;/em&gt; (fine-grained).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotations / ThreadLocal&lt;/strong&gt; are coarse overrides — they apply uniformly to all SQL types under the annotated Mapper / method.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical usage:&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;@SqlCircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;// interface level: 5s for this whole Mapper&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;OrderMapper&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseMapper&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@SqlCircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;circuitOpenMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// method-level override&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;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;complexQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QueryParam&lt;/span&gt; &lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@SqlCircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;disableCircuitBreaker&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="c1"&gt;// admin query, skip the breaker&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;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;adminQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AdminParam&lt;/span&gt; &lt;span class="n"&gt;param&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;ThreadLocal fits the "loosen/disable just for this request" case — e.g. a scheduled data-repair job that you &lt;em&gt;know&lt;/em&gt; will be slow but don't want to trip the breaker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SqlCircuitBreakerContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disableCircuitBreaker&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;orderMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;batchFixData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SqlCircuitBreakerContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// must clear, or thread-pool reuse pollutes the next request&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A trade-off here: the interceptor &lt;strong&gt;does not&lt;/strong&gt; auto-clear. We want a Service to set it once and have it apply across the several Mapper calls it makes; if the interceptor cleared after the first SQL, it'd be lost from the second onward. The cost is putting &lt;code&gt;clear()&lt;/code&gt; on the caller (in a &lt;code&gt;finally&lt;/code&gt;), in exchange for "set once, applies to the whole block" semantics.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Production essential: observability
&lt;/h2&gt;

&lt;p&gt;The scariest thing about a circuit breaker is "it's quietly working, but you don't know". With &lt;code&gt;spring-boot-actuator&lt;/code&gt; on the classpath, the SDK auto-exposes 5 Micrometer metrics with &lt;strong&gt;zero extra config&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql.circuit.breaker.intercept.total&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;total SQL intercepted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql.circuit.breaker.timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql.circuit.breaker.open&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;trips (CLOSED→OPEN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql.circuit.breaker.fast.fail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;fast-fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sql.circuit.breaker.open.count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;breakers currently OPEN (real-time)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most useful one is that last Gauge. A single alert rule is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# any breaker currently open → alert immediately; back to zero means everything auto-recovered
sql_circuit_breaker_open_count &amp;gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a &lt;strong&gt;trap that only shows up at scale&lt;/strong&gt;: &lt;code&gt;timeout&lt;/code&gt; / &lt;code&gt;open&lt;/code&gt; / &lt;code&gt;fast.fail&lt;/code&gt; carry a &lt;code&gt;mapper_id&lt;/code&gt; label by default (handy for pinpointing a specific Mapper). But that label explodes your time series — per service it's roughly &lt;code&gt;(# Mapper methods) × 4 (types) × 3 (metrics)&lt;/code&gt;. With a few hundred Mappers across multiple replicas, Prometheus chokes (and series-billed backends cost you real money).&lt;/p&gt;

&lt;p&gt;So there's a switch to drop the &lt;code&gt;mapper_id&lt;/code&gt; label, collapsing the series count from N×12 to a fixed 12; you then locate the specific Mapper via logs instead:&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;sql-circuit-breaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;include-mapper-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="c1"&gt;# turn off when you're at scale or sensitive to series cardinality&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;When you build infrastructure-type components, you have to think ahead to "what happens once this is at scale". Labels-on-by-default makes the out-of-the-box experience nice, but you must give people an escape hatch for when it grows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Other "production-grade" details I bolted on
&lt;/h2&gt;

&lt;p&gt;A few, all forged by production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Notifications fire once&lt;/strong&gt;: implement the &lt;code&gt;MessageCenterClient&lt;/code&gt; interface to push breaker events to Slack / Teams / whatever. But notifications fire &lt;strong&gt;only when the breaker first opens&lt;/strong&gt;, not on the fast-fail path — otherwise a high-concurrency trip would spam thousands of messages a second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-datasource isolation&lt;/strong&gt;: the breaker key includes a datasource identifier, so a slow query in DB-A won't trip DB-B. With a runtime-routing framework, implement a &lt;code&gt;DataSourceKeyResolver&lt;/code&gt; that returns the current datasource key; single-datasource needs zero config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; misclassification&lt;/strong&gt;: MyBatis types SQL by its XML tag, so &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; is treated as a SELECT (the loose threshold) — even though it holds locks and behaves more like DML. Tighten such methods with a dedicated annotation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config validated at startup&lt;/strong&gt;: all four SQL-type blocks are required and illegal values (e.g. &lt;code&gt;timeout-ms &amp;lt;= 0&lt;/code&gt;) fail the boot — surfacing errors at startup rather than when some query hits it in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unified log prefix &lt;code&gt;[SqlCircuitBreaker]&lt;/code&gt;&lt;/strong&gt;: easy to filter in ELK, with a ready-made example for a dedicated logback appender (don't forget &lt;code&gt;additivity="false"&lt;/code&gt;, or you haven't actually isolated anything).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Its boundaries: know what it is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Being honest about boundaries matters more than hyping features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's an "after-the-fact, statistical" breaker — it does not interrupt in-flight SQL.&lt;/strong&gt; A query already sent to the DB and running won't be killed by the breaker (that's the job of JDBC / driver / pool timeouts). The breaker stops the &lt;em&gt;subsequent&lt;/em&gt; requests of the same class from piling on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State lives in each instance's memory, not shared across instances.&lt;/strong&gt; In a multi-replica deployment each counts on its own, so treat the thresholds as "per-instance". Under uneven traffic you can lower them so a single instance converges faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeouts only, not other exceptions&lt;/strong&gt; (covered above — deliberate).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These aren't shortcomings, they're &lt;strong&gt;explicit design boundaries&lt;/strong&gt; — a component that's clear about what it &lt;em&gt;won't&lt;/em&gt; do is one that won't get misused under the wrong expectations.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Wrapping up
&lt;/h2&gt;

&lt;p&gt;Back to the original question: a single slow query shouldn't have the power to take down an entire service.&lt;/p&gt;

&lt;p&gt;The idea behind this SDK is actually plain — &lt;strong&gt;push circuit breaking down to the MyBatis interceptor layer, use the SQL fingerprint as the matching unit, configure per SQL type, and fast-fail before the request ever reaches the DB&lt;/strong&gt;. It fills exactly the "SQL layer" that Resilience4j / Hystrix-style frameworks can't reach.&lt;/p&gt;

&lt;p&gt;But what really decides whether a component survives in production is rarely the core algorithm — it's the small trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;why two states and not half-open;&lt;/li&gt;
&lt;li&gt;why access-expiry is derived instead of configurable;&lt;/li&gt;
&lt;li&gt;why it trips on timeouts only, not exceptions;&lt;/li&gt;
&lt;li&gt;why the exception skips its stack trace, and the "can't catch it" gotcha that follows;&lt;/li&gt;
&lt;li&gt;why metrics need an escape hatch to drop a label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A good piece of infrastructure bakes the "traps you've hit" and the "boundaries you've thought through" into its defaults and constraints, so its users hit fewer of them.&lt;/strong&gt; Hope this is useful next time you're building something similar.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;The SDK is open source and on Maven Central, with both Spring Boot 2.x and 3.x support, running in production across several systems. One dependency + a bit of YAML to integrate. Try it, break it, file issues.&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;Repo&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/showingdata/sql-circuit-breaker" rel="noopener noreferrer"&gt;https://github.com/showingdata/sql-circuit-breaker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gitee: &lt;a href="https://gitee.com/LanyXP/sql-circuit-breaker" rel="noopener noreferrer"&gt;https://gitee.com/LanyXP/sql-circuit-breaker&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(If this helped, a Star ⭐ helps more devs who've been burned by slow SQL find it.)&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>springboot</category>
      <category>database</category>
      <category>java</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
