<?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: Thanawat Sritho</title>
    <description>The latest articles on DEV Community by Thanawat Sritho (@thestrawberry09).</description>
    <link>https://dev.to/thestrawberry09</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%2F4207915%2F008e540f-17db-4d03-a462-5e5b7f3679b2.png</url>
      <title>DEV Community: Thanawat Sritho</title>
      <link>https://dev.to/thestrawberry09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thestrawberry09"/>
    <language>en</language>
    <item>
      <title>I'm building a flaky-test finder with Spring Boot, Go and SQS (v0.1.0)</title>
      <dc:creator>Thanawat Sritho</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:02:04 +0000</pubDate>
      <link>https://dev.to/thestrawberry09/im-building-a-flaky-test-finder-with-spring-boot-go-and-sqs-v010-4a2</link>
      <guid>https://dev.to/thestrawberry09/im-building-a-flaky-test-finder-with-spring-boot-go-and-sqs-v010-4a2</guid>
      <description>&lt;p&gt;A flaky test passes and fails on the same code. Nothing changed, but the build is red. You click "re-run", it goes green, and you move on.&lt;/p&gt;

&lt;p&gt;I read about a dev team whose tests kept failing on the same commit. They spent a lot of time re-running and investigating why it happened.&lt;/p&gt;

&lt;p&gt;So I started &lt;strong&gt;FlakeSince&lt;/strong&gt;, a free, self-hosted tool that finds flaky tests in CI. Most free tools tell you a test is flaky &lt;em&gt;today&lt;/em&gt;. FlakeSince keeps the pass/fail history of every normal CI run, so it can also estimate &lt;strong&gt;since which commit&lt;/strong&gt; a test became flaky, without re-running anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi9ripmmzt3ecp8s21e39.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi9ripmmzt3ecp8s21e39.png" alt="FlakeSince architecture" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;GitHub Action&lt;/strong&gt; uploads the JUnit XML files from your test run (Gradle, Maven Surefire, pytest or go-junit-report).&lt;/li&gt;
&lt;li&gt;A small &lt;strong&gt;Go ingest service&lt;/strong&gt; checks the API key, parses the XML and puts one message per CI run on a queue. It answers &lt;code&gt;202 Accepted&lt;/code&gt; as soon as the run is safely queued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SQS&lt;/strong&gt; holds the message. Locally I use &lt;a href="https://github.com/softwaremill/elasticmq" rel="noopener noreferrer"&gt;ElasticMQ&lt;/a&gt;, which speaks the SQS protocol, so you don't need an AWS account to develop.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Spring Boot API&lt;/strong&gt; (Java 25) reads the queue and saves each run to &lt;strong&gt;PostgreSQL&lt;/strong&gt;. If saving fails 3 times, the message goes to a dead-letter queue instead of being lost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using it in a workflow looks like this:&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="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;Upload test results to FlakeSince&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;   &lt;span class="c1"&gt;# upload even when tests fail&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;TheStrawberry-09/flakesince/action@v0&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;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FLAKESINCE_URL }}&lt;/span&gt;
    &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.FLAKESINCE_API_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**/build/test-results/**/*.xml'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed upload only adds a warning, so FlakeSince never turns your CI red by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: why a separate Go service in front of Spring Boot?
&lt;/h2&gt;

&lt;p&gt;The simplest design is one Spring Boot app that receives uploads and saves them. I didn't do that, because of two facts about CI uploads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They arrive in &lt;strong&gt;bursts&lt;/strong&gt;. Many repositories finish CI at the same moment.&lt;/li&gt;
&lt;li&gt;They &lt;strong&gt;must not be lost&lt;/strong&gt;, even when the main API is busy, restarting or down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Spring receives uploads directly, every API restart means lost test results. So I put a small service and a queue in front of it. Uploads keep being accepted even when the API is down, and the API catches up when it comes back.&lt;/p&gt;

&lt;p&gt;I considered writing that front service in Java too, which would mean one language in the repo. I chose Go because the service is small and does one job: it starts in milliseconds, uses little memory, builds to a single binary, and the standard library already has a production-ready HTTP server and XML parser.&lt;/p&gt;

