<?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: Fred Feng</title>
    <description>The latest articles on DEV Community by Fred Feng (@paganini2008).</description>
    <link>https://dev.to/paganini2008</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%2F682844%2F5db059fa-8625-4f85-9d3c-a35b8cb93013.jpeg</url>
      <title>DEV Community: Fred Feng</title>
      <link>https://dev.to/paganini2008</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paganini2008"/>
    <language>en</language>
    <item>
      <title>SqlJam: Move Tables Across Databases with Schema, Data and All</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:23:54 +0000</pubDate>
      <link>https://dev.to/paganini2008/sqljam-move-tables-across-databases-with-schema-data-and-all-4n87</link>
      <guid>https://dev.to/paganini2008/sqljam-move-tables-across-databases-with-schema-data-and-all-4n87</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tables, indexes, constraints, sequences, partitions and rows, in one go.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;strong&gt;SqlJam&lt;/strong&gt;: &lt;em&gt;Move tables across databases. Schema, data and all.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Java 17 + JavaFX desktop tool that copies tables between &lt;strong&gt;MySQL, PostgreSQL, Oracle, SQL Server, H2, SQLite&lt;/strong&gt; and more, in any direction:&lt;br&gt;
&lt;strong&gt;import directly&lt;/strong&gt; into a target database, or save a self-describing &lt;strong&gt;SQL export package&lt;/strong&gt; and import it later.&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%2Fyg8idm2oshhxk4bg1018.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%2Fyg8idm2oshhxk4bg1018.png" alt="SqlJam main window" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;The hard part of a cross-database migration is not &lt;code&gt;INSERT&lt;/code&gt;, it is the details: type names, identity columns, default value syntax, &lt;code&gt;bigint unsigned&lt;/code&gt; overflow, timestamps shifted by time zones, BLOBs inflating scripts, and old servers rejecting new syntax. Hand-written scripts are slow and miss things.&lt;/p&gt;

&lt;p&gt;SqlJam reads &lt;strong&gt;one metadata tree&lt;/strong&gt; and generates SQL with a &lt;strong&gt;dialect chosen by target type and version&lt;/strong&gt;. Export packages carry a &lt;code&gt;manifest.json&lt;/code&gt;, so an import knows the target, checks completeness and verifies every file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pain&lt;/th&gt;
&lt;th&gt;SqlJam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dialect differences&lt;/td&gt;
&lt;td&gt;Types, identifiers, defaults, identity and literals translated by the dialect of each target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old versions&lt;/td&gt;
&lt;td&gt;Version subclasses: &lt;code&gt;Oracle11gDialect&lt;/code&gt;, &lt;code&gt;PostgreSQL9Dialect&lt;/code&gt;, &lt;code&gt;MySQL56Dialect&lt;/code&gt;, &lt;code&gt;SQLServer2008Dialect&lt;/code&gt; …&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing keys &amp;amp; indexes&lt;/td&gt;
&lt;td&gt;Primary keys, unique/regular indexes, foreign keys, comments, sequences, partitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huge scripts&lt;/td&gt;
&lt;td&gt;Data files split at 10 MB: &lt;code&gt;orders.sql&lt;/code&gt;, &lt;code&gt;orders_2.sql&lt;/code&gt; …&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOBs&lt;/td&gt;
&lt;td&gt;Stored as files, restored by primary key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripts without context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;manifest.json&lt;/code&gt;: source, target, options, SHA-256 per file, row counts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  3. Quick Start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:paganini2008/sqljam.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;sqljam
./mvnw &lt;span class="nt"&gt;-DskipTests&lt;/span&gt; package       &lt;span class="c"&gt;# Windows: mvnw.cmd, output goes to bin/&lt;/span&gt;
bin/sqljam.sh                    &lt;span class="c"&gt;# Windows: bin\sqljam.bat, or double click the jar of your platform&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/
├── sqljam-1.0.0-SNAPSHOT-win.jar           # runnable jar of each platform,
├── sqljam-1.0.0-SNAPSHOT-mac-aarch64.jar   # JavaFX and all JDBC drivers inside
├── sqljam-1.0.0-SNAPSHOT-mac.jar
├── sqljam-1.0.0-SNAPSHOT-linux.jar
├── sqljam.sh / sqljam.bat                  # launchers, pick the jar of the platform
├── sqljam.properties                       # configuration, edit and restart
├── sqljam.vmoptions                        # JVM options, one per line
└── sqljam.png                              # Dock icon of macOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;sqljam.properties&lt;/code&gt; and &lt;code&gt;sqljam.vmoptions&lt;/code&gt; sit next to the runnable jar. Edit them and restart, no rebuild needed.&lt;/p&gt;

&lt;p&gt;A splash screen shows the loading of the configuration, the data sources and the JDBC drivers:&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%2Fraw.githubusercontent.com%2Fpaganini2008%2Fsqljam%2Fmaster%2Fdocs%2Fassets%2Fsplash.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%2Fraw.githubusercontent.com%2Fpaganini2008%2Fsqljam%2Fmaster%2Fdocs%2Fassets%2Fsplash.png" alt="SqlJam splash screen" width="560" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1. Log in to a data source&lt;/th&gt;
&lt;th&gt;2. Export wizard&lt;/th&gt;
&lt;th&gt;3. Progress&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2F7pxkdi820hto7ke9lfub.png" alt="login" width="800" height="577"&gt;&lt;/td&gt;
&lt;td&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%2Ff6vw3uyycoro94016rz0.png" alt="export" width="600" height="753"&gt;&lt;/td&gt;
&lt;td&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%2Fbcime3fknu4wktcjz5os.png" alt="progress" width="760" height="560"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  4. Requirements
&lt;/h2&gt;