&lt;p&gt;The cost is real: two languages, two CI workflows, and a message format (&lt;code&gt;RunMessage&lt;/code&gt; in Go, &lt;code&gt;RunUploadRequest&lt;/code&gt; in Java) that must stay in sync. I wrote this down as an architecture decision record (&lt;a href="https://github.com/TheStrawberry-09/flakesince/tree/main/docs/decisions" rel="noopener noreferrer"&gt;ADR 001&lt;/a&gt;) so future me remembers why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Who checks the API key?
&lt;/h3&gt;

&lt;p&gt;This led to a second question. The API key hashes live in a table that Spring owns. I had three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go asks Spring&lt;/strong&gt; to check the key. But if Spring is down, uploads fail, and the queue becomes pointless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring checks the key later&lt;/strong&gt;, when it reads the queue. But then anyone can fill the queue, and a wrong key still gets &lt;code&gt;202&lt;/code&gt;, so users never find out their setup is broken.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go reads the key table directly.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I picked the third. Go looks up the SHA-256 hash of the key with its own read-only database role, &lt;code&gt;ingest_reader&lt;/code&gt;, which can read only three columns. If that password leaked, nothing else would be exposed. Valid keys are cached for 60 seconds, so a revoked key can keep working for up to a minute. That's a trade-off I accepted and wrote down in &lt;a href="https://github.com/TheStrawberry-09/flakesince/tree/main/docs/decisions" rel="noopener noreferrer"&gt;ADR 002&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: saving each run exactly once
&lt;/h2&gt;

&lt;p&gt;SQS guarantees &lt;em&gt;at-least-once&lt;/em&gt; delivery: the same message can arrive twice. If the API saved it twice, one CI run would count as two, and the flaky statistics would be wrong.&lt;/p&gt;

&lt;p&gt;The fix has two layers. First, the database refuses duplicates:&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="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the ingest code checks before saving and treats a duplicate as a success, not an error:&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;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CiRun&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByProjectIdAndProviderRunIdAndRunAttempt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;providerRunId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runAttempt&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;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isPresent&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;IngestResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getId&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="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// duplicate: already saved&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Including &lt;code&gt;run_attempt&lt;/code&gt; matters. When you click "re-run" in GitHub Actions, the run ID stays the same but the attempt number goes up. Those re-runs are exactly the data a flaky-test tool needs, so they must be saved as separate attempts, not thrown away as duplicates.&lt;/p&gt;

&lt;p&gt;The queue listener deletes a message only after it is saved. If anything throws, the message stays in SQS and is retried, and after 3 tries it moves to the dead-letter queue:&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;IngestResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ingestService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ingest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queueUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queueUrl&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;receiptHandle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;receiptHandle&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener runs on a Java 25 virtual thread with SQS long polling, so it waits cheaply when the queue is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  It runs
&lt;/h2&gt;

&lt;p&gt;Here's the action uploading results from my sample repo, &lt;a href="https://github.com/TheStrawberry-09/flaky-demo" rel="noopener noreferrer"&gt;flaky-demo&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnwm1owwot8smpp2uawu.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnwm1owwot8smpp2uawu.png" alt="GitHub Action upload accepted" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;v0.1.0 collects the history. The next steps are the interesting part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flaky test detection&lt;/strong&gt;: a test that both passed and failed on the same commit is flaky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Flaky since commit X"&lt;/strong&gt;: using the history to estimate when the flakiness started.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;dashboard&lt;/strong&gt; and a hosted demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is Apache-2.0 on GitHub: &lt;strong&gt;&lt;a href="https://github.com/TheStrawberry-09/flakesince" rel="noopener noreferrer"&gt;github.com/TheStrawberry-09/flakesince&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is my first time making an open-source project, so a lot of things are still new to me. I used AI to help me find information, draft code and check bugs in logs.&lt;br&gt;
Feel free to give feedback! I’ll keep updating this project, and I’ll add a mascot too.&lt;br&gt;
Hope you enjoy it. Thank you!&lt;/p&gt;

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