&lt;p&gt;Only &lt;strong&gt;Java 17+&lt;/strong&gt; is needed to run SqlJam. JavaFX and the JDBC drivers are inside the runnable jar, and building needs no Maven installation thanks to the Maven Wrapper (&lt;code&gt;./mvnw&lt;/code&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Runnable jar&lt;/th&gt;
&lt;th&gt;Launcher&lt;/th&gt;
&lt;th&gt;Supported&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows 10 / 11 (x64)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam-&amp;lt;version&amp;gt;-win.jar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam.bat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Apple Silicon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam-&amp;lt;version&amp;gt;-mac-aarch64.jar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Intel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam-&amp;lt;version&amp;gt;-mac.jar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux x64 (GTK 3)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam-&amp;lt;version&amp;gt;-linux.jar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqljam.sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;5.5 to 9.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;9.x to 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;11g to 23ai&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;2008 to 2022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2 / SQLite&lt;/td&gt;
&lt;td&gt;2.x / 3.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All JDBC drivers are bundled in the fat jar.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. 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%2F19dzc0dq8pktkfhdcgzf.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%2F19dzc0dq8pktkfhdcgzf.png" alt="How it works" width="800" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    S[(Source DB)] --&amp;gt; T[Metadata tree]
    T --&amp;gt; D[Target dialect&amp;lt;br/&amp;gt;type + version subclass]
    D --&amp;gt; I[Import directly] --&amp;gt; TD[(Target DB)]
    D --&amp;gt; E[Export package&amp;lt;br/&amp;gt;schema.sql · data*.sql · lob/ · manifest.json]
    E --&amp;gt; SI[ScriptImporter] --&amp;gt; TD&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read metadata once&lt;/strong&gt;: &lt;code&gt;XxxMetaDataOperations&lt;/code&gt; completes comments, identity, generated columns, partitions and sequences per database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate for the target&lt;/strong&gt;: &lt;code&gt;DbType.createDialect(major, minor)&lt;/code&gt; picks the version subclass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stream rows&lt;/strong&gt;: count rows (percentage progress) → page by primary key → normalize values → batch insert or SQL literals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Code Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example 1: Export as a PostgreSQL package
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: MySQL database &lt;code&gt;shop&lt;/code&gt;&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;ScriptExporter&lt;/span&gt; &lt;span class="n"&gt;exporter&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;ScriptExporter&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;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;DataFileStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FILE_PER_TABLE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ExportConfiguration&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfiguration&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;setDbType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MYSQL&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;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MYSQL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3306&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"shop"&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;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"root"&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;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"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;setIdReused&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="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTargetDbType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POSTGRESQL&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportDdlAndData&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&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;export/
├── schema.sql   ├── data/orders.sql   ├── data/orders_2.sql
├── lob/         ├── lob-manifest.json ├── constraints.sql
└── manifest.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 2: Oracle → SQL Server directly (schema created)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ImportExporter&lt;/span&gt; &lt;span class="n"&gt;importer&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;ImportExporter&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;Exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ExportConfiguration&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getExportConfiguration&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDbType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ORACLE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ORACLE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ora-host"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1521&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ORCL"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hr"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secret"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIncludedSchemaNames&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;String&lt;/span&gt;&lt;span class="o"&gt;[]{&lt;/span&gt;&lt;span class="s"&gt;"HR"&lt;/span&gt;&lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="nc"&gt;ImportExportHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ImportConfiguration&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getImportConfiguration&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDbType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SQLSERVER&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DbType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SQLSERVER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mssql-host"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1433&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"demo"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sa"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"secret"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTargetSchemaName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hr"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;importer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exportDdlAndData&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;: tables, keys, indexes, foreign keys, comments and sequences in &lt;code&gt;demo.hr&lt;/code&gt;. Identities continue from the imported max value.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source column&lt;/th&gt;
&lt;th&gt;→ PostgreSQL&lt;/th&gt;
&lt;th&gt;→ Oracle&lt;/th&gt;
&lt;th&gt;→ SQL Server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL &lt;code&gt;bigint unsigned&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;numeric(20, 0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NUMBER(20,0)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;decimal(20,0)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL &lt;code&gt;jsonb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLOB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nvarchar(max)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server &lt;code&gt;datetime2(7)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TIMESTAMP(7)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Configuration
&lt;/h2&gt;

&lt;p&gt;All settings live in &lt;code&gt;bin/sqljam.properties&lt;/code&gt; next to the runnable jar. Settings changed in the UI are saved to &lt;code&gt;~/.sqljam/sqljam.properties&lt;/code&gt; and win over the file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.export.max-file-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10485760&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Max bytes of a data file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.export.data-file-strategy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SINGLE_FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single file or one file per table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.export.lob-separated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Write LOBs as files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.export.page-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rows per page, tuned by benchmark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.export.lob-page-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rows per page for tables with LOB columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.banner.mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Startup banner with the version: console, log or off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.import.batch-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;INSERT statements per batch when importing a package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.ui.theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Primer Dark&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7 themes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqljam.pool.maximum-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connection pool size&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  8. Performance
&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%2F09hw7x6rgzc72akkxqf2.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%2F09hw7x6rgzc72akkxqf2.png" alt="Benchmark" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario (200,000 rows × 6 columns)&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Rows/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;H2 → Oracle 23ai&lt;/td&gt;
&lt;td&gt;2.1 s&lt;/td&gt;
&lt;td&gt;95.3k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2 → export package (32 MB, 4 files)&lt;/td&gt;
&lt;td&gt;2.9 s&lt;/td&gt;
&lt;td&gt;69.4k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2 → PostgreSQL 16&lt;/td&gt;
&lt;td&gt;3.2 s&lt;/td&gt;
&lt;td&gt;62.4k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2 → MySQL 9.6&lt;/td&gt;
&lt;td&gt;3.3 s&lt;/td&gt;
&lt;td&gt;60.7k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H2 → SQL Server 2022&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;td&gt;40.1k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export package → PostgreSQL&lt;/td&gt;
&lt;td&gt;4.5 s&lt;/td&gt;
&lt;td&gt;44.5k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Environment: Apple M2 Max, 32 GB, JDK 17. MySQL/PostgreSQL local, Oracle/SQL Server in Docker. Default settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quality&lt;/strong&gt;: 303 tests (full cross-database import matrix, export package round trips, old-version SQL executed on real servers, UI and boundary tests), 91% line coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Design Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table-level focus&lt;/strong&gt;: tables, keys, indexes, constraints, sequences, partitions and comments: the objects data migration depends on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows paged by primary key&lt;/strong&gt;: stable, deterministic reads on every database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tables copied one by one&lt;/strong&gt;: predictable load on production servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain SQL in packages&lt;/strong&gt;: readable, editable and portable. Direct import uses batched inserts for speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-specific SQL&lt;/strong&gt;: older servers get the syntax they understand instead of a lowest common denominator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Any-to-any copies. Every cross-database pair passes automated tests.&lt;/li&gt;
&lt;li&gt;Dialects are chosen by &lt;strong&gt;type + version&lt;/strong&gt;. Old versions are subclasses.&lt;/li&gt;
&lt;li&gt;Tables, indexes, constraints, sequences, partitions and comments move together.&lt;/li&gt;
&lt;li&gt;Export package = SQL + LOB files + &lt;code&gt;manifest.json&lt;/code&gt;. Export and import are paired.&lt;/li&gt;
&lt;li&gt;Data files split at 10 MB by default. LOBs stored separately.&lt;/li&gt;
&lt;li&gt;Timestamps, unsigned numbers, bits, money, UUID and JSON survive the trip.&lt;/li&gt;
&lt;li&gt;Pooled connections and batch inserts: 40k to 95k rows/s for direct imports.&lt;/li&gt;
&lt;li&gt;A mainstream dark UI with 7 themes, percentage progress and cancel.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>database</category>
      <category>java</category>
      <category>sql</category>
      <category>tools</category>
    </item>
    <item>
      <title>Vortex TSDB: Live Metrics in One Command — HTTP Writes, Minute Aggregates, Sliding Windows, Replicated in Memory</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:22:32 +0000</pubDate>
      <link>https://dev.to/paganini2008/vortex-tsdb-live-metrics-in-one-command-http-writes-minute-aggregates-sliding-windows-3kf</link>
      <guid>https://dev.to/paganini2008/vortex-tsdb-live-metrics-in-one-command-http-writes-minute-aggregates-sliding-windows-3kf</guid>
      <description>&lt;h2&gt;
  
  
  1. Overview
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lightweight. Replicated. Real-time.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Vortex TSDB&lt;/strong&gt; is a distributed time series database for live metrics. Push numbers over HTTP;&lt;br&gt;
read per-minute aggregates, instant values and tumbling or sliding windows. Every node holds the&lt;br&gt;
whole dataset in memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Images (amd64 · arm64)&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;License&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1.0.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://hub.docker.com/r/fredfeng033/vortex-tsdb" rel="noopener noreferrer"&gt;&lt;code&gt;fredfeng033/vortex-tsdb&lt;/code&gt;&lt;/a&gt; · &lt;code&gt;ghcr.io/paganini2008/vortex-tsdb&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/paganini2008/vortex" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Flehd7ow3xlnxcuh9ftl6.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%2Flehd7ow3xlnxcuh9ftl6.png" alt="Vortex TSDB web console" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  2. What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;Live metrics — devices online, API QPS, orders, temperatures — need exact counts from many&lt;br&gt;
writers, aggregation by the minute and over any window, and a service that survives a dead node.&lt;br&gt;
General-purpose TSDBs do this with a disk engine, a query language and several components; Vortex&lt;br&gt;
does only this layer, in memory, behind plain HTTP.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pain&lt;/th&gt;
&lt;th&gt;Vortex&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Many components to deploy and run&lt;/td&gt;
&lt;td&gt;One service + optional Redis; &lt;code&gt;docker compose up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counts drift under concurrent writers&lt;/td&gt;
&lt;td&gt;Writes serialised on the leader, then replicated: exact, no locks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows need tables or a query language&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;range&lt;/code&gt; · &lt;code&gt;step&lt;/code&gt; · &lt;code&gt;window&lt;/code&gt; · &lt;code&gt;z&lt;/code&gt;, folded at read time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One node down, service down&lt;/td&gt;
&lt;td&gt;Full replica on every node; new leader in ~5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory grows with every series&lt;/td&gt;
&lt;td&gt;Per-node key limit; overflow to Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  3. Quick Start
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Free on Docker Hub and GHCR&lt;/strong&gt; · &lt;code&gt;linux/amd64&lt;/code&gt; + &lt;code&gt;linux/arm64&lt;/code&gt; · no JDK, Node or build needed&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/paganini2008/vortex/main/deploy/docker-compose.yml
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;                     &lt;span class="c"&gt;# 3 nodes + web console + gateway&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://localhost:9080/tsd/push?t=long&amp;amp;c=car&amp;amp;d=speed&amp;amp;v=44'&lt;/span&gt;
curl &lt;span class="s1"&gt;'http://localhost:9080/tsd/last?t=long&amp;amp;c=car&amp;amp;d=speed'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1791182928819&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"elapsed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"requestPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/tsd/last"&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;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Open&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web console&lt;/td&gt;
&lt;td&gt;&lt;a href="http://localhost:9080/" rel="noopener noreferrer"&gt;http://localhost:9080/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swagger UI&lt;/td&gt;
&lt;td&gt;&lt;a href="http://localhost:9080/swagger-ui.html" rel="noopener noreferrer"&gt;http://localhost:9080/swagger-ui.html&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Also&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pull&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pull fredfeng033/vortex-tsdb:latest&lt;/code&gt; · &lt;code&gt;docker pull fredfeng033/vortex-tsdb-web:latest&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GHCR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker pull ghcr.io/paganini2008/vortex-tsdb:latest&lt;/code&gt; · &lt;code&gt;VORTEX_REGISTRY=ghcr.io/paganini2008 docker compose up -d&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your Redis&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VORTEX_REDIS_HOST=host.docker.internal VORTEX_REDIS_PASSWORD=secret docker compose up -d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;From source&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git clone https://github.com/paganini2008/vortex.git &amp;amp;&amp;amp; cd vortex &amp;amp;&amp;amp; ./run-docker.sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Nodes take 20–30 s to start; wait for &lt;code&gt;(healthy)&lt;/code&gt; in &lt;code&gt;docker compose ps&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;To&lt;/th&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run the images&lt;/td&gt;
&lt;td&gt;Docker with compose v2 (Docker Desktop, Rancher Desktop, OrbStack, Engine 23+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overflow (optional)&lt;/td&gt;
&lt;td&gt;Redis — tested with 7.4 and 8.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build from source&lt;/td&gt;
&lt;td&gt;JDK 17+, Maven 3.9, Node.js 20+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stack&lt;/td&gt;
&lt;td&gt;Spring Boot 4.1 · openspreader · Next.js 16 · React 19 · Tailwind 4 · Traefik v3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. 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%2Fpf63wh1r83f9pr0rek8k.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%2Fpf63wh1r83f9pr0rek8k.png" alt="Architecture" width="799" height="361"&gt;&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%2Fyww72qlzcr0xx8fc8xpn.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%2Fyww72qlzcr0xx8fc8xpn.png" alt="One write, one read" width="800" height="268"&gt;&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%2Flepa77w8dt4yo6hnmct9.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%2Flepa77w8dt4yo6hnmct9.png" alt="Tumbling and sliding windows" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Structure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minute bucket&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsd:&amp;lt;type&amp;gt;:&amp;lt;category&amp;gt;:&amp;lt;dimension&amp;gt;:&amp;lt;minute&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;max · min · sum · count; retention as TTL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsd:last:&amp;lt;category&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash: a category in one read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catalog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsd:catalog&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sorted set by last sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Health&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tsd:health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hash; each node every 5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  6. Code Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Example 1 — push and read back
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;for v in 44 67 112; do curl -X POST "localhost:9080/tsd/push?t=long&amp;amp;c=car&amp;amp;d=speed&amp;amp;v=$v"; done&lt;/code&gt; then &lt;code&gt;curl 'localhost:9080/tsd/retrieve?t=long&amp;amp;c=car&amp;amp;d=speed&amp;amp;z=Asia/Shanghai'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three samples land in one minute bucket; &lt;code&gt;retrieve&lt;/code&gt; returns the latest 60 buckets in zone &lt;code&gt;z&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"09:29:00": {"count": 3, "highestValue": 112, "lowestValue": 44, "totalValue": 223, "averageValue": 74.3333}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Example 2 — a sliding window
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl 'localhost:9080/tsd/query?t=long&amp;amp;c=car&amp;amp;d=speed&amp;amp;range=6h&amp;amp;step=5&amp;amp;window=15'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;72 points; each folds the 15 minute buckets before it. &lt;code&gt;window = step&lt;/code&gt; gives tumbling windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"step": 5, "window": 15, "last": {"value": 61}, "summary": {"count": 21540, …}, "points": [{"from": …, "to": …, "count": 900, "averageValue": 69.8}, …]}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Ftpth15c7p1eifw601tvj.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%2Ftpth15c7p1eifw601tvj.png" alt="Query explorer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3 — the whole cluster
&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl 'localhost:9080/tsd/categories'&lt;/code&gt; · &lt;code&gt;curl 'localhost:9080/tsd/health'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any node answers from replicated memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Categories busiest first; per node QPS, error rate, cache, replication lag, JVM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fs0ohwujsh3rmb9jjkanb.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%2Fs0ohwujsh3rmb9jjkanb.png" alt="System health" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Categories&lt;/th&gt;
&lt;th&gt;Zoom&lt;/th&gt;
&lt;th&gt;Light theme&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fzvg4d4r5v1lrxvjkymqn.png" width="800" height="500"&gt;&lt;/td&gt;
&lt;td&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%2Fxntnizkh107g1jnh6vbp.png" width="800" height="500"&gt;&lt;/td&gt;
&lt;td&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%2Fser8ml8ie6lh3xg0252n.png" width="800" height="500"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Configuration
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_RETENTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;24h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How long data is kept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_SPAN_MINUTES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bucket width; must divide 60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_TIME_ZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UTC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone when a request has no &lt;code&gt;z&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_CACHE_MAX_KEYS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keys per node in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_REDIS_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;blank&lt;/td&gt;
&lt;td&gt;Overflow Redis; blank deletes keys beyond the limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_SNAPSHOT_INTERVAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Snapshot interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VORTEX_GATEWAY_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9080&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Host port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All settings: &lt;a href="https://github.com/paganini2008/vortex/blob/main/docs/configuration.md" rel="noopener noreferrer"&gt;docs/configuration.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Performance &amp;amp; Comparison
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt; Docker Desktop, 4 CPUs / 8 GB, 3 nodes, k6 through the gateway.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;826,781 writes at rising rates&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 failures&lt;/strong&gt;, identical totals on all nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sustained&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1,000 samples/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,400–1,500 samples/s&lt;/strong&gt;, leader ~2.3 CPUs, heap ≤ 310 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;kill -9&lt;/code&gt; leader at 600/s&lt;/td&gt;
&lt;td&gt;new leader in &lt;strong&gt;4.7 s&lt;/strong&gt;, 0.28% of requests failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graceful leader stop&lt;/td&gt;
&lt;td&gt;immediate hand-over, 0 failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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;Vortex&lt;/th&gt;
&lt;th&gt;Prometheus&lt;/th&gt;
&lt;th&gt;InfluxDB&lt;/th&gt;
&lt;th&gt;TimescaleDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writes&lt;/td&gt;
&lt;td&gt;HTTP push&lt;/td&gt;
&lt;td&gt;pull&lt;/td&gt;
&lt;td&gt;HTTP push&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;HTTP params&lt;/td&gt;
&lt;td&gt;PromQL&lt;/td&gt;
&lt;td&gt;InfluxQL / SQL&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;memory + snapshots&lt;/td&gt;
&lt;td&gt;disk&lt;/td&gt;
&lt;td&gt;disk&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HA&lt;/td&gt;
&lt;td&gt;built in&lt;/td&gt;
&lt;td&gt;extra setup&lt;/td&gt;
&lt;td&gt;by edition&lt;/td&gt;
&lt;td&gt;PG tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;live metrics, days&lt;/td&gt;
&lt;td&gt;monitoring&lt;/td&gt;
&lt;td&gt;time series&lt;/td&gt;
&lt;td&gt;analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  9. Limitations &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One leader applies every write&lt;/td&gt;
&lt;td&gt;~1,000–1,500 samples/s; does not grow with nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data in memory&lt;/td&gt;
&lt;td&gt;Every node killed at once → back to the last snapshot (≤ 5 min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Async replication&lt;/td&gt;
&lt;td&gt;Writes in flight when the leader crashes fail; some may still be stored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis overflow&lt;/td&gt;
&lt;td&gt;Reads of spilled keys cost a network round trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No query language, alerting, auth&lt;/td&gt;
&lt;td&gt;Keep it on a private network&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Not for:&lt;/strong&gt; ledgers that must never lose a record · months or years of retention · tens of thousands of writes per second.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One command&lt;/strong&gt; — &lt;code&gt;docker compose up -d&lt;/code&gt;, images for amd64 and arm64.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just HTTP&lt;/strong&gt; — push with &lt;code&gt;t/c/d/v&lt;/code&gt;, query with &lt;code&gt;range/step/window/z&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact&lt;/strong&gt; — leader-serialised writes, no drift, no locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local reads&lt;/strong&gt; — every node a full replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows for free&lt;/strong&gt; — tumbling and sliding, folded from minute buckets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-healing&lt;/strong&gt; — new leader in ~5 s; graceful stops unnoticed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable enough&lt;/strong&gt; — snapshots at stop and every 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded&lt;/strong&gt; — key limit per node, optional Redis overflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observable&lt;/strong&gt; — &lt;code&gt;/tsd/health&lt;/code&gt; and a web console out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clear niche&lt;/strong&gt; — live metrics with days of retention.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/paganini2008/vortex" rel="noopener noreferrer"&gt;https://github.com/paganini2008/vortex&lt;/a&gt; · &lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://paganini2008.github.io/vortex/" rel="noopener noreferrer"&gt;https://paganini2008.github.io/vortex/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Openspreader: The missing piece of Java concurrency. `java.util.concurrent`, scoped to the cluster.</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:19:12 +0000</pubDate>
      <link>https://dev.to/paganini2008/openspreader-javautilconcurrent-scoped-to-the-cluster-no-redis-no-zookeeper-db2</link>
      <guid>https://dev.to/paganini2008/openspreader-javautilconcurrent-scoped-to-the-cluster-no-redis-no-zookeeper-db2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Thirteen coordination primitives as one Spring Boot starter. The APIs you already know,&lt;br&gt;
spanning every instance of your application instead of one JVM. Zero lines of configuration to&lt;br&gt;
start.&lt;/strong&gt;&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;// A lock across every replica of this application&lt;/span&gt;
&lt;span class="nc"&gt;ProcessingMutex&lt;/span&gt; &lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syncs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applicationMutex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"daily-report"&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;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryAcquire&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="n"&gt;generateReport&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="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&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;// A scheduled task that runs on one instance per round. One annotation.&lt;/span&gt;
&lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0 0 2 * * *"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@MultiProcessingScheduled&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;syncOrdersDaily&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;h2&gt;
  
  
  What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;synchronized&lt;/code&gt; only covers one JVM. &lt;code&gt;Semaphore&lt;/code&gt; only limits one process. &lt;code&gt;@Scheduled&lt;/code&gt; on three&lt;br&gt;
replicas runs three times. The usual fix is Redis for a distributed lock, ZooKeeper for&lt;br&gt;
coordination, and hand-written leader election for the scheduler: three pieces of&lt;br&gt;
infrastructure, so that three replicas can agree on who does what.&lt;/p&gt;

&lt;p&gt;openspreader does it with one dependency. Locks acquire in about a millisecond, cache reads are&lt;br&gt;
served from local memory at over four million per second and never touch the network, and a&lt;br&gt;
task dispatched to a peer runs there without your code knowing it moved.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&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;com.chaconne-ai&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;openspreader&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;1.0.0-SNAPSHOT&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;repositories&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;repository&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;central-snapshots&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;https://central.sonatype.com/repository/maven-snapshots/&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;snapshots&amp;gt;&amp;lt;enabled&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/enabled&amp;gt;&amp;lt;/snapshots&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/repository&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/repositories&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Not a line of configuration is required.&lt;/strong&gt; Discovery defaults to 127.0.0.1, so two instances&lt;br&gt;
on one machine with different &lt;code&gt;server.port&lt;/code&gt; values form a cluster by themselves. Local&lt;br&gt;
debugging works out of the box.&lt;/p&gt;

&lt;p&gt;One line has to change for production:&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;spring.spreader.ip-addresses&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;192.168.0.111,192.168.0.63,192.168.0.77&lt;/span&gt;
&lt;span class="py"&gt;spring.spreader.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;order-cluster&lt;/span&gt;
&lt;span class="py"&gt;spring.spreader.multiprocessing.mutex.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With three instances started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node started: cluster=order-cluster, name=order-service, clusterPort=22000
Mutex service started

[instance 1] acquired nightly-report, generating
[instance 2] tryAcquire returned false, skipping this round
[instance 3] tryAcquire returned false, skipping this round
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Every component is off by default&lt;/strong&gt;, so adding the starter to a running service changes&lt;br&gt;
nothing until you ask for one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;17 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;4.1, built and tested against it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Micrometer for Prometheus, Kryo for faster serialisation, &lt;code&gt;spring-data-redis&lt;/code&gt; for cache overflow, Netty/MINA/Grizzly for an alternative transport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ports&lt;/td&gt;
&lt;td&gt;one cluster port (22000 by default, identical on every node) plus one work port per node&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Everything rests on &lt;a href="//./spreader-en.md"&gt;spreader&lt;/a&gt;: the cluster runs &lt;strong&gt;inside the same JVM&lt;/strong&gt; as&lt;br&gt;
your beans, so there is no broker and no registry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─────────────────── your application ───────────────────┐
  │                                                        │
  │   @Scheduled   mutex.tryAcquire()   cache.get(k)       │
  │        │              │                  │             │
  │   ┌────┴──────────────┴──────────────────┴─────────┐   │
  │   │            openspreader components             │   │
  │   │  lock  semaphore  latch  cache  pool  rpc  dag │   │
  │   └────────────────────┬───────────────────────────┘   │
  │                        │                               │
  │   ┌────────────────────┴───────────────────────────┐   │
  │   │      spreader: gossip, members, leader         │   │
  │   └────────────────────┬───────────────────────────┘   │
  └────────────────────────┼───────────────────────────────┘
                           │ TCP/UDP
              ┌────────────┼────────────┐
         other replica           other replica
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Writes go through the leader, reads stay local.&lt;/strong&gt; The registers for locks, permits, latches&lt;br&gt;
and barriers live in the leader's memory; the cache replicates operations rather than data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cache.incr("views", 1) on a follower
    │
    ├─ forwarded to the leader
    │
    ├─ leader applies it, assigns version 1007
    │
    └─ broadcasts {v1007, INCR, "views", 1} to every replica
                      │
         each replica applies it to its OWN copy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why &lt;code&gt;setbit&lt;/code&gt; on a 100MB bitmap ships one datagram rather than the bitmap, and why a&lt;br&gt;
cluster-wide Bloom filter is practical at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code Examples
&lt;/h2&gt;

&lt;p&gt;Five representative components. Every component has a runnable example under&lt;br&gt;
&lt;code&gt;com.chaconneai.openspreader.example&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lock
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;&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;ProcessingMutex&lt;/span&gt; &lt;span class="n"&gt;mutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syncs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applicationMutex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"bulk-export"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;: three ways to take it, matching three attitudes.&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="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryAcquire&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                    &lt;span class="c1"&gt;// skip if taken, do not wait&lt;/span&gt;
&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// wait three seconds, then skip&lt;/span&gt;
&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;acquire&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                       &lt;span class="c1"&gt;// wait indefinitely&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isHeld&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// is anyone holding it&lt;/span&gt;
&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentOccupied&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// WHO holds it, which answers "why did my task not run"&lt;/span&gt;
&lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// release, but keep others out for five seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That cooldown solves a real problem: a task finishes, another replica immediately takes the&lt;br&gt;
lock and runs it again. A cooldown window removes the duplicate.&lt;/p&gt;
&lt;h3&gt;
  
  
  Semaphore
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: protect a third party API at five concurrent calls.&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;ProcessingSemaphore&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;syncs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applicationSemaphore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"third-party-api"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryAcquire&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&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="n"&gt;callThirdParty&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="n"&gt;sem&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&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;&lt;strong&gt;Output&lt;/strong&gt;: &lt;strong&gt;the five permits are shared cluster-wide&lt;/strong&gt;, whatever the replica count. That is&lt;br&gt;
exactly what a single process &lt;code&gt;Semaphore&lt;/code&gt; cannot do: three replicas each holding a local&lt;br&gt;
semaphore of 5 gives you an actual concurrency of 15.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cache
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: 50 commands over four data structures, plus bitmaps and Bloom filters.&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="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"k"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MINUTES&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hset&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user:1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zadd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"leaderboard"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;member&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;99.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setbit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"seen"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&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="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zrangeByScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"series"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&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="mi"&gt;500&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// a page, not a million members&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;zremrangeByScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"series"&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="n"&gt;cutoff&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// a retention policy in one operation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;: reads come from local memory; writes are forwarded to the leader and broadcast&lt;br&gt;
back incrementally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output&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;reads     over 4,000,000 per second, never touching the network
writes    roughly 2,000 per second (TCP), 8,300 (UDP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it suits shared state that is &lt;strong&gt;read far more than written&lt;/strong&gt;: configuration, allow-lists,&lt;br&gt;
rate-limit counters, sessions. A write becomes visible elsewhere after milliseconds. It is&lt;br&gt;
&lt;strong&gt;eventually consistent&lt;/strong&gt;, not a database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When memory is not enough&lt;/strong&gt;: register a &lt;code&gt;CacheStore&lt;/code&gt; bean and eviction becomes a &lt;em&gt;move&lt;/em&gt;&lt;br&gt;
rather than a loss.&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;spring.spreader.multiprocessing.cache.external.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A key lives in exactly one half&lt;/td&gt;
&lt;td&gt;Memory, or outside. Reads ask memory first; writes go where the key already is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eviction moves instead of deleting&lt;/td&gt;
&lt;td&gt;Same sampling, same policy, but the key is written across before the memory copy goes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only the leader writes outside&lt;/td&gt;
&lt;td&gt;A follower replaying the replication stream touches memory only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitmaps stay in memory&lt;/td&gt;
&lt;td&gt;A Bloom filter lookup does seven bit tests, which outside would be seven round trips&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing is enabled by default&lt;/td&gt;
&lt;td&gt;Without the bean, &lt;code&gt;store&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; the local store: same object, no wrapper, no extra call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  MapReduce
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: implement a job, register it as a bean.&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="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wordCount"&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;WordCountJob&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;MapReduceJob&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;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;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;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;split&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;text&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;suggestedShards&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;splitByLines&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suggestedShards&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;map&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;shard&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Emitter&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;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&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;word&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\\W+"&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;word&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;emitter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&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="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="nf"&gt;reduce&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;word&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;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;counts&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;counts&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="na"&gt;mapToInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Integer:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;intValue&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;sum&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;&lt;strong&gt;Execution&lt;/strong&gt;: where each phase runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;split     the submitter only, once
map       every node, the submitter included, once per shard
reduce    wherever the key's hash sends it, once per distinct key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;MapReduceResult&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;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;mapReduce&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;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wordCount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;)&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="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MINUTES&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining the partial results is not yours to write: one key is reduced on exactly one node.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every node needs this bean, at the same version.&lt;/strong&gt; Half the nodes on a new &lt;code&gt;map&lt;/code&gt; and half&lt;br&gt;
on the old one produce &lt;strong&gt;a wrong answer rather than an error&lt;/strong&gt;. Do not submit jobs during a&lt;br&gt;
rolling deployment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  RPC
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: enable the scan, declare an interface pointing at another application's name.&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;@SpringBootApplication&lt;/span&gt;
&lt;span class="nd"&gt;@EnableRpcClients&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.example.client"&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;Application&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@RpcClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"inventory-service"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;maxConcurrent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                       &lt;span class="c1"&gt;// in-flight ceiling, fail fast when full&lt;/span&gt;
           &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InventoryFallback&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="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;InventoryApi&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;stockOf&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;sku&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;&lt;strong&gt;Execution&lt;/strong&gt;: inject and call it like any bean. The server side needs a bean with a matching&lt;br&gt;
method carrying &lt;code&gt;@MultiProcessingCall&lt;/code&gt;.&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stockOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"widget-1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;: routing is by application name rather than URL, and the target is chosen afresh on&lt;br&gt;
every call, so scaling on the other side takes effect immediately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxConcurrent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Without it, requests pile up locally when the downstream slows, and what falls over is your own process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maxRetries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A failure is retried &lt;strong&gt;on a different instance each time&lt;/strong&gt;, so the called method should be idempotent. Retrying after a timeout is ambiguous: the request may have completed with only the reply lost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring.spreader.name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cluster name, the only means of isolation between dev, staging and prod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring.spreader.ip-addresses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where to look for peers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring.spreader.advertise-host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Set this in containers&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;...cache.max-keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Per process, not per cluster&lt;/strong&gt;: every node holds a full replica&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;...cache.eviction-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LRU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Samples 5 keys and evicts the least recently used, as Redis does. Exact LRU would need a global lock on every read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;...cache.external.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build the Redis-backed overflow store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;...mutex.request-timeout-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shorter than the cache's: a lock request not answered quickly is better retried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;...mutex.lease-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;15000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renewed while held, so a task may run as long as it likes. Only a crashed holder loses it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Conditions&lt;/strong&gt;: single machine over loopback, 3-node cluster in one JVM, 4-core container, JDK&lt;br&gt;
serialization. Absolute values do not transfer; the ratios do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reads never leave the process
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,562,196&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,566,915&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;stats&lt;/code&gt; (aggregate, four numbers)&lt;/td&gt;
&lt;td&gt;4,130,498&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hget&lt;/code&gt; (single field)&lt;/td&gt;
&lt;td&gt;2,104,340&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getbit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,548,987&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zscore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,210,631&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;lrange&lt;/code&gt; (50 elements)&lt;/td&gt;
&lt;td&gt;345,364&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;zrange&lt;/code&gt; (50 members)&lt;/td&gt;
&lt;td&gt;287,519&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hgetAll&lt;/code&gt; (all fields)&lt;/td&gt;
&lt;td&gt;68,489&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;hget&lt;/code&gt; against &lt;code&gt;hgetAll&lt;/code&gt; is &lt;strong&gt;30x&lt;/strong&gt;. Calling &lt;code&gt;hgetAll&lt;/code&gt; in a loop is the easiest accidental&lt;br&gt;
mistake in this API, and it costs more than the read/write gap does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writes go through the leader
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;max&lt;/code&gt; (aggregate)&lt;/td&gt;
&lt;td&gt;7,314&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;incr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6,524&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;min&lt;/code&gt; / &lt;code&gt;sum&lt;/code&gt; (aggregate)&lt;/td&gt;
&lt;td&gt;6,356 to 6,539&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zadd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5,504&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rpush&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;setbit&lt;/code&gt; (fixed capacity)&lt;/td&gt;
&lt;td&gt;3,419&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;setbit&lt;/code&gt; (growing)&lt;/td&gt;
&lt;td&gt;1,991&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Aggregates are the fastest writes.&lt;/strong&gt; &lt;code&gt;max&lt;/code&gt;/&lt;code&gt;min&lt;/code&gt;/&lt;code&gt;sum&lt;/code&gt; keep four numbers, so there is no&lt;br&gt;
allocation, no resizing and no hash lookup, and the value travels in the &lt;code&gt;arg&lt;/code&gt; field rather&lt;br&gt;
than as a payload. That is what makes them suitable for downsampling a time series.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ceiling, and where it comes from
&lt;/h3&gt;

&lt;p&gt;Writes are serialised globally: every write goes through the leader and takes one state lock,&lt;br&gt;
because a monotonic version number is what lets all replicas replay in the same order. The&lt;br&gt;
lock's granularity &lt;strong&gt;is&lt;/strong&gt; the version number's granularity.&lt;/p&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read scaling&lt;/td&gt;
&lt;td&gt;Linear with node count. Reads are local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write scaling&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Flat.&lt;/strong&gt; Adding nodes does not raise write throughput; it raises the leader's broadcast fan-out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practical write ceiling&lt;/td&gt;
&lt;td&gt;~2,000/s on TCP, ~8,300/s on UDP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the question to ask is how far your write rate is from those numbers. Far means no problem.&lt;br&gt;
Close means that data may not belong here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;p&gt;Every coordination capability rests on one thing: &lt;strong&gt;whoever holds the cluster port is the&lt;br&gt;
leader&lt;/strong&gt;, and the write path for locks, permits, latches, barriers and the cache is all there.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Premise&lt;/th&gt;
&lt;th&gt;Consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Leader uniqueness rests on timing, not consensus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Under partition each side may have a leader, and two parties hold the same lock. Fine for work where a repeat is wasteful: scheduled tasks, cache warming, batch de-duplication. &lt;strong&gt;Not&lt;/strong&gt; for a transfer or a debit, which belongs in a database transaction or behind an idempotence key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A change of leader leaves a gap&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Takeover measured at 3.3 to 4.4 seconds, during which lock acquisitions and cache writes retry. Leave business timeouts headroom, or you will see "it failed once and was fine a few seconds later"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The cache is a cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every node holds a full replica in heap. Not a database, not durable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eviction is local&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nodes can disagree about which keys are resident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Everything is in-process&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No persistence by default: a full cluster restart starts from empty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resume is at-least-once&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A DAG node that ran and reported back, whose record was not yet written when the coordinator died, is a step that happened and is not in the record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-machine performance is unmeasured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every number above is one JVM over loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Thirteen coordination primitives, one starter&lt;/strong&gt;, zero configuration to start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The APIs you already know&lt;/strong&gt;, scoped to every instance of the application rather than one JVM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads never leave the process&lt;/strong&gt;: over four million per second, because every node holds a full cache replica&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes have a clear ceiling&lt;/strong&gt;: ~2,000/s on TCP, from a monotonic version number that must be assigned serially&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregates are the fastest writes&lt;/strong&gt; (7,314/s), four numbers in constant memory, suited to downsampling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hget&lt;/code&gt; is 30x faster than &lt;code&gt;hgetAll&lt;/code&gt;&lt;/strong&gt;, a trap easier to fall into than the read/write gap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every component is off by default&lt;/strong&gt;, so adding the starter changes no behaviour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory can overflow to an external store&lt;/strong&gt;, turning eviction from a loss into a move. Off by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two premises to accept&lt;/strong&gt;: leader uniqueness rests on timing not consensus, and a change of leader leaves a gap&lt;/li&gt;
&lt;li&gt;The cluster layer underneath is &lt;a href="//./spreader-en.md"&gt;spreader&lt;/a&gt;, where the same post builds a replicated H2 on it&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Spreader: Your JVM processes are the cluster. Nothing else to install.</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:17:21 +0000</pubDate>
      <link>https://dev.to/paganini2008/spreader-your-jvm-processes-are-the-cluster-nothing-else-to-install-34jl</link>
      <guid>https://dev.to/paganini2008/spreader-your-jvm-processes-are-the-cluster-nothing-else-to-install-34jl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Turn plain JVM instances into a cluster that elects a leader, tracks every member and passes&lt;br&gt;
messages between them. One dependency, one line of configuration, and no ZooKeeper, etcd,&lt;br&gt;
Consul or registry anywhere in the picture. The processes are the cluster.&lt;/strong&gt;&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;GossipCluster&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GossipCluster&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;GossipConfig&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="na"&gt;ipAddresses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"10.0.0.1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"10.0.0.2"&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="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&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;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLeader&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;runTheNightlyJob&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// exactly one instance does this&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;Three machines running the same application, and you need them to agree on which one runs the&lt;br&gt;
nightly job, to notice within seconds when one dies, and to send each other a message. The&lt;br&gt;
standard answer is a new piece of infrastructure: something to deploy, monitor, upgrade and get&lt;br&gt;
paged about at 3am, for a problem that is not your product.&lt;/p&gt;

&lt;p&gt;spreader keeps it inside the processes. Every node runs the same code and holds the same&lt;br&gt;
complete member list, with no special node and no role to assign. Measured: 18,464 cross-node&lt;br&gt;
round trips per second, and a dead leader replaced in 3 to 4 seconds.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&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;com.chaconne-ai&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;spreader&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;1.0.0-SNAPSHOT&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;Snapshots need the repository declared too:&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;repositories&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;repository&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;central-snapshots&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;https://central.sonatype.com/repository/maven-snapshots/&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;snapshots&amp;gt;&amp;lt;enabled&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/enabled&amp;gt;&amp;lt;/snapshots&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/repository&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/repositories&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open two or three terminals and run one in each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-cp&lt;/span&gt; spreader-1.0.0-SNAPSHOT.jar &lt;span class="se"&gt;\&lt;/span&gt;
     com.chaconneai.spreader.example.BestPractice &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;22000 &lt;span class="nt"&gt;--ipAddresses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 &lt;span class="nt"&gt;--portRetry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node started: cluster=demo, id=a1b2c3d4, clusterPort=22000, transport=TCP/NioTcpTransport
joined: the only node so far, so this one leads

===== cluster [demo] =====
this node: demo-app@127.0.0.1:30001  [leader]
leader:    demo-app@127.0.0.1:30001
members:   1

&amp;gt;&amp;gt;&amp;gt; [joined] demo-app@127.0.0.1:30002, members now 2
&amp;gt;&amp;gt;&amp;gt; [joined] demo-app@127.0.0.1:30003, members now 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kill the leader, and within seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; [leader changed] 127.0.0.1:30001 -&amp;gt; 127.0.0.1:30002  (this node is now the leader)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Requirements
&lt;/h2&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;17 or later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Netty, MINA or Grizzly, to replace the built-in NIO transport&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ports&lt;/td&gt;
&lt;td&gt;one cluster port (22000 by default, identical on every node) plus one work port per node, chosen automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Two mechanisms, both deliberately boring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Membership spreads by SWIM gossip.&lt;/strong&gt; Every second each node picks a few peers at random and&lt;br&gt;
exchanges member lists directly, never through a coordinator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    Node A ◄──────── gossip ────────► Node B
       ▲  holds :22000                   ▲
       │  = is the leader                │
       └──────────── gossip ─────────────┘
                      │
                   Node C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Leadership is pre-emptive.&lt;/strong&gt; The cluster has one fixed port and whichever node holds it&lt;br&gt;
leads. A new node scans the configured addresses, knocks on that port, and either finds a&lt;br&gt;
leader or becomes one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;leader dies
    │
    ├─ failure detection notices (probe, indirect probe, suspect timeout)
    │
    ├─ every eligible node computes its rank in the takeover order
    │
    ├─ rank 0 waits 0ms, rank 1 waits takeoverDelayMs, rank 2 waits 2x ...
    │
    └─ first to bind :22000 is the new leader; the rest see it and stand down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pre-emptive and consensus-based election are two routes, not two grades
&lt;/h3&gt;

&lt;p&gt;Where a leader's legitimacy comes from is the root of every other difference. Pre-emptive: from&lt;br&gt;
holding a physically exclusive resource. Consensus-based: from a majority's authorisation,&lt;br&gt;
since two majorities must intersect and a node in that intersection will not vote twice in one&lt;br&gt;
term.&lt;/p&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;Pre-emptive (this library)&lt;/th&gt;
&lt;th&gt;Consensus-based (Raft)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How a leader arises&lt;/td&gt;
&lt;td&gt;whoever takes the port&lt;/td&gt;
&lt;td&gt;whoever a majority votes for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Must the leader be up to date?&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No.&lt;/strong&gt; A node that just restarted can lead&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes.&lt;/strong&gt; A stale log cannot win a vote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terms&lt;/td&gt;
&lt;td&gt;none, so no built-in fencing&lt;/td&gt;
&lt;td&gt;monotonic; an older term is refused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One node&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two nodes, one dies&lt;/td&gt;
&lt;td&gt;the survivor takes over&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;stalls&lt;/strong&gt;, no majority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Membership changes&lt;/td&gt;
&lt;td&gt;soft state, gossiped, free&lt;/td&gt;
&lt;td&gt;configuration state, changed through the log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;fsync before voting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Under partition&lt;/td&gt;
&lt;td&gt;both sides can lead and accept writes&lt;/td&gt;
&lt;td&gt;the minority elects nobody and refuses writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Takeover time&lt;/td&gt;
&lt;td&gt;3 to 4 seconds, dominated by failure detection&lt;/td&gt;
&lt;td&gt;typically a few hundred milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two consequences shape how to use it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A pre-emptive leader carries no data authority.&lt;/strong&gt; It is not required to be up to date, so
any state kept on it has to be rebuildable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With no terms there is no built-in fencing.&lt;/strong&gt; A leader already replaced can still write to
an outside resource. Where that matters, have the leader issue a monotonic token, carry it
into whatever you write, and have that resource refuse older ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dividing line is the &lt;strong&gt;nature of the work, not its importance&lt;/strong&gt;. Coordination where a&lt;br&gt;
repeat is merely wasteful fits this route. Work where a repeat causes real harm needs&lt;br&gt;
consensus-based election.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code Examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Example 1: members and the leader
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                        &lt;span class="c1"&gt;// the whole cluster&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;Node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;peers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;membersOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-service"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// just my own application&lt;/span&gt;
&lt;span class="nc"&gt;Node&lt;/span&gt; &lt;span class="n"&gt;leader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;leader&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                            &lt;span class="c1"&gt;// can be null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;: reach for &lt;code&gt;membersOf&lt;/code&gt; more often than &lt;code&gt;members&lt;/code&gt;. "The other replicas of my own&lt;br&gt;
application" is usually the question actually being asked, especially once a cluster runs more&lt;br&gt;
than one application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;: each &lt;code&gt;Node&lt;/code&gt; carries what you need.&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="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// an address that can be dialled&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// application name&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// ALIVE / SUSPECT / LEFT&lt;/span&gt;
&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// your own business metadata, gossiped cluster-wide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;metadata&lt;/code&gt; is an extension channel: to let everyone know this node's version, zone or canary&lt;br&gt;
flag, put it in the configuration with &lt;code&gt;.metadata("zone", "az-1")&lt;/code&gt; and every node reads it off&lt;br&gt;
the member list.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 2: a real trap
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: three replicas, and a scheduled task that should run once.&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;// Wrong&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;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLeader&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;syncOrdersDaily&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;&lt;strong&gt;Execution&lt;/strong&gt;: the leader is a &lt;strong&gt;cluster-wide&lt;/strong&gt; notion that does not distinguish applications.&lt;br&gt;
If your cluster also runs an api-facade and the cluster port happens to be held by one of its&lt;br&gt;
instances...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;: every replica of order-service sees &lt;code&gt;isLeader() == false&lt;/code&gt; and the task &lt;strong&gt;never runs&lt;br&gt;
at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The right tool is openspreader's &lt;code&gt;@MultiProcessingScheduled&lt;/code&gt;, which picks one executor per&lt;br&gt;
application. Or elect within your own application against &lt;code&gt;membersOf(self().name())&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 3: a replicated H2
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt;: any node writes, and callers do not need to know whether they are the leader.&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="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INSERT INTO product (name, price) VALUES ('widget-1', 10.99)"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;: the write takes one path only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write on a follower          write on the leader
       │                            │
 sendToLeader(sql)                  │
       │                            │
       └────► the leader applies it locally
                      │
             multicast to every replica
                      │
             each applies it to its own H2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;write&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;sql&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;SQLException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&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="n"&gt;encode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLeader&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;REPLICATE&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;WRITE_REQUEST&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&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;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLeader&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Apply first, then tell the others. The other order would have a replica&lt;/span&gt;
        &lt;span class="c1"&gt;// holding a row the leader itself does not&lt;/span&gt;
        &lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multicastOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CHANNEL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;self&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&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="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendToLeaderOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CHANNEL&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="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// No leader at this instant. Surfacing it beats silently dropping the write&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;SQLException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No leader to take the write; retry in a moment"&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;&lt;strong&gt;Output&lt;/strong&gt;: every node reads its own copy, and no read leaves the process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-cp&lt;/span&gt; spreader.jar:h2.jar &lt;span class="se"&gt;\&lt;/span&gt;
     com.chaconneai.spreader.example.ReplicatedH2Example &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;22000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[127.0.0.1:30001 leader] rows=8 applied=8 members=3
[127.0.0.1:30002]        rows=8 applied=8 members=3
[127.0.0.1:30003]        rows=8 applied=8 members=3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One step is easy to miss: &lt;strong&gt;a node joining an established cluster starts empty.&lt;/strong&gt; Replicating&lt;br&gt;
only the increments leaves it permanently short of history, answering reads with nothing, which&lt;br&gt;
is worse than answering slowly. So ask for a full copy on join:&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;@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;onClusterJoined&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Node&lt;/span&gt; &lt;span class="n"&gt;self&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;alone&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;alone&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendToLeaderOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CHANNEL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SNAPSHOT_REQUEST&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="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;H2 has a convenient statement for this: &lt;code&gt;SCRIPT&lt;/code&gt; dumps the whole database as SQL, one statement&lt;br&gt;
a row. The leader dumps it and sends it over, the new node runs &lt;code&gt;DROP ALL OBJECTS&lt;/code&gt; and replays&lt;br&gt;
it, and it is caught up.&lt;/p&gt;

&lt;p&gt;The complete code is in &lt;code&gt;com.chaconneai.spreader.example.ReplicatedH2Example&lt;/code&gt;, about 350 lines.&lt;br&gt;
It needs &lt;strong&gt;no H2 at compile time&lt;/strong&gt;, using only &lt;code&gt;java.sql&lt;/code&gt; from the JDK, so the spreader jar&lt;br&gt;
does not carry it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Defaults are tuned for a small cluster on a reliable LAN. One line genuinely has to change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip-addresses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where to look for peers. A few seeds are enough; a new node learns the full member list from any one of them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The only means of isolation.&lt;/strong&gt; Nodes with different names ignore each other even when the network can reach them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The application name. Instances sharing one are replicas, and the layers above use it to tell who belongs with whom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster-port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The port that decides leadership. Identical on every node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;advertise-host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;auto-detected&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Set this in containers.&lt;/strong&gt; Auto-detection may pick an address no peer can reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;leader-eligible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt; makes this application join and work but never contend for leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transport-type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TCP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TCP unless you have measured otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;payload-ack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wait for an ACK and resend. &lt;strong&gt;Off is roughly 3x the throughput&lt;/strong&gt;, since the ACK round trip dominates small-message cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe-timeout-ms&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;800&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raise this first on a congested network. Too low and healthy nodes get suspected during a GC pause&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Numbers expose a property of the design. &lt;strong&gt;Absolute values will not transfer to your&lt;br&gt;
hardware&lt;/strong&gt;: every cross-node call here costs a loopback hop instead of a real network one. The&lt;br&gt;
ratios will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditions&lt;/strong&gt;: single machine over loopback, 8 threads x 2000 synchronous round trips, ~300&lt;br&gt;
byte payloads, JDK serialization, 4-core container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transport x protocol: the pairing is the unit of choice
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Combination&lt;/th&gt;
&lt;th&gt;QPS&lt;/th&gt;
&lt;th&gt;avg&lt;/th&gt;
&lt;th&gt;P50&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NIO / TCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18,464&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.426 ms&lt;/td&gt;
&lt;td&gt;0.367 ms&lt;/td&gt;
&lt;td&gt;1.69 ms&lt;/td&gt;
&lt;td&gt;5.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NIO / UDP&lt;/td&gt;
&lt;td&gt;13,023&lt;/td&gt;
&lt;td&gt;0.606 ms&lt;/td&gt;
&lt;td&gt;0.423 ms&lt;/td&gt;
&lt;td&gt;6.46 ms&lt;/td&gt;
&lt;td&gt;15.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINA / TCP&lt;/td&gt;
&lt;td&gt;12,249&lt;/td&gt;
&lt;td&gt;0.640 ms&lt;/td&gt;
&lt;td&gt;0.438 ms&lt;/td&gt;
&lt;td&gt;5.18 ms&lt;/td&gt;
&lt;td&gt;15.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NETTY / UDP&lt;/td&gt;
&lt;td&gt;10,682&lt;/td&gt;
&lt;td&gt;0.746 ms&lt;/td&gt;
&lt;td&gt;0.486 ms&lt;/td&gt;
&lt;td&gt;7.19 ms&lt;/td&gt;
&lt;td&gt;28.9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MINA / UDP&lt;/td&gt;
&lt;td&gt;10,494&lt;/td&gt;
&lt;td&gt;0.751 ms&lt;/td&gt;
&lt;td&gt;0.464 ms&lt;/td&gt;
&lt;td&gt;7.24 ms&lt;/td&gt;
&lt;td&gt;21.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRIZZLY / TCP&lt;/td&gt;
&lt;td&gt;9,171&lt;/td&gt;
&lt;td&gt;0.864 ms&lt;/td&gt;
&lt;td&gt;0.580 ms&lt;/td&gt;
&lt;td&gt;7.68 ms&lt;/td&gt;
&lt;td&gt;21.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRIZZLY / UDP&lt;/td&gt;
&lt;td&gt;8,751&lt;/td&gt;
&lt;td&gt;0.910 ms&lt;/td&gt;
&lt;td&gt;0.644 ms&lt;/td&gt;
&lt;td&gt;6.96 ms&lt;/td&gt;
&lt;td&gt;20.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NETTY / TCP&lt;/td&gt;
&lt;td&gt;7,081&lt;/td&gt;
&lt;td&gt;1.115 ms&lt;/td&gt;
&lt;td&gt;0.495 ms&lt;/td&gt;
&lt;td&gt;14.46 ms&lt;/td&gt;
&lt;td&gt;56.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pick the cell, not the framework.&lt;/strong&gt; Netty is second-best on UDP and &lt;em&gt;last&lt;/em&gt; on TCP, with a P99&lt;br&gt;
twice anyone else's. MINA is the opposite. "Which transport is fastest" has no answer.&lt;/p&gt;

&lt;p&gt;Treat anything under ~20% as noise: an earlier run on a 12-core host put Netty/UDP on top, and&lt;br&gt;
NIO/TCP measured 16,351 and 14,637 on two consecutive runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same four transports, through the components
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation (TCP)&lt;/th&gt;
&lt;th&gt;NIO&lt;/th&gt;
&lt;th&gt;NETTY&lt;/th&gt;
&lt;th&gt;MINA&lt;/th&gt;
&lt;th&gt;GRIZZLY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lock acquire + release&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;875&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;686&lt;/td&gt;
&lt;td&gt;616&lt;/td&gt;
&lt;td&gt;648&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache write (&lt;code&gt;incr&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,110&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,733&lt;/td&gt;
&lt;td&gt;1,440&lt;/td&gt;
&lt;td&gt;1,748&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task dispatch to a peer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11,581&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10,028&lt;/td&gt;
&lt;td&gt;11,473&lt;/td&gt;
&lt;td&gt;9,148&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation (UDP)&lt;/th&gt;
&lt;th&gt;NIO&lt;/th&gt;
&lt;th&gt;NETTY&lt;/th&gt;
&lt;th&gt;MINA&lt;/th&gt;
&lt;th&gt;GRIZZLY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lock acquire + release&lt;/td&gt;
&lt;td&gt;2,878&lt;/td&gt;
&lt;td&gt;2,162&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,009&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache write (&lt;code&gt;incr&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8,326&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,237&lt;/td&gt;
&lt;td&gt;2,645&lt;/td&gt;
&lt;td&gt;1,618&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task dispatch to a peer&lt;/td&gt;
&lt;td&gt;5,809&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10,787&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10,407&lt;/td&gt;
&lt;td&gt;8,856&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coordination runs 3 to 4 times faster over UDP&lt;/strong&gt; (locks 875 to 2,878, cache writes 2,110 to
8,326). These are short one-way messages plus a reply, and TCP's acknowledgement and
congestion control are pure overhead on that shape. A sustained request/response stream is
the opposite, and TCP wins there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grizzly collapses on UDP&lt;/strong&gt;: 720 against NIO's 2,878 is four-fold, far outside the noise
band, and it appears on every coordination row while leaving task dispatch alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reads are absent from both tables on purpose: they come from local memory and never touch the&lt;br&gt;
transport, so comparing them would only mislead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dispatch threads are not a throughput dial
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;msg/s&lt;/th&gt;
&lt;th&gt;per message&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 producers, 4 dispatch threads&lt;/td&gt;
&lt;td&gt;5,334,329&lt;/td&gt;
&lt;td&gt;187 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;serial dispatch (&lt;code&gt;payload-dispatch-threads=1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;5,100,959&lt;/td&gt;
&lt;td&gt;196 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;concurrent dispatch (&lt;code&gt;payload-dispatch-threads=4&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;3,453,596&lt;/td&gt;
&lt;td&gt;290 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rows two and three share a single producer, and &lt;strong&gt;the serial dispatcher wins by almost 50%&lt;/strong&gt;.&lt;br&gt;
Coordination between dispatch threads costs more than the parallelism returns. Raise this&lt;br&gt;
setting when several threads publish concurrently, leave it at 1 otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;th&gt;Detail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Election is pre-emptive, not consensus-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Under partition two sides can each elect a leader. It heals when the partition does, and every occurrence is counted. Work where a repeat causes real harm does not belong on this route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Membership is eventually consistent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Right after a change, different nodes briefly hold different views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UDP messages can be lost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;payload-ack&lt;/code&gt; covers it with resend and dedup, at a throughput cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-machine performance is unmeasured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every number above is one JVM over loopback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where it does not fit: thousands of nodes, clusters spanning datacentres, or a leader that must&lt;br&gt;
stay correct under partition. The first two make gossip convergence unpredictable; the third&lt;br&gt;
calls for Raft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One jar, one line of configuration.&lt;/strong&gt; No registry, no coordinator. The processes are the cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully decentralized&lt;/strong&gt;: every node runs the same code and holds the same complete member list&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Election needs no consensus round&lt;/strong&gt;: whoever holds the cluster port leads, and the OS already guarantees one port, one process&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-emptive and consensus-based are two routes&lt;/strong&gt;, not two grades. The line falls on the nature of the work, not its importance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;18,464 cross-node round trips per second&lt;/strong&gt; measured, and a dead leader replaced in 3 to 4 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the cell, not the framework&lt;/strong&gt;: Netty is last on TCP and second on UDP. There is no "fastest transport"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordination runs 3 to 4x faster over UDP&lt;/strong&gt;, while a sustained request/response stream favours TCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two failures that log nothing each have a counter&lt;/strong&gt;: dropped messages and a second leader&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits are stated, not hidden&lt;/strong&gt;: partition dual leaders, eventual consistency, UDP loss, cross-machine unmeasured&lt;/li&gt;
&lt;li&gt;For ready-made locks, caches and scheduled-task exclusion, see &lt;a href="//./openspreader-en.md"&gt;openspreader&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>distributedsystems</category>
      <category>gossip</category>
      <category>nio</category>
      <category>decentralization</category>
    </item>
    <item>
      <title>Cronflower: Turn a Spring Boot app into a distributed cron cluster</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Tue, 29 Sep 2026 11:32:26 +0000</pubDate>
      <link>https://dev.to/paganini2008/cronflower-turn-a-spring-boot-app-into-a-distributed-cron-cluster-5bep</link>
      <guid>https://dev.to/paganini2008/cronflower-turn-a-spring-boot-app-into-a-distributed-cron-cluster-5bep</guid>
      <description>&lt;p&gt;&lt;strong&gt;cronflower&lt;/strong&gt; is an open-source, distributed cron scheduler for the JVM with a web console. It forms&lt;br&gt;
its own cluster and needs no external database, broker, or coordinator. This post is the usage tour of&lt;br&gt;
its distributed &lt;strong&gt;task&lt;/strong&gt; scheduling (its DAG side has &lt;a href="//../cronflower/dag-workflow-orchestration.en.html"&gt;its own post&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%2Fsd1ssz1yat00mngwom2x.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsd1ssz1yat00mngwom2x.jpg" alt="The task list in the console" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What problem does it solve?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Scheduled&lt;/code&gt; runs in one JVM, so the moment you scale to two instances the job fires twice. It has no&lt;br&gt;
retry, no timeout, no history, and no view of what ran; if the box reboots at 02:00 the nightly rollup&lt;br&gt;
just quietly doesn't happen.&lt;/p&gt;

&lt;p&gt;The usual fix is to bolt on Quartz, a database, a lock table, and a dashboard you wrote yourself.&lt;br&gt;
cronflower is that whole stack behind two dependencies: you annotate a method, run the scheduler&lt;br&gt;
alongside it, and get a clustered, persistent, retrying scheduler with a console.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paganini2008/cronflower
&lt;span class="nb"&gt;cd &lt;/span&gt;cronflower/deploy
./run-local.sh &lt;span class="nt"&gt;-e&lt;/span&gt; 1          &lt;span class="c"&gt;# scheduler + console + 1 executor  (embedded H2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open &lt;a href="http://localhost:7200" rel="noopener noreferrer"&gt;http://localhost:7200&lt;/a&gt;, sign in &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin123&lt;/code&gt;, and the example tasks are already running.&lt;br&gt;
Scale into a real cluster with &lt;code&gt;./run-local.sh -n 3 -e 2&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Version / note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;17+ (builds via the bundled Maven Wrapper)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;20+ (builds the console)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;optional (container path only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;optional — none → embedded H2; MySQL / PostgreSQL for a shared store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Two kinds of process: a &lt;strong&gt;scheduler&lt;/strong&gt; owns the schedule and durable state; an &lt;strong&gt;executor&lt;/strong&gt; is your app,&lt;br&gt;
which declares tasks and runs the code on callback. Run several schedulers and they elect a leader over&lt;br&gt;
gossip; state lives in the store, so a restart or failover loses nothing.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  App["your app = executor&amp;lt;br/&amp;gt;@Task beans"] --&amp;gt;|register on boot| L
  subgraph Cluster["scheduler cluster"]
    L["leader"] &amp;lt;--&amp;gt;|gossip| F1["follower"]
    F1 &amp;lt;--&amp;gt;|gossip| F2["follower"]
  end
  Cluster --&amp;gt; Store[("store&amp;lt;br/&amp;gt;H2 · MySQL · PostgreSQL")]
  L --&amp;gt;|"dispatch when due (HTTP callback)"| App&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The leader loads only the tasks due in the next few minutes into a timing wheel and claims the rest&lt;br&gt;
from the store as they come due, so a cluster with hundreds of thousands of tasks still starts&lt;br&gt;
instantly.&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%2Fkoelehv49zvk3imvam6e.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkoelehv49zvk3imvam6e.jpg" alt="Cluster: three nodes, one leader, over a replicated store" width="799" height="381"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A task is a &lt;code&gt;@Task&lt;/code&gt; bean method
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — annotate a method on an executor (0 args, or a single &lt;code&gt;String&lt;/code&gt; parameter):&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoTasks&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Task&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*/5 * * * * ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"showcase"&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;"sayHello"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialParameter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"world"&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;String&lt;/span&gt; &lt;span class="nf"&gt;sayHello&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;who&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;"hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Fixed interval, no cron. Or iso = "PT1H30M" for an ISO-8601 duration.&lt;/span&gt;
    &lt;span class="nd"&gt;@Task&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intervalUnit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SECONDS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"showcase"&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;"every10s"&lt;/span&gt;&lt;span class="o"&gt;)&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;every10s&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// YCRON: noon on the 200th day of the year — no classic cron can say this.&lt;/span&gt;
    &lt;span class="nd"&gt;@Task&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0 0 12 ? ? 200"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ycron"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"showcase"&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;"dayOfYear200"&lt;/span&gt;&lt;span class="o"&gt;)&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;dayOfYear200&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&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;&lt;strong&gt;Execution&lt;/strong&gt; — discovered on boot, registered with the scheduler, which owns the schedule from then on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — each task shows up in the console with its schedule, run count, and next fire time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reliability is configured, not coded
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — add attributes; the &lt;strong&gt;scheduler&lt;/strong&gt; enforces them, so every executor behaves the same:&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;@Task&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"*/20 * * * * ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"reliability"&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;"flaky"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;maxRetryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// retry with back-off&lt;/span&gt;
      &lt;span class="n"&gt;timeout&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="c1"&gt;// fail a run that overruns 2s&lt;/span&gt;
      &lt;span class="n"&gt;misfirePolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SKIP"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;// FIRE_ONCE_NOW (default) / FIRE_ALL / SKIP&lt;/span&gt;
      &lt;span class="n"&gt;repeatCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                          &lt;span class="c1"&gt;// finish after 10 fires&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;flaky&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;parameter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* fails twice, then succeeds */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — in the execution history a single fire is three rows (attempt &lt;code&gt;#0&lt;/code&gt; fail, &lt;code&gt;#1&lt;/code&gt; fail, &lt;code&gt;#2&lt;/code&gt;&lt;br&gt;
success), each tagged with the scheduler that dispatched it and the executor that ran it:&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%2F1sksi1h9mz837fhcufne.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1sksi1h9mz837fhcufne.jpg" alt="Execution history: retry attempts and the node that ran each" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Build a schedule in code, or skip the executor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fluent, self-validating schedule instead of a hand-typed string:&lt;/span&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt; &lt;span class="nc"&gt;CronExpressionBuilder&lt;/span&gt; &lt;span class="nf"&gt;mondayMornings&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="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;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyWeek&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;Mon&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nd"&gt;@Task&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mondayMornings"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"weekly report"&lt;/span&gt;&lt;span class="o"&gt;)&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;weeklyReport&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a job that is just "call this URL on a schedule", create an &lt;strong&gt;HTTP-API task&lt;/strong&gt; from the console's&lt;br&gt;
&lt;em&gt;New task&lt;/em&gt; form or the REST API — the scheduler makes the call itself, no executor involved, and&lt;br&gt;
operators add or edit tasks with no redeploy.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@Task&lt;/code&gt; attributes, for reference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cron&lt;/code&gt; / &lt;code&gt;parser&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a cron schedule, or YCRON with &lt;code&gt;parser = "ycron"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;interval&lt;/code&gt; + &lt;code&gt;intervalUnit&lt;/code&gt; / &lt;code&gt;iso&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;a fixed interval, or an ISO-8601 duration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;builder&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;CronExpressionBuilder&lt;/code&gt; bean (wins over &lt;code&gt;cron&lt;/code&gt;; the only way to set a deadline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialParameter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a constant, or a SpEL template evaluated on each fire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;maxRetryCount&lt;/code&gt; / &lt;code&gt;retryInterval&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;retry a failed run, with back-off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fail a run that overruns, in milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;misfirePolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FIRE_ONCE_NOW&lt;/code&gt; (default) / &lt;code&gt;FIRE_ALL&lt;/code&gt; / &lt;code&gt;SKIP&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;repeatCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;total fires before the task finishes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronsmith.client.server-urls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;On the executor: scheduler seed URL(s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronsmith.server.scheduler.zone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UTC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fire-time zone — must match cluster-wide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronsmith.server.scheduler.window-minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Windowed-loading horizon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronsmith.server.scheduler.sharding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Group sharding over a shared store (else leader-only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cronsmith.server.dispatch.routing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROUND_ROBIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;…&lt;code&gt;WEIGHTED&lt;/code&gt; / &lt;code&gt;CONSISTENT_HASH&lt;/code&gt; / &lt;code&gt;RANDOM&lt;/code&gt; / &lt;code&gt;FIRST&lt;/code&gt; / &lt;code&gt;LAST&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spring.datasource.url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;H2 file&lt;/td&gt;
&lt;td&gt;Point at MySQL/PostgreSQL for a shared, sharding-capable store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Limitations &amp;amp; trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is a &lt;strong&gt;platform to run&lt;/strong&gt; (its own gossip cluster + store), not a tiny in-process library. For one
JVM with a couple of fixed jobs, plain &lt;code&gt;@Scheduled&lt;/code&gt; is lighter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group sharding&lt;/strong&gt; needs a &lt;strong&gt;shared&lt;/strong&gt; database (MySQL/PostgreSQL); on node-local H2/SQLite it safely
degrades to leader-only.&lt;/li&gt;
&lt;li&gt;Fire-time &lt;strong&gt;zone must match across nodes&lt;/strong&gt;; everything is computed and stored in UTC.&lt;/li&gt;
&lt;li&gt;Bean tasks run on executors, so the scheduler reaches them over HTTP — they must be reachable from it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A distributed, stateful cron scheduler for Spring Boot, &lt;strong&gt;two dependencies&lt;/strong&gt;, no external infra.&lt;/li&gt;
&lt;li&gt;Nodes &lt;strong&gt;gossip and elect a leader&lt;/strong&gt;; a restart or failover loses nothing, and no cron fires twice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@Task&lt;/code&gt; gives you cron / &lt;strong&gt;YCRON&lt;/strong&gt; / interval / ISO-8601, plus retry, timeout, misfire, repeat — declarative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP-API tasks&lt;/strong&gt; need no executor; operators add or edit tasks with no redeploy.&lt;/li&gt;
&lt;li&gt;Every run is &lt;strong&gt;recorded&lt;/strong&gt; with result, timing, attempt, and the node that ran it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group sharding&lt;/strong&gt; + &lt;strong&gt;weighted dispatch&lt;/strong&gt; scale it horizontally; a timing wheel + windowed loading keep startup instant.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;console&lt;/strong&gt; shows tasks, cluster, executors, health, and live settings from one endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it: &lt;a href="https://github.com/paganini2008/cronflower" rel="noopener noreferrer"&gt;cronflower on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>distributedsystems</category>
      <category>jobscheduler</category>
      <category>cronexpression</category>
    </item>
    <item>
      <title>Cronflower: Run a DAG workflow across your cluster, instead of chaining cron jobs</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Tue, 29 Sep 2026 11:30:51 +0000</pubDate>
      <link>https://dev.to/paganini2008/cronflower-run-a-dag-workflow-across-your-cluster-59be</link>
      <guid>https://dev.to/paganini2008/cronflower-run-a-dag-workflow-across-your-cluster-59be</guid>
      <description>&lt;p&gt;&lt;strong&gt;cronflower&lt;/strong&gt; is an open-source, distributed scheduler for the JVM with a web console. &lt;code&gt;cronflow&lt;/code&gt; is&lt;br&gt;
its optional DAG add-on: declare the steps and how they depend on each other as a graph, and the same&lt;br&gt;
cluster runs the graph node by node, with data flowing between nodes over typed channels. (Its&lt;br&gt;
distributed &lt;code&gt;@Task&lt;/code&gt; scheduling has &lt;a href="//../cronsmith/distributed-task-scheduling.en.html"&gt;its own post&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%2Fro0tlw9y7s7jacovwxda.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fro0tlw9y7s7jacovwxda.jpg" alt="Registered workflows and the selected graph's shape" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What problem does it solve?
&lt;/h2&gt;

&lt;p&gt;Teams end up chaining cron jobs: one at 02:00 charges the orders, another at 02:15 ships them, set&lt;br&gt;
fifteen minutes later because that is &lt;em&gt;usually&lt;/em&gt; long enough for the first to finish. There is no real&lt;br&gt;
dependency, no shared data, and no way to tell afterwards whether step two ran because step one&lt;br&gt;
succeeded or just because the clock moved.&lt;/p&gt;

&lt;p&gt;That is the wall plain cron hits: it schedules single jobs, it cannot orchestrate a flow of them. A&lt;br&gt;
DAG makes the dependency real, lets data flow between steps, and records every run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paganini2008/cronflower
&lt;span class="nb"&gt;cd &lt;/span&gt;cronflower/deploy
./run-local.sh &lt;span class="nt"&gt;-e&lt;/span&gt; 1          &lt;span class="c"&gt;# scheduler + console + 1 executor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open &lt;a href="http://localhost:7200" rel="noopener noreferrer"&gt;http://localhost:7200&lt;/a&gt; (&lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin123&lt;/code&gt;) and the example DAGs are registered under &lt;strong&gt;DAG &amp;gt;&lt;br&gt;
Workflows&lt;/strong&gt;, ready to trigger.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Version / note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JDK&lt;/td&gt;
&lt;td&gt;17+ (builds via the bundled Maven Wrapper)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;20+ (builds the console)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cronflow add-on&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cronflow-spring-boot-starter&lt;/code&gt; on the scheduler, &lt;code&gt;cronflow-executor-spring-boot-starter&lt;/code&gt; on the executor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;optional — none → embedded H2; MySQL / PostgreSQL for a shared store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A DAG is a Spring bean: &lt;code&gt;@Dag&lt;/code&gt; names it, each &lt;code&gt;@DagNode&lt;/code&gt; method is a step, and the &lt;code&gt;to&lt;/code&gt; list is the&lt;br&gt;
edges. A node returns a &lt;code&gt;Map&lt;/code&gt; of named &lt;strong&gt;channel&lt;/strong&gt; writes; downstream nodes read them from a&lt;br&gt;
&lt;code&gt;DagState&lt;/code&gt;. Each &lt;code&gt;@Channel&lt;/code&gt; says how concurrent writes to it merge, via a &lt;strong&gt;reducer&lt;/strong&gt;. The engine&lt;br&gt;
drives the graph &lt;strong&gt;across the whole cluster&lt;/strong&gt;, dispatching each node to a live executor.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  input((input)) --&amp;gt; intake
  intake --&amp;gt; credit &amp;amp; income &amp;amp; collateral
  credit --&amp;gt; decide
  income --&amp;gt; decide
  collateral --&amp;gt; decide
  decide --&amp;gt;|"score &amp;gt;= 70"| APPROVED
  decide --&amp;gt;|else| REJECTED&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Code examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Declare a workflow — &lt;code&gt;@Dag&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — a bean with &lt;code&gt;@Dag&lt;/code&gt; + &lt;code&gt;@DagNode&lt;/code&gt; methods and typed channels:&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;@Dag&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;"scoring-flow"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt;&lt;span class="o"&gt;},&lt;/span&gt; &lt;span class="n"&gt;channels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;@Channel&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;"score"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;reducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChannelReducer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SUM_INT&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
        &lt;span class="nd"&gt;@Channel&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;"factors"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reducer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChannelReducer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JOIN_CSV&lt;/span&gt;&lt;span class="o"&gt;)})&lt;/span&gt;
&lt;span class="nd"&gt;@Component&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;ScoringFlow&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@DagNode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&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="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"credit"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"income"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"collateral"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;     &lt;span class="c1"&gt;// fan out to 3 parallel scorers&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;intake&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&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;Map&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="s"&gt;"applicant"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&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;"input"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@DagNode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"decide"&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&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;Map&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="s"&gt;"score"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"factors"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"credit"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// income(), collateral() … same shape, writing to the same channels&lt;/span&gt;

    &lt;span class="nd"&gt;@DagNode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ALL"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                                           &lt;span class="c1"&gt;// join: wait for all three&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;)&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"score"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                           &lt;span class="c1"&gt;// reducer already summed them&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&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="s"&gt;"decision"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"APPROVED"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"REJECTED"&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;&lt;strong&gt;Execution&lt;/strong&gt; — three scorers run in parallel, each writing &lt;code&gt;score&lt;/code&gt;; the reducer merges the concurrent&lt;br&gt;
writes (summed to 90) so &lt;code&gt;decide&lt;/code&gt; reads one combined value. No shared mutable state, no ordering&lt;br&gt;
assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt; — the console renders the graph; a run lights up node by node, each node showing &lt;strong&gt;which&lt;br&gt;
executor ran it&lt;/strong&gt;, so a wide fan-out really runs in parallel on different machines:&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%2F0fc6s5zozfvsykp6k4bn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0fc6s5zozfvsykp6k4bn.jpg" alt="Per-node results, including which executor ran each node" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Edges beyond straight lines
&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;@DagNode&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="nd"&gt;@When&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#risk &amp;gt; 80"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"humanReview"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;otherwise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"fulfilment"&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;riskScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* writes risk */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@DagNode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subgraph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"fulfilment-flow"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"notify"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;                &lt;span class="c1"&gt;// nest a whole other DAG&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fulfilment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@DagNode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;@Shard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ids"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"squares"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"report"&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;Map&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;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;square&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DagState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* runs once per id, at run time */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;when&lt;/code&gt; + &lt;code&gt;otherwise&lt;/code&gt;&lt;/strong&gt;: route by a SpEL expression over the channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;trigger&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;ALL&lt;/code&gt; waits for every upstream edge; &lt;code&gt;ANY&lt;/code&gt; fires on the first arrival.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;subgraph&lt;/code&gt;&lt;/strong&gt;: a node is a whole other DAG, so you compose instead of copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Shard&lt;/code&gt;&lt;/strong&gt;: fan out dynamically, once per element of a list known only at run time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three ways to start it
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;By hand&lt;/strong&gt; from the console's &lt;em&gt;Trigger&lt;/em&gt; button, with an optional JSON seed for the input channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From a finished &lt;code&gt;@Task&lt;/code&gt;&lt;/strong&gt; in the same bean: its return value becomes the DAG input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a schedule&lt;/strong&gt; directly: pick &lt;em&gt;Trigger DAG&lt;/em&gt; in the &lt;em&gt;Create Task&lt;/em&gt; form.&lt;/li&gt;
&lt;/ul&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%2Fhogyyq9zih6kzkxqk146.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhogyyq9zih6kzkxqk146.jpg" alt="A completed run: the graph and how it was triggered" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration — channels &amp;amp; reducers
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;@Channel&lt;/code&gt; merges concurrent writes with a built-in reducer, or your own via &lt;code&gt;customReducer&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reducer&lt;/th&gt;
&lt;th&gt;Merge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SUM_INT&lt;/code&gt; / &lt;code&gt;SUM_LONG&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;add the numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MAX&lt;/code&gt; / &lt;code&gt;MIN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;keep the largest / smallest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AND&lt;/code&gt; / &lt;code&gt;OR&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;boolean fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CONCAT_LIST&lt;/code&gt; / &lt;code&gt;UNION_SET&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;append / union collections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CONCAT_STRING&lt;/code&gt; / &lt;code&gt;JOIN_CSV&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;join text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MERGE_MAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;merge maps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;LAST_WINS&lt;/code&gt; / &lt;code&gt;FIRST_WINS&lt;/code&gt; / &lt;code&gt;WRITE_ONCE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pick one writer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The executor points at the scheduler with &lt;code&gt;cronflow.client.server-urls&lt;/code&gt;; the server prefix is&lt;br&gt;
&lt;code&gt;cronflow.server.api-prefix&lt;/code&gt; (default &lt;code&gt;/cronflow&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations &amp;amp; trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The DAG engine rides on the &lt;strong&gt;cronflow add-on&lt;/strong&gt; over the scheduler cluster; it is not a standalone
workflow server. You run cronflower, then add &lt;code&gt;@Dag&lt;/code&gt; beans.&lt;/li&gt;
&lt;li&gt;Nodes are dispatched to executors over HTTP, so a node's bean must be &lt;strong&gt;reachable and idempotent&lt;/strong&gt;
enough to be retried.&lt;/li&gt;
&lt;li&gt;Channel values cross the wire between nodes, so keep them &lt;strong&gt;serializable and reasonably small&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It orchestrates &lt;em&gt;your&lt;/em&gt; steps; it is not a data-pipeline engine for streaming large datasets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A DAG is a Spring bean: &lt;strong&gt;&lt;code&gt;@Dag&lt;/code&gt; + &lt;code&gt;@DagNode&lt;/code&gt;&lt;/strong&gt;, edges via &lt;code&gt;to&lt;/code&gt;, data via typed &lt;strong&gt;channels&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reducers&lt;/strong&gt; merge concurrent writes, so joins need no locks or ordering assumptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Branching&lt;/strong&gt; (&lt;code&gt;when&lt;/code&gt;), &lt;strong&gt;join modes&lt;/strong&gt; (&lt;code&gt;ALL&lt;/code&gt;/&lt;code&gt;ANY&lt;/code&gt;), &lt;strong&gt;subgraphs&lt;/strong&gt;, and &lt;strong&gt;dynamic fan-out&lt;/strong&gt; (&lt;code&gt;@Shard&lt;/code&gt;) are annotation attributes.&lt;/li&gt;
&lt;li&gt;The engine &lt;strong&gt;drives the graph across the cluster&lt;/strong&gt;, dispatching each node to a live executor.&lt;/li&gt;
&lt;li&gt;Trigger a flow &lt;strong&gt;by hand, from a finished task, or on a schedule&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Every run is &lt;strong&gt;recorded&lt;/strong&gt; node by node, showing what ran where and what it produced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it: &lt;a href="https://github.com/paganini2008/cronflower" rel="noopener noreferrer"&gt;cronflower on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>distributedsystems</category>
      <category>dag</category>
      <category>jobscheduler</category>
    </item>
    <item>
      <title>Greenfinger 2.0: One url in, a searchable archive out</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:49:39 +0000</pubDate>
      <link>https://dev.to/paganini2008/greenfinger-20-one-url-in-a-searchable-archive-out-968</link>
      <guid>https://dev.to/paganini2008/greenfinger-20-one-url-in-a-searchable-archive-out-968</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Crawl the whole site. Keep every page, every picture, every version.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Search it by words, by meaning, or by describing a picture you remember.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Add a node and it goes faster. Kill one and it keeps going.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Nothing to install. Nothing to sign up for.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A distributed web crawler for the JVM. One pass writes plain files, a full text index and a vector&lt;br&gt;
collection. Every node runs the same jar, there is no coordinator to deploy, and the first crawl&lt;br&gt;
needs no database, no search server and no API key.&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%2Fguur37dz14wt6nkvteiv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fguur37dz14wt6nkvteiv.jpg" alt="Watching a crawl, live" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A rebuild in progress. v1 keeps answering searches while v2 is being written, and each node&lt;br&gt;
reports what it pulled.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What problem does it solve
&lt;/h2&gt;

&lt;p&gt;Most crawler code starts the same way. Someone needs the content of a site, writes a hundred lines&lt;br&gt;
around an http client and an html parser, and it works. Then reality arrives. One advert link and&lt;br&gt;
the crawler is downloading the rest of the web. The process is killed at page 40,000 and the queue&lt;br&gt;
was in memory. Half the site turns out to be pdfs. Navigation and cookie banners get indexed&lt;br&gt;
alongside the article, so every result looks the same.&lt;/p&gt;

&lt;p&gt;And then storing it is a second project: files here, an index there, embeddings somewhere else, and&lt;br&gt;
three pieces of glue that each fail differently. Greenfinger answers all of that in the product&lt;br&gt;
rather than in your code, and the first crawl needs nothing running beside it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/paganini2008/greenfinger.git
&lt;span class="nb"&gt;cd &lt;/span&gt;greenfinger/backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; mvn clean &lt;span class="nb"&gt;install
cd&lt;/span&gt; ../deploy

./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;demo crawl &lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://books.toscrape.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Crawling 'books.toscrape.com' from https://books.toscrape.com

  pages kept    1,000      urls seen   28,411      1 in every 28
  images        3,204      indexed      1,000      elapsed  0h 4m 12s

Finished: reached maxFetchSize
Catalog 01a0c3d5-7d3d-7000-81f6-76057a403db8, version 0, now searchable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./run-local.sh                &lt;span class="c"&gt;# nodes plus the page on http://localhost:9700&lt;/span&gt;
&lt;span class="nv"&gt;GF_NODES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3 ./run-local.sh     &lt;span class="c"&gt;# three nodes sharing one crawl&lt;/span&gt;
./run-docker.sh               &lt;span class="c"&gt;# the same, in containers&lt;/span&gt;
./run-local.sh stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sign in with &lt;code&gt;admin&lt;/code&gt; and the password in &lt;code&gt;deploy/config/api/users.xml&lt;/code&gt;. Seven example catalogs ship&lt;br&gt;
with a fresh install, so there is something to crawl before you have picked anything.&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%2F0umoplrxsgb67a77ro43.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0umoplrxsgb67a77ro43.jpg" alt="Catalogs" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Requirements
&lt;/h2&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;Version&lt;/th&gt;
&lt;th&gt;Needed for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;17 or later&lt;/td&gt;
&lt;td&gt;Running anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maven&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.9 or later&lt;/td&gt;
&lt;td&gt;Building from source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20 or later&lt;/td&gt;
&lt;td&gt;Building the web interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;any current&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;run-docker.sh&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything else is optional and opt in one variable at a time: PostgreSQL, MySQL, SQL Server,&lt;br&gt;
Oracle or SQLite instead of the H2 file, Elasticsearch instead of the embedded Lucene index, Qdrant&lt;br&gt;
or Weaviate instead of the embedded vector store, MinIO or any S3 compatible store instead of local&lt;br&gt;
disk, and Ollama or OpenAI instead of the local ONNX models.&lt;/p&gt;

&lt;p&gt;A browser is needed only for the &lt;code&gt;playwright&lt;/code&gt; and &lt;code&gt;selenium&lt;/code&gt; extractors. The default &lt;code&gt;adaptive&lt;/code&gt;&lt;br&gt;
falls back to HtmlUnit, which is pure Java and needs nothing installed.&lt;/p&gt;


&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;
&lt;h3&gt;
  
  
  One pass, three outputs
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            ┌──────────────┐
                            │  file        │  local disk, MinIO, any S3 store
   one crawl  ───────────►  ├──────────────┤
                            │  index       │  Lucene embedded, Elasticsearch
                            ├──────────────┤
                            │  vector      │  Lucene embedded, Qdrant, Weaviate, ES
                            └──────────────┘
                                   │
                    replay ────────┘   rebuild index or vectors from the files,
                                       without fetching the site again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The file layer is always on, because the database keeps metadata only and the other two rebuild&lt;br&gt;
from what it wrote. &lt;strong&gt;Search never reads the database&lt;/strong&gt;, and that single constraint is where the&lt;br&gt;
rest of the design comes from.&lt;/p&gt;
&lt;h3&gt;
  
  
  What one page goes through
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  url from the frontier
        │
        ├─ UrlPathAcceptor chain    domain, start url prefix, assets, robots.txt,
        │                           depth, path patterns. First refusal wins
        ├─ ExistingUrlPathFilter    seen before? RocksDB
        ├─ Extractor                plain http, browser only for unrendered shells
        ├─ ContentExtractor         the article, not the navigation
        │     └─ DocumentContentParser   when it is not html
        ├─ ContentDedupFilter       SHA-256 or SimHash
        ├─ OutputChannels           file, index, vector
        └─ new links ─────────────► back to the frontier, to whichever node owns them
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every drop is counted and named. That is why the monitor can say where 22,000 urls went rather than&lt;br&gt;
only that 140 pages were kept.&lt;/p&gt;
&lt;h3&gt;
  
  
  How the cluster shares one crawl
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   node A ── finds /a/b ──► node C owns it ──► fetches, finds /a/b/c ──► node A owns it ──► ...

   no central queue        no leader in the fetch path        join or leave mid crawl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A crawl is a recursive function, and the only thing distribution changes is that the recursive call&lt;br&gt;
crosses a process. There is no join, because a parent page does not care what its children found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completion is decided by everyone.&lt;/strong&gt; Every node checks the shared counters against&lt;br&gt;
&lt;code&gt;maxFetchSize&lt;/code&gt; and &lt;code&gt;fetchDuration&lt;/code&gt;, and the first to notice writes the reason. A leader that dies&lt;br&gt;
mid crawl cannot leave a crawl that never ends.&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%2Fhpctxx01zs3z5tljnqlz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhpctxx01zs3z5tljnqlz.jpg" alt="Members, roles, transport, health" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Code examples
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Crawl, update, rebuild, replay
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; A catalog id, and a verb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly crawl   &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c"&gt;# from the start url&lt;/span&gt;
./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly update  &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c"&gt;# urls that appeared since&lt;/span&gt;
./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly merge   &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c"&gt;# and revisit what is held&lt;/span&gt;
./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly rebuild &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c"&gt;# new version, old one served&lt;/span&gt;
./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly resume  &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c"&gt;# continue after a pause&lt;/span&gt;
./greenfinger-cli.sh &lt;span class="nt"&gt;--cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nightly replay  &lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;index+vector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output.&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;Verb&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Fetches&lt;/th&gt;
&lt;th&gt;Writes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crawl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current&lt;/td&gt;
&lt;td&gt;from the start url&lt;/td&gt;
&lt;td&gt;everything it saves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current&lt;/td&gt;
&lt;td&gt;only urls not seen before&lt;/td&gt;
&lt;td&gt;new pages only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;merge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current&lt;/td&gt;
&lt;td&gt;new urls &lt;strong&gt;and&lt;/strong&gt; pages already held&lt;/td&gt;
&lt;td&gt;only the pages that changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rebuild&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a new one&lt;/td&gt;
&lt;td&gt;the whole site again&lt;/td&gt;
&lt;td&gt;the new version, old one keeps serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resume&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current&lt;/td&gt;
&lt;td&gt;what is left on the frontier&lt;/td&gt;
&lt;td&gt;as the interrupted run would have&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a named one&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;rebuilds an output from what is stored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;replay&lt;/code&gt; is the one to remember. Your Elasticsearch was down for an hour, you changed the analyzer,&lt;br&gt;
or you decided six months in that you want embeddings after all. None of those need the site to be&lt;br&gt;
polite to you a second time.&lt;/p&gt;
&lt;h3&gt;
  
  
  Search three ways
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; One box, three modes, from the console or the prompt.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;greenfinger:&amp;gt; search --query="bread"
greenfinger:&amp;gt; search --query="what happens when a star runs out of fuel" --mode=meaning
greenfinger:&amp;gt; search --query="a bright spiral galaxy against black sky" --mode=pictures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output.&lt;/strong&gt; Words goes to the index, so this is exact terms with the matches highlighted.&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%2Fsb1jbf984lyr7m7r76dl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsb1jbf984lyr7m7r76dl.jpg" alt="Searching by words" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Meaning goes to the text vectors. The top answer below is a supernova remnants page that never&lt;br&gt;
contains the sentence that was typed.&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%2Ft77b6bg7gqr3ihgtpzf8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft77b6bg7gqr3ihgtpzf8.jpg" alt="Searching by meaning" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pictures goes to the image vectors, matched against the picture itself rather than the filename or&lt;br&gt;
the alt text.&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%2Fqoj16ft7f0znnvurkbmh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqoj16ft7f0znnvurkbmh.jpg" alt="Finding pictures by describing them" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same thing at the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╭────────┬──────────────────────────────────────────┬────────────────────────────────────────────────╮
│ Score  │ Title                                    │ Url                                            │
├────────┼──────────────────────────────────────────┼────────────────────────────────────────────────┤
│ 0.9356 │ APOD: 2008 June 4 - Chasing the ISS      │ https://apod.nasa.gov/apod/ap080604.html       │
│ 0.9317 │ APOD Index - Nebulae: Supernova Remnants │ https://apod.nasa.gov/apod/supernova_remnants… │
│ 0.9289 │ APOD Index - Stars: Binary Stars         │ https://apod.nasa.gov/apod/binary_stars.html   │
╰────────┴──────────────────────────────────────────┴────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The embedding models run locally and need no account: &lt;code&gt;multilingual-e5-small&lt;/code&gt; for text and&lt;br&gt;
&lt;code&gt;SigLIP 2&lt;/code&gt; for images, both ONNX, both preloaded at startup on a background thread.&lt;/p&gt;
&lt;h3&gt;
  
  
  Add a pdf parser
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; A site that links pdfs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; Every default component is &lt;code&gt;@ConditionalOnMissingBean&lt;/code&gt;, so publishing a bean is the whole&lt;br&gt;
registration. There is no plugin registry and no ordering property.&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;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;DocumentContentParser&lt;/span&gt; &lt;span class="nf"&gt;pdfParser&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;DocumentContentParser&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;Set&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;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fileTypes&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;Set&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="s"&gt;"pdf"&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;String&lt;/span&gt; &lt;span class="nf"&gt;extractText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&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="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Charset&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&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;Tika&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;parseToString&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;ByteArrayInputStream&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="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;&lt;strong&gt;Output.&lt;/strong&gt; With &lt;code&gt;GF_DOCUMENTS=true&lt;/code&gt; and &lt;code&gt;GF_DOCUMENT_TYPES=pdf&lt;/code&gt;, pdfs linked from a crawled page&lt;br&gt;
now contribute text to the index and the vectors. Collecting the links was always happening and&lt;br&gt;
costs nothing. Fetching and reading them is what you just switched on.&lt;/p&gt;

&lt;p&gt;The same shape works for every decision the crawler makes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Interface&lt;/th&gt;
&lt;th&gt;Decides&lt;/th&gt;
&lt;th&gt;What ships&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Extractor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How a page is fetched&lt;/td&gt;
&lt;td&gt;restclient, htmlunit, playwright, selenium, adaptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UrlPathAcceptor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether a link is followed&lt;/td&gt;
&lt;td&gt;Domain, start url, assets, robots.txt, depth, patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContentDedupFilter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether two urls are the same page&lt;/td&gt;
&lt;td&gt;sha256, simhash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ContentExtractor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The text inside a page&lt;/td&gt;
&lt;td&gt;Link density boilerplate removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DocumentContentParser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text out of a non html file&lt;/td&gt;
&lt;td&gt;text, markdown, csv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CompletionChecker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When the crawl is over&lt;/td&gt;
&lt;td&gt;Saved count, elapsed time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OutputChannel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where results are written&lt;/td&gt;
&lt;td&gt;file, index, vector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BlobStore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pages and images as bytes&lt;/td&gt;
&lt;td&gt;local, minio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Searcher&lt;/code&gt; / &lt;code&gt;VectorStore&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Full text, and meaning&lt;/td&gt;
&lt;td&gt;lucene, elasticsearch, qdrant, weaviate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EmbeddingClient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What turns text into a vector&lt;/td&gt;
&lt;td&gt;local ONNX, ollama, openai&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Embed it in your own application
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input.&lt;/strong&gt; A Spring Boot application of your own.&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;@EnableGreenfingerServer&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootApplication&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;MyApplication&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;&lt;strong&gt;Output.&lt;/strong&gt; The whole REST api, the login and the page, inside your process. It is explicit rather&lt;br&gt;
than auto configured, because sitting on a classpath is not a reason to open RocksDB and take a&lt;br&gt;
crawl permit. To drive a crawl without the server, &lt;code&gt;CrawlerLauncher.crawl(catalogId, onReady)&lt;/code&gt;&lt;br&gt;
returns a &lt;code&gt;CrawlerEngine.Result&lt;/code&gt; with the counters and the reason it ended.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Per catalog
&lt;/h3&gt;

&lt;p&gt;One url is required. Everything else has a default chosen to give a useful crawl of a site you know&lt;br&gt;
nothing about. Run &lt;code&gt;options&lt;/code&gt; at the prompt for the live values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;http:// or https://. The identity and the outer boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the domain&lt;/td&gt;
&lt;td&gt;Unique text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;other&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One of nine categories, used to filter and group&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;start-url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;= url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where fetching begins. Must sit under &lt;code&gt;url&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sitemap-url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;Empty discovers it from robots.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**.&amp;lt;domain&amp;gt;/**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ant path pattern, comma for several&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exclude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;Ant path pattern, comma for several&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;encoding&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UTF-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Page charset, when the server is wrong about it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;extractor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adaptive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;adaptive, restclient, htmlunit, playwright, selenium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max-size&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saved pages before the crawl stops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;depth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Link depth. -1 for no limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minutes before the crawl stops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Milliseconds between fetches, per node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retries per url&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;images&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether pictures are fetched at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output-types&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file+index+vector&lt;/code&gt;. file is always on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text+image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What reaches the index and the vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max-versions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Versions kept before the oldest is pruned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Node wide, in &lt;code&gt;deploy/.env&lt;/code&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_DB_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an H2 file&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL, SQL Server, Oracle, SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_INDEX_PROVIDER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lucene&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Or &lt;code&gt;elasticsearch&lt;/code&gt;, with &lt;code&gt;GF_ES_URIS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_VECTOR_STORE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lucene&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Or &lt;code&gt;elasticsearch&lt;/code&gt;, &lt;code&gt;qdrant&lt;/code&gt;, &lt;code&gt;weaviate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_FILE_TARGET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Or &lt;code&gt;minio&lt;/code&gt;. Object keys match the local paths exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_EMBEDDING_PROVIDER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Or &lt;code&gt;ollama&lt;/code&gt;, &lt;code&gt;openai&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_LUCENE_ANALYZER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;standard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;standard&lt;/code&gt;, &lt;code&gt;smartcn&lt;/code&gt; or &lt;code&gt;cjk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_ES_ANALYZER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;standard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ik_max_word&lt;/code&gt; needs the analysis-ik plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_DOCUMENTS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whether linked documents are fetched and read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_CLUSTER_TRANSPORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NETTY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Or the built in &lt;code&gt;NIO&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_NODES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many processes a launcher starts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GF_MEMORY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per container. Caps heap &lt;strong&gt;and&lt;/strong&gt; off heap together&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Analyzers matter more than they look. The standard analyzer cuts Chinese into single characters,&lt;br&gt;
and a Chinese analyzer drops French characters outright, so &lt;code&gt;chaîne&lt;/code&gt; comes back as &lt;code&gt;cha î ne&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Our own numbers only. There is no comparison here against other crawlers, because we have not run&lt;br&gt;
the controlled experiment that would make one honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test environment.&lt;/strong&gt; Apple M2 Max, 12 cores, 32 GB, macOS 26.3.1, JDK 17.0.12. Two nodes started&lt;br&gt;
by &lt;code&gt;run-local.sh&lt;/code&gt;, each &lt;code&gt;-Xms256m -Xmx2g&lt;/code&gt;. H2 file, embedded Lucene index, embedded Lucene vector&lt;br&gt;
store, pages and images on local disk, local ONNX embeddings. Home broadband. Polite crawling, so&lt;br&gt;
&lt;code&gt;fetchInterval&lt;/code&gt; is the floor on throughput rather than the hardware.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;Nodes&lt;/th&gt;
&lt;th&gt;Kept&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;th&gt;Urls seen&lt;/th&gt;
&lt;th&gt;Elapsed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;apod.nasa.gov&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;46 pages&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;3,773&lt;/td&gt;
&lt;td&gt;1m 41s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;simplefood.blog&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;140 pages&lt;/td&gt;
&lt;td&gt;2,489&lt;/td&gt;
&lt;td&gt;22,857 already known&lt;/td&gt;
&lt;td&gt;5m 01s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;books.toscrape.com&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1,000 pages&lt;/td&gt;
&lt;td&gt;3,204&lt;/td&gt;
&lt;td&gt;28,411&lt;/td&gt;
&lt;td&gt;4m 12s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap between urls seen and pages kept is the point rather than an inefficiency. On simplefood&lt;br&gt;
22,340 urls were filtered out by the boundary rules and 561 were duplicate content, which is work&lt;br&gt;
the outputs never had to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How evenly the work spreads.&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;two nodes, apod.nasa.gov
  node 18bbe738   76 handled   51%   26 pages   9 images
  node 96c82b18   74 handled   49%   19 pages   7 images

three nodes, a 61 page site, one shared PostgreSQL
  dispatched 61      handled 66      saved 61
  node-1: 18 fetches   node-2: 18   node-3: 25     each page exactly once
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Everything else we measured.&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;&lt;/th&gt;
&lt;th&gt;Measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Word search, embedded Lucene, 151 documents&lt;/td&gt;
&lt;td&gt;38 matches in 16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster wire format, one &lt;code&gt;CrawlTask&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;JSON 327 bytes, encode and decode 1,394 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both ONNX models preloaded&lt;/td&gt;
&lt;td&gt;4.47 s, on a background thread after the app is ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle RSS with both models loaded&lt;/td&gt;
&lt;td&gt;1.77 to 1.88 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image vector duplication, before and after the fix&lt;/td&gt;
&lt;td&gt;43× then 1.00×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container memory, vectors or a browser&lt;/td&gt;
&lt;td&gt;1g is OOMKilled, 2g passes, and they want separate runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;133 classes, 1,098 methods, 80% line coverage gate on four modules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Limitations and trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The embedded index and vector store cost the extracted text once per node.&lt;/strong&gt; Fine for one or
two nodes. For a real cluster, point it at Elasticsearch or Qdrant, which the startup report
recommends out loud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication is asynchronous.&lt;/strong&gt; Immediately after a crawl, one node can answer a search before
another has caught up. Seconds, not minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A node killed mid crawl leaves &lt;code&gt;runningState&lt;/code&gt; set.&lt;/strong&gt; The registry says nothing is running, the
row says otherwise, and &lt;code&gt;interrupt&lt;/code&gt; has nothing to interrupt. On the list to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article extraction leaves some template text in the chunks.&lt;/strong&gt; Share buttons and footer credits
turn up in search snippets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Politeness is the throughput ceiling.&lt;/strong&gt; &lt;code&gt;fetchInterval&lt;/code&gt; defaults to a second per node, and that
is deliberate. This is not the tool for hammering one host as fast as it will answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One crawl at a time per cluster.&lt;/strong&gt; Two crawls means two clusters, which is a name and a port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pdf, Word and Excel need a bean.&lt;/strong&gt; Each is another dependency with its own licence and its own
appetite for memory, so the choice belongs to the application.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One url in, and you get a whole site on disk, a full text index and vectors&lt;/strong&gt;, from one pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing to provision.&lt;/strong&gt; H2, an embedded Lucene index and local ONNX models are the defaults.
No database, no search server, no API key, no model download by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decentralised by default.&lt;/strong&gt; Every node runs the same jar. No coordinator, no central queue, no
leader in the fetch path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling is starting another process&lt;/strong&gt; and pointing it at the same cluster name, including
joining a crawl already running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay rebuilds an output from the files&lt;/strong&gt;, so a lost index or a changed analyzer never means
crawling the site again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versions make a rebuild safe.&lt;/strong&gt; The old version keeps answering searches, and an interrupted
rebuild publishes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three ways to search&lt;/strong&gt;, including finding a picture by describing it, with the models running
locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every decision the crawler makes is an interface&lt;/strong&gt; with a shipped default that a bean replaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The crawl cannot leave the site.&lt;/strong&gt; Two boundary rules, neither of which can be switched off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache 2.0, JDK 17, Spring Boot 4.1.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Source, issues and full documentation: &lt;a href="https://github.com/paganini2008/greenfinger" rel="noopener noreferrer"&gt;github.com/paganini2008/greenfinger&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>distributedsystems</category>
      <category>elasticsearch</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>EasyJPA: Criteria API Power in Three Lines, Not Thirty</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:41:02 +0000</pubDate>
      <link>https://dev.to/paganini2008/easyjpa-type-safe-dynamic-queries-in-three-lines-not-thirty-542e</link>
      <guid>https://dev.to/paganini2008/easyjpa-type-safe-dynamic-queries-in-three-lines-not-thirty-542e</guid>
      <description>&lt;h2&gt;
  
  
  1. Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;EasyJPA — Criteria API power. Three lines, not thirty.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;EasyJPA is a Spring Boot starter that puts a fluent, lambda-driven API over the JPA Criteria API.&lt;br&gt;
Every condition is a method reference, every join has a name, and not a single JPQL string is&lt;br&gt;
written. Joins, subqueries, grouping, pagination, updates and native SQL all compose in one chain&lt;br&gt;
you read top to bottom.&lt;/p&gt;

&lt;p&gt;Here is the entire pitch:&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;// ── The Criteria API ──────────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nc"&gt;CriteriaBuilder&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;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCriteriaBuilder&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;CriteriaQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cq&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;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&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="nc"&gt;Root&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cq&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="nc"&gt;User&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="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Predicate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;predicates&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;predicates&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="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"Jack"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;predicates&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="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&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="s"&gt;"password"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"123456"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;cq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;where&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;and&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predicates&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&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;Predicate&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="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cq&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getSingleResult&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ── EasyJPA ───────────────────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&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;FilterList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Jack"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPassword&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"123456"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;selectThis&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;one&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same query, same type safety, same generated SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;The Criteria API is the only type-safe way to build a dynamic JPA query, and almost nobody enjoys&lt;br&gt;
using it. A three-condition filter costs ten lines of &lt;code&gt;CriteriaBuilder&lt;/code&gt; plumbing; a multi-branch&lt;br&gt;
join means tracking &lt;code&gt;Join&amp;lt;?, ?&amp;gt;&lt;/code&gt; variables by hand; a correlated subquery is close to unreadable.&lt;br&gt;
So teams fall back on JPQL strings or Spring Data method names — and lose type safety, or&lt;br&gt;
composability, or both.&lt;/p&gt;

&lt;p&gt;The second problem is quieter and costs more. A paginated query is really &lt;em&gt;two&lt;/em&gt; statements, the&lt;br&gt;
listing one and the counting one, and they are written separately. They drift. And when the query&lt;br&gt;
has a &lt;code&gt;group by&lt;/code&gt;, the obvious &lt;code&gt;count(*)&lt;/code&gt; counts rows rather than groups, so the total is simply&lt;br&gt;
wrong. EasyJPA derives both statements from one definition, and counts groups through a derived&lt;br&gt;
table.&lt;/p&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keeps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Criteria's type safety and dynamic composition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Drops&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CriteriaBuilder&lt;/code&gt; / &lt;code&gt;Root&lt;/code&gt; / &lt;code&gt;Predicate[]&lt;/code&gt; boilerplate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fixes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;pagination counts that drift, and group counts that count rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  3. Quick Start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt; — the current version is &lt;code&gt;2.0.0-SNAPSHOT&lt;/code&gt;, in the Central Portal snapshot repository,&lt;br&gt;
which Maven reads from no project that has not named it:&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;com.github.paganini2008&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;easyjpa-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.0.0-SNAPSHOT&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;repositories&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;repository&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;id&amp;gt;&lt;/span&gt;central-portal-snapshots&lt;span class="nt"&gt;&amp;lt;/id&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;url&amp;gt;&lt;/span&gt;https://central.sonatype.com/repository/maven-snapshots/&lt;span class="nt"&gt;&amp;lt;/url&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;releases&amp;gt;&amp;lt;enabled&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/enabled&amp;gt;&amp;lt;/releases&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;snapshots&amp;gt;&amp;lt;enabled&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/enabled&amp;gt;&amp;lt;/snapshots&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/repository&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/repositories&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Name the provider:&lt;/strong&gt;&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;@EntityScan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"com.example.entity"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@EnableJpaRepositories&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repositoryFactoryBeanClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HibernateEntityDaoFactoryBean&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="n"&gt;basePackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"com.example.dao"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyBeanMethods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;JpaConfig&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;&lt;strong&gt;Extend &lt;code&gt;EntityDao&lt;/code&gt;:&lt;/strong&gt;&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;UserDao&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;EntityDao&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;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;&lt;code&gt;EntityDao&lt;/code&gt; &lt;strong&gt;is&lt;/strong&gt; a &lt;code&gt;JpaRepositoryImplementation&lt;/code&gt;, so &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;findById&lt;/code&gt; and &lt;code&gt;deleteAll&lt;/code&gt; are&lt;br&gt;
untouched. Query:&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getVip&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&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;selectThis&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;list&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 the whole setup. No properties to add, nothing to tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Requirements
&lt;/h2&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;Minimum&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;Spring Boot 3.1 – 3.5 takes the &lt;code&gt;1.0.x&lt;/code&gt; line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jakarta Persistence&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPA provider&lt;/td&gt;
&lt;td&gt;Hibernate 7.4&lt;/td&gt;
&lt;td&gt;or EclipseLink 5.0.1, or any Criteria implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build&lt;/td&gt;
&lt;td&gt;Maven 3.9&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The version line follows the Spring Boot line, and the two are maintained apart:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your Spring Boot&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4.0 and later&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;2.0.x&lt;/code&gt;, this one&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.1 – 3.5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.0.x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.0 and earlier&lt;/td&gt;
&lt;td&gt;not supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. How It Works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────────────┐
│  YOUR CODE        userDao.query().filter(...).sort(...).selectThis().list()  │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │  method references + aliases
┌──────────────────────────────────▼──────────────────────────────────────────┐
│  EASYJPA          Model ─────────── resolves an alias to a table            │
│                   Filter · Field · Column · JpaSort                         │
│                   JpaPageResultSet ─ one definition, two statements         │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │  "may I use a derived table here?"
┌──────────────────────────────────▼──────────────────────────────────────────┐
│  JpaProvider        Hibernate   │   EclipseLink   │   Standard              │
│                     supportsDerivedTable() · supportsRightJoin() · …        │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
┌──────────────────────────────────▼──────────────────────────────────────────┐
│  JAKARTA PERSISTENCE   CriteriaBuilder · CriteriaQuery · Root · Predicate    │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
┌──────────────────────────────────▼──────────────────────────────────────────┐
│  DATABASE    H2 · PostgreSQL · MySQL · SQL Server · SQLite · Oracle         │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanic&lt;/th&gt;
&lt;th&gt;In one line&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alias resolution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A lambda names an &lt;em&gt;entity&lt;/em&gt;, not a table; the alias is looked up inside the statement being built and never outside it — so an outer query and its subquery keep their tables apart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Two statements, one definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JpaPageResultSet&lt;/code&gt; holds the listing query and derives the counting query from it, so they cannot drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Providers are asked, not assumed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every optional feature is a &lt;code&gt;boolean&lt;/code&gt; on &lt;code&gt;JpaProvider&lt;/code&gt;, readable at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the pagination split, which is the part worth seeing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      customPage().join(...).filter(...).groupBy(...).select(...)
                              one definition
                                    │
                 ┌──────────────────┴──────────────────┐
                 ▼                                     ▼
            list(10, 0)                           rowCount()
                 │                                     │
   select … offset ? rows                   no group by → select count(1) from (…) d
          fetch first ? rows only              group by → select count(1) from
                                                          ( &amp;lt;the groups&amp;gt; ) d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Code Examples
&lt;/h2&gt;

&lt;p&gt;Every SQL block below is what Hibernate actually emitted, captured from the test suite against H2.&lt;br&gt;
The model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User ──&amp;lt; Order ──&amp;lt; OrderProduct &amp;gt;── Product        User.vip, Order.status, Order.totalPrice
                                        │          Product.name/price/location
                                      Stock        Stock.amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 1 — Nested conditions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — &lt;code&gt;vip or (username in ('Scott','Lee') and email is not null)&lt;/code&gt;, by username.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getVip&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;or&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;FilterList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&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="s"&gt;"Scott"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Lee"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;notNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getEmail&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&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;selectThis&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vip&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_user&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 2 — Four tables, two branches
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — order lines with their order, customer and product; only discounted products on paid or&lt;br&gt;
shipped orders; newest first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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="n"&gt;orderProductDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customPage&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getOrder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"o"&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="c1"&gt;// OrderProduct → Order&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"u"&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="c1"&gt;// Order        → User&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getProduct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"p"&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="c1"&gt;// OrderProduct → Product, second branch&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;FilterList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;notNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getDiscount&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"o"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"status"&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="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PAID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SHIPPED&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getOrderDate&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&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;ColumnList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&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;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getOrderDate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                        &lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"subtotal"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;p1_0&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;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order_product&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;example_order&lt;/span&gt;   &lt;span class="n"&gt;o1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;example_user&lt;/span&gt;    &lt;span class="n"&gt;u1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;example_product&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;first&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third join branches back off &lt;code&gt;OrderProduct&lt;/code&gt; rather than continuing from &lt;code&gt;User&lt;/code&gt;: the lambda&lt;br&gt;
carries its own entity, so the tree comes out the way it reads.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 3 — A grouping pagination that counts groups
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — best sellers: units sold, distinct orders and turnover per product, keeping products&lt;br&gt;
sold more than once. Then the total, for the page navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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;JpaPageResultSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tuple&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;resultSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderProductDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customPage&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;join&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getProduct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"p"&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="na"&gt;groupBy&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;FieldList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addFields&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;having&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1L&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&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;ColumnList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"soldAmount"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                        &lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;countDistinct&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"this"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order.id"&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderAmount"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                        &lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;multiply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                                &lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;OrderProduct:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;))).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"turnover"&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;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resultSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rowCount&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;SalesVo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resultSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTransformer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transformers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SalesVo&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="na"&gt;list&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output — the listing query&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="n"&gt;p1_0&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="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;distinct&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order_product&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;example_product&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
&lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;having&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;span class="k"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output — the counting query, derived from the same definition&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order_product&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;
    &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;example_product&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
    &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;having&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;derived1_0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One statement, &lt;code&gt;having&lt;/code&gt; honoured, no groups fetched. This is the bug you do not have to find.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 4 — A correlated subquery
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — the users who have ever ordered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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;JpaQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;JpaSubQuery&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;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;subQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&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="s"&gt;"o"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&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="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&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;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subQuery&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;selectThis&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vip&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_user&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subquery is created &lt;strong&gt;from&lt;/strong&gt; the outer query, which is what correlates the two. Negate it with&lt;br&gt;
&lt;code&gt;.not()&lt;/code&gt; for &lt;code&gt;not exists&lt;/code&gt;; swap &lt;code&gt;exists&lt;/code&gt; for &lt;code&gt;in&lt;/code&gt; and it becomes an &lt;code&gt;in&lt;/code&gt; subquery.&lt;/p&gt;
&lt;h3&gt;
  
  
  Example 5 — Joining an aggregate as a derived table
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — every product with its sales, without a correlated subquery per row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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;JpaQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tuple&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;productDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;customQuery&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;JpaSubQuery&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderProduct&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tuple&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderProduct&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="s"&gt;"op"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Tuple&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="n"&gt;sales&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;groupBy&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;FieldList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addFields&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"op"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"product.id"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&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;ColumnList&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"op"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"product.id"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"productId"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"op"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Integer&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="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"soldAmount"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                         &lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"op"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderAmount"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;joinSubQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"productId"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                                               &lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"this"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JpaSort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"soldAmount"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;select&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;ColumnList&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;Product:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
             &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addColumns&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Property&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"soldAmount"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"soldAmount"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTransformer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Transformers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;asCaseInsensitiveMap&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;select&lt;/span&gt; &lt;span class="n"&gt;p1_0&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;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;soldAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orderAmount&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_product&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="n"&gt;c0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;
      &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order_product&lt;/span&gt; &lt;span class="n"&gt;op1_0&lt;/span&gt;
      &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;c0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;soldAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orderAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;desc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 6 — Update by an expression, delete by a subquery
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input&lt;/strong&gt; — shout a username; decrement a stock; drop the users who never ordered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt;&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="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setField&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;concat&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upper&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&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="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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Lee"&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;stockDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setField&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Stock:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Fields&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;minusValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Stock:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1L&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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Stock:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getAmount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0L&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="nc"&gt;JpaDelete&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userDao&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="nc"&gt;JpaSubQuery&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;,&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="n"&gt;subQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&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="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eq&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Order:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;User:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;delete&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="nc"&gt;Restrictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subQuery&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;not&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;&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;update&lt;/span&gt; &lt;span class="n"&gt;example_user&lt;/span&gt;  &lt;span class="n"&gt;u1_0&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;||?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="k"&gt;update&lt;/span&gt; &lt;span class="n"&gt;example_stock&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;s1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;

&lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_user&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;example_order&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;o1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;u1_0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The rest of the surface, in one table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want&lt;/th&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The entities&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dao.query()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Columns into a VO&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dao.query(Vo.class)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any columns&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dao.customQuery()&lt;/code&gt; → &lt;code&gt;Tuple&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…with a total count&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dao.page()&lt;/code&gt; · &lt;code&gt;dao.customPage()&lt;/code&gt; → &lt;code&gt;JpaPageResultSet&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fetch an association&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch(Order::getUser)&lt;/code&gt; · &lt;code&gt;leftFetch(User::getOrders).distinct()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CASE WHEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;new IfExpression&amp;lt;&amp;gt;(attr).when(a, b).otherwise(c)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date parts&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Fields.year/month/day(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Any database function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Function.build("LOWER", String.class, Product::getName)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A result shape&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Transformers.asBean/asMap/asCaseInsensitiveMap/asList/noop&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native SQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dao.queryForMap(sql, args)&lt;/code&gt; — keeps &lt;code&gt;rowCount()&lt;/code&gt; and &lt;code&gt;paginate()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  7. Configuration
&lt;/h2&gt;

&lt;p&gt;EasyJPA has &lt;strong&gt;no properties of its own&lt;/strong&gt; — no &lt;code&gt;spring.easyjpa.*&lt;/code&gt; namespace. What you configure is&lt;br&gt;
the provider and, for three databases, the driver.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Provider&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@EnableJpaRepositories(repositoryFactoryBeanClass = …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HibernateEntityDaoFactoryBean&lt;/code&gt; (default) · &lt;code&gt;EclipseLinkEntityDaoFactoryBean&lt;/code&gt; · &lt;code&gt;StandardEntityDaoFactoryBean&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EclipseLink&lt;/td&gt;
&lt;td&gt;&lt;code&gt;spring.autoconfigure.exclude&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;…orm.jpa.HibernateJpaAutoConfiguration&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;JDBC url&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;calcBigDecimalPrecision=true&lt;/code&gt; — otherwise every &lt;code&gt;BigDecimal&lt;/code&gt; binds as &lt;code&gt;decimal(38,0)&lt;/code&gt; and &lt;code&gt;0.90&lt;/code&gt; reads back as &lt;code&gt;1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;entity id&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GenerationType.IDENTITY&lt;/code&gt; — &lt;code&gt;AUTO&lt;/code&gt; needs a sequence table SQLite locks against&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle 23+ on Hibernate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OracleDialect&lt;/code&gt; subclass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DatabaseVersion.make(21)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  8. Comparison
&lt;/h2&gt;

&lt;p&gt;Feature-level. No timings are claimed — nothing here is a benchmark.&lt;/p&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;EasyJPA&lt;/th&gt;
&lt;th&gt;Criteria API&lt;/th&gt;
&lt;th&gt;Spring Data &lt;code&gt;Specification&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;QueryDSL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type-safe attributes&lt;/td&gt;
&lt;td&gt;method references&lt;/td&gt;
&lt;td&gt;metamodel or strings&lt;/td&gt;
&lt;td&gt;metamodel or strings&lt;/td&gt;
&lt;td&gt;generated &lt;code&gt;Q&lt;/code&gt; classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code generation step&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;annotation processor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dynamic composition&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;manual &lt;code&gt;Predicate[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-branch joins&lt;/td&gt;
&lt;td&gt;one call each&lt;/td&gt;
&lt;td&gt;track &lt;code&gt;Join&amp;lt;?,?&amp;gt;&lt;/code&gt; by hand&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correlated subqueries&lt;/td&gt;
&lt;td&gt;built from the outer query&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;awkward&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination count over &lt;code&gt;group by&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;derived table, one statement&lt;/td&gt;
&lt;td&gt;write it yourself&lt;/td&gt;
&lt;td&gt;write it yourself&lt;/td&gt;
&lt;td&gt;write it yourself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Derived-table join&lt;/td&gt;
&lt;td&gt;&lt;code&gt;joinSubQuery(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native SQL, pagination kept&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;separate API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider gaps discoverable at runtime&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;JpaProvider&lt;/code&gt; flags&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Provider and database coverage
&lt;/h3&gt;

&lt;p&gt;The same 202 tests run on three providers against six databases.&lt;/p&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;H2 2.3&lt;/th&gt;
&lt;th&gt;PostgreSQL 16&lt;/th&gt;
&lt;th&gt;MySQL 9.6&lt;/th&gt;
&lt;th&gt;SQL Server 16&lt;/th&gt;
&lt;th&gt;SQLite 3.49&lt;/th&gt;
&lt;th&gt;Oracle 23&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hibernate&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 1&lt;/td&gt;
&lt;td&gt;⚠️ 3&lt;/td&gt;
&lt;td&gt;⚠️ 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Criteria API alone&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ 1&lt;/td&gt;
&lt;td&gt;⚠️ 3&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EclipseLink&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ no platform&lt;/td&gt;
&lt;td&gt;⚠️ 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each ⚠️ belongs to the database or to the provider on that database, never to a query EasyJPA built.&lt;br&gt;
The README names every one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Limitations &amp;amp; Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aliases are strings.&lt;/strong&gt; A typo in &lt;code&gt;"op"&lt;/code&gt; is a runtime error, not a compile error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not auto-configured.&lt;/strong&gt; You name the factory bean yourself; there is no &lt;code&gt;@AutoConfiguration&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Criteria API is the ceiling.&lt;/strong&gt; Window functions, CTEs and &lt;code&gt;UNION&lt;/code&gt; need native SQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EclipseLink reaches less than Hibernate.&lt;/strong&gt; No derived table, no right join, no subquery as a
selected column — all of it reported through &lt;code&gt;JpaProvider&lt;/code&gt; rather than failed over silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fetched collection paginates in memory.&lt;/strong&gt; Keep paginated fetches to to-one associations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No reactive support.&lt;/strong&gt; Blocking JDBC only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a good fit&lt;/strong&gt; for a project that writes mostly static queries — Spring Data method names are
shorter for those.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10. Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One chain instead of ten lines.&lt;/strong&gt; &lt;code&gt;query().filter().sort().select().list()&lt;/code&gt; replaces the whole
&lt;code&gt;CriteriaBuilder&lt;/code&gt; / &lt;code&gt;Root&lt;/code&gt; / &lt;code&gt;Predicate[]&lt;/code&gt; ritual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method references throughout&lt;/strong&gt;, so a renamed field is a compile error rather than a runtime
surprise. No code generation, no annotation processor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Joins branch by themselves.&lt;/strong&gt; A lambda carries its own entity, so a four-table, two-branch join
is four calls that read in order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pagination is one definition, two statements.&lt;/strong&gt; The counting query is derived from the
listing query and cannot drift from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A grouping pagination counts groups&lt;/strong&gt;, through a derived table, in one statement, &lt;code&gt;having&lt;/code&gt;
included — the count most hand-written pagination gets wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subqueries correlate themselves&lt;/strong&gt; because they are created from the query that uses them.
Filter, column, comparison or nested — all the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregates join once&lt;/strong&gt;, as a derived table, instead of running a correlated subquery per row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider gaps are a &lt;code&gt;boolean&lt;/code&gt;, not a stack trace.&lt;/strong&gt; &lt;code&gt;JpaProviders.getProvider().supportsXxx()&lt;/code&gt;
answers before you build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested where it matters&lt;/strong&gt; — 202 tests, three providers, six databases, and every documented
failure attributed to the database or the provider rather than hidden.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero configuration.&lt;/strong&gt; No properties; name one factory bean and extend &lt;code&gt;EntityDao&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt; — &lt;a href="https://github.com/paganini2008/easyjpa" rel="noopener noreferrer"&gt;paganini2008/easyjpa&lt;/a&gt;, MIT licensed.&lt;br&gt;
Spring Boot 4 takes the &lt;code&gt;2.0.x&lt;/code&gt; line, Spring Boot 3 the &lt;code&gt;1.0.x&lt;/code&gt; line.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>jpa</category>
      <category>hibernate</category>
      <category>orm</category>
    </item>
    <item>
      <title>Cronsmith: Stop hand-writing cron: build and parse it in Java</title>
      <dc:creator>Fred Feng</dc:creator>
      <pubDate>Sun, 20 Sep 2026 08:55:55 +0000</pubDate>
      <link>https://dev.to/paganini2008/stop-hand-writing-cron-build-and-parse-it-in-java-3gkj</link>
      <guid>https://dev.to/paganini2008/stop-hand-writing-cron-build-and-parse-it-in-java-3gkj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Cronsmith&lt;/strong&gt; is a Java library that builds, parses and runs cron through a fluent, object-oriented API&lt;br&gt;
instead of hand-typed strings. You describe &lt;em&gt;when&lt;/em&gt; in plain method calls and get back a cron expression,&lt;br&gt;
the exact date-times it fires at, or a running task — across Quartz, Spring, AWS EventBridge and crontab.&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;// "the last Friday of every month at 18:00" — readable, and it tells you the next fire&lt;/span&gt;
&lt;span class="nc"&gt;CronExpression&lt;/span&gt; &lt;span class="n"&gt;cron&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;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastDayOfWeek&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DayOfWeek&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FRIDAY&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;()).&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&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="n"&gt;cron&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="c1"&gt;// 0 0 18 ? * FRIL&lt;/span&gt;
&lt;span class="n"&gt;cron&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNextFiredDateTime&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// the next last-Friday-of-the-month, 18:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What problem does it solve?
&lt;/h2&gt;

&lt;p&gt;Quick, what does &lt;code&gt;0 0 18 ? * FRIL&lt;/code&gt; fire at? Cron strings are write-only: easy to fumble, hard to review,&lt;br&gt;
and impossible to unit-test by eye. Worse, "cron" isn't one language — Quartz, Spring, AWS EventBridge&lt;br&gt;
and Unix crontab disagree on fields, on whether &lt;code&gt;1&lt;/code&gt; is Sunday or Monday, and on which of &lt;code&gt;L&lt;/code&gt; / &lt;code&gt;#&lt;/code&gt; / &lt;code&gt;W&lt;/code&gt;&lt;br&gt;
they even support. Copy a Quartz line into crontab and it can silently fire at a different time.&lt;/p&gt;

&lt;p&gt;Cronsmith makes the schedule &lt;strong&gt;code you can read, iterate and port&lt;/strong&gt;: build it with method calls, print&lt;br&gt;
it for the target you deploy to, and ask it for its next fire times before you trust it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick start
&lt;/h2&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;com.github.paganini2008&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;cronsmith&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;1.0.0-SNAPSHOT&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CronExpression&lt;/span&gt; &lt;span class="n"&gt;weekdays9&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;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyWeek&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;Mon&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toFri&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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="n"&gt;weekdays9&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="c1"&gt;// 0 0 9 ? * MON-FRI&lt;/span&gt;
&lt;span class="n"&gt;weekdays9&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNextFiredDateTime&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// next weekday at 09:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JDK 17+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The core parser's only runtime dependency is &lt;strong&gt;ANTLR&lt;/strong&gt; — no Spring, HTTP or database.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;CronBuilder&lt;/code&gt; builds a &lt;code&gt;CronExpression&lt;/code&gt; (an iterable, serializable model of the schedule). From there&lt;br&gt;
you render it for a dialect, or walk its fire times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CronBuilder  ──build──▶  CronExpression  ──┬── toString / toQuartz / toAws / toUnix  (render)
   or CRON.parse(str) ───────────────▶     ├── consume(..) / getNextFiredDateTime()  (iterate)
                                            └── serialize() / deserialize(..)         (store)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build — describe it, don't assemble strings
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Input → Output:&lt;/strong&gt;&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;new&lt;/span&gt; &lt;span class="nc"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everySecond&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="c1"&gt;// */5 * * * * ?&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;andDay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;andLastDay&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyHour&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;everyMinute&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="c1"&gt;// 0 */5 */2 10,15,L * ?&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;dayOfWeek&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DayOfWeek&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SATURDAY&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;everyHour&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 0 0 */2 ? * SAT#3 (3rd Sat)&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastDay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;23&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="c1"&gt;// 0 30 23 L-3 * ?&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;latestWeekday&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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;// 0 0 9 15W * ? (nearest weekday to 15th)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Look at a schedule instead of trusting it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setStartTime&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&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="mi"&gt;2027&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;atStartOfDay&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;everyMonth&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;latestWeekday&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consume&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;out&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;println&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="c1"&gt;// 2027-01-15T09:00 … 2027-05-14T09:00  &amp;lt;- the 15th is a Saturday, so it moves to the nearest weekday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parse a string back into a model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;CRON&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;"*/5 * * * *"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 0 */5 * * * ?      a crontab line, seconds filled in&lt;/span&gt;
&lt;span class="no"&gt;CRON&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;"0 9 * * 1-5"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 0 0 9 ? * MON-FRI  crontab: 1 is Monday&lt;/span&gt;
&lt;span class="no"&gt;CRON&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;"0 0 12 ? * 1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0 0 12 ? * SUN     Quartz:  1 is Sunday&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field count picks the reading: &lt;strong&gt;5&lt;/strong&gt; = crontab, &lt;strong&gt;6&lt;/strong&gt; = Quartz without a year, &lt;strong&gt;7&lt;/strong&gt; = Quartz with a year.&lt;/p&gt;

&lt;h3&gt;
  
  
  YCRON — schedules month-based cron can't say
&lt;/h3&gt;

&lt;p&gt;Seven fields: &lt;code&gt;&amp;lt;sec&amp;gt; &amp;lt;min&amp;gt; &amp;lt;hour&amp;gt; &amp;lt;day-of-week&amp;gt; &amp;lt;week-of-year&amp;gt; &amp;lt;day-of-year&amp;gt; [&amp;lt;year&amp;gt;]&lt;/code&gt;.&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;new&lt;/span&gt; &lt;span class="nc"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// 0 0 12 ? ? 100 2026  (100th day of the year)&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;week&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;Mon&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 0 0 9 MON 20 ? 2026  (Mon of ISO week 20)&lt;/span&gt;
&lt;span class="no"&gt;YCRON&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;"0 0 12 ? ? 100 2026"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getCronType&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// CronType.YCRON (its own grammar)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Turn a plain interval into cron, or run it in-process
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="no"&gt;CRON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setInterval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PT30S"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                    &lt;span class="c1"&gt;// every 30s (PT1H30M / sub-second are rejected, not rounded)&lt;/span&gt;

&lt;span class="nc"&gt;ScheduledExecutorService&lt;/span&gt; &lt;span class="n"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newScheduledThreadPool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;CronFuture&lt;/span&gt; &lt;span class="n"&gt;f&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;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everySecond&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="na"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;runTask&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;doWork&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// run 10x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration — syntax reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; sec min hour day-of-month month day-of-week [year]
  *   *   *       *           *        ?        *
 day-of-month: 1-31, L, LW, L-n, nW, ?     day-of-week: 1-7 (SUN=1) or SUN-SAT, nL, n#m, ?
 YCRON: &amp;lt;sec&amp;gt; &amp;lt;min&amp;gt; &amp;lt;hour&amp;gt; &amp;lt;day-of-week&amp;gt; &amp;lt;week-of-year&amp;gt; &amp;lt;day-of-year&amp;gt; [&amp;lt;year&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&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;L&lt;/code&gt; / &lt;code&gt;L-n&lt;/code&gt; / &lt;code&gt;LW&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;last day / &lt;code&gt;n&lt;/code&gt; days before last / last weekday of the month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nW&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;weekday nearest the &lt;code&gt;n&lt;/code&gt;th, without leaving the month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dow&amp;gt;L&lt;/code&gt; / &lt;code&gt;&amp;lt;dow&amp;gt;#n&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;last &lt;code&gt;&amp;lt;dow&amp;gt;&lt;/code&gt; (&lt;code&gt;FRIL&lt;/code&gt;) / &lt;code&gt;n&lt;/code&gt;th &lt;code&gt;&amp;lt;dow&amp;gt;&lt;/code&gt; (&lt;code&gt;TUE#2&lt;/code&gt;, skipped in months without an &lt;code&gt;n&lt;/code&gt;th)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;a-b&lt;/code&gt; / &lt;code&gt;a/n&lt;/code&gt; / &lt;code&gt;a,b&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;range / step / list (entries may themselves be ranges or steps)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How it compares across schedulers
&lt;/h2&gt;

&lt;p&gt;One schedule, printed for whatever you deploy to. Unsupported features are &lt;strong&gt;reported, not silently&lt;br&gt;
rewritten&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;Schedule&lt;/th&gt;
&lt;th&gt;Quartz&lt;/th&gt;
&lt;th&gt;Spring&lt;/th&gt;
&lt;th&gt;AWS EventBridge&lt;/th&gt;
&lt;th&gt;Unix crontab&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;weekdays 09:00&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 9 ? * MON-FRI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 9 ? * MON-FRI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 9 ? * MON-FRI *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 9 * * MON-FRI&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;every 15 seconds&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*/15 * * * * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;*/15 * * * * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no seconds field&lt;/td&gt;
&lt;td&gt;no seconds field&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;last day of month&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 59 23 L * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 59 23 L * ?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;59 23 L * ? *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no &lt;code&gt;L&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd Tuesday&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 10 ? * TUE#2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 0 10 ? * TUE#2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 10 ? * TUE#2 *&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restricted to 2027-2029&lt;/td&gt;
&lt;td&gt;&lt;code&gt;… 2027-2029&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no year field&lt;/td&gt;
&lt;td&gt;&lt;code&gt;… 2027-2029&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no year field&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CronExpression&lt;/span&gt; &lt;span class="n"&gt;daily&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;CronBuilder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;everyDay&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&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="no"&gt;CRON&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toUnixString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;daily&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 30 9 * * *   (throws if the schedule needs L/#/seconds crontab lacks)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations &amp;amp; trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is a &lt;strong&gt;cron builder / parser / in-process runner&lt;/strong&gt;, not a distributed job system. For persistent,
clustered scheduling with retries and a console, use &lt;code&gt;cronsmith-spring-boot-starter&lt;/code&gt; in
&lt;a href="https://github.com/paganini2008/cronflower" rel="noopener noreferrer"&gt;cronflower&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Rendering is &lt;strong&gt;honest, not lossy&lt;/strong&gt;: a schedule a target can't express &lt;strong&gt;throws&lt;/strong&gt; instead of quietly
changing when it fires.&lt;/li&gt;
&lt;li&gt;ISO-8601 intervals must be an exact multiple of one unit (&lt;code&gt;PT1H30M&lt;/code&gt;, &lt;code&gt;PT90M&lt;/code&gt;, sub-second are rejected).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Build cron with a &lt;strong&gt;fluent, readable API&lt;/strong&gt;; &lt;code&gt;toString()&lt;/code&gt; for the string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt; any dialect back into an inspectable, iterable &lt;code&gt;CronExpression&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port&lt;/strong&gt; one schedule to Quartz / Spring / AWS / crontab; unsupported features are reported.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YCRON&lt;/strong&gt; expresses year-, week- and day-of-year schedules classic cron can't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate&lt;/strong&gt; the real fire times (&lt;code&gt;consume&lt;/code&gt; / &lt;code&gt;getNextFiredDateTime&lt;/code&gt;) before you trust a schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serializable&lt;/strong&gt;, with an in-process timing-wheel scheduler for simple runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get it: &lt;a href="https://github.com/paganini2008/cronflower" rel="noopener noreferrer"&gt;cronsmith&lt;/a&gt; · JDK 17+, Apache 2.0.&lt;/p&gt;

</description>
      <category>cronexpression</category>
      <category>crontab</category>
      <category>anltr</category>
      <category>jobscheduler</category>
    </item>
  </channel>
</rss>
