<?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: Solon Framework</title>
    <description>The latest articles on DEV Community by Solon Framework (@solonjava).</description>
    <link>https://dev.to/solonjava</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%2F4003833%2F83933c1e-7d66-44d1-9237-16669f6b9a80.png</url>
      <title>DEV Community: Solon Framework</title>
      <link>https://dev.to/solonjava</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/solonjava"/>
    <language>en</language>
    <item>
      <title>No JSON Required: Configuring SolonCode's Model from the Web Settings Panel</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:42:39 +0000</pubDate>
      <link>https://dev.to/solonjava/no-json-required-configuring-soloncodes-model-from-the-web-settings-panel-ceo</link>
      <guid>https://dev.to/solonjava/no-json-required-configuring-soloncodes-model-from-the-web-settings-panel-ceo</guid>
      <description>&lt;p&gt;Most coding agents make you earn your first conversation. You install the binary, then you go hunting for a config file, then you find out the config file wants a provider name you have to spell exactly right, a base URL that may or may not need a trailing path segment, and a key you paste into a text editor and hope you didn't leave a trailing space on.&lt;/p&gt;

&lt;p&gt;SolonCode skips that. You start it in web mode, the browser opens, and you add your model in a settings panel — paste the endpoint, paste the key, hit test, save. The chat area picks up the new model immediately. There is a &lt;code&gt;settings.json&lt;/code&gt; underneath, and you can read it if you're curious, but on the way to your first task you never need to open it.&lt;/p&gt;

&lt;p&gt;Here's the whole path, with the screens you'll actually see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java 8 or newer.&lt;/strong&gt; SolonCode runs on Java 8 through Java 26, so whatever JDK your project already builds against is almost certainly fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One reachable model endpoint.&lt;/strong&gt; OpenAI, Anthropic, Gemini, Ollama, or anything speaking a compatible format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optional, and only if you want the matching features later: Node.js (for some MCP servers, Skills, and browser-type tools), Git (so the web UI can show you diffs and staging), and a language server (for LSP-backed code understanding). None of these block your first run.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS / Linux / Harmony PC&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://solon.noear.org/soloncode/setup.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Windows PowerShell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://solon.noear.org/soloncode/setup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the same command later is how you update. The installer refreshes program files and does its best to preserve what you've already configured — &lt;code&gt;settings.json&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, and friends stay put.&lt;/p&gt;

&lt;p&gt;Confirm it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode version
&lt;span class="c"&gt;# SolonCode v2026.8.4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start web mode
&lt;/h2&gt;

&lt;p&gt;Go to the project you actually want to work on. The directory you launch from becomes the workspace, and that matters for scoping later, so &lt;code&gt;cd&lt;/code&gt; first rather than launching from your home directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
soloncode web 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ways to pick a port:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default, 4808&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Random free port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;soloncode web 1212&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;That specific port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;web 0&lt;/code&gt; is the one to reach for when you keep several projects open, because you're not going to collide with an instance you forgot about. The browser tab is labeled with the workspace name and path, which is how you tell four open tabs apart.&lt;/p&gt;

&lt;p&gt;The terminal prints the address it picked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SolonCode v2026.8.4 PID-4131 Model:your-model
/path/to/your-project
Web interface: http://localhost:1212/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser opens on its own. If it doesn't — remote box, headless environment, an OS that decided not to cooperate — copy the address from that output.&lt;/p&gt;

&lt;p&gt;What you land on is the welcome screen: an input box in the middle, the workspace file tree on the right, conversation controls on the left, and the current model shown under the input.&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%2Fe2m8r29hercjz3x03p78.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%2Fe2m8r29hercjz3x03p78.png" alt="SolonCode welcome screen" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First stop: set your language
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; in the left rail, and the first card under &lt;strong&gt;General&lt;/strong&gt; is &lt;strong&gt;Display Language&lt;/strong&gt;. It ships as &lt;em&gt;Follow system default&lt;/em&gt;, which means the UI speaks whatever your OS does.&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%2F4jpxpyqh63i7z3kk3fhx.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%2F4jpxpyqh63i7z3kk3fhx.png" alt="Display language dropdown" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twenty-three locales in that list. Pick yours and the interface re-renders on the spot — no restart, no reload. Menus, section names, buttons, placeholder text, all of 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%2F3ghg3oh3acyn8scjzzm8.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%2F3ghg3oh3acyn8scjzzm8.png" alt="Settings panel in English" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things worth knowing that the dropdown doesn't tell you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setting is per-session.&lt;/strong&gt; Selecting a language applies it immediately, but reloading the page drops you back to &lt;em&gt;Follow system default&lt;/em&gt;. If your OS locale already matches what you want, you'll never notice. If it doesn't, expect to re-pick after a reload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It changes the UI, not the agent.&lt;/strong&gt; Display Language controls chrome. The language your model &lt;em&gt;answers&lt;/em&gt; in comes from the model, and it will happily reply in one language to a prompt written in another. If you want English answers, ask for English answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reply in English. &amp;lt;your actual request&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or put it in &lt;code&gt;AGENTS.md&lt;/code&gt; once and stop repeating yourself.&lt;/p&gt;

&lt;p&gt;The rest of General, for when you come back to it: Conversation Strategy, Sandbox Mode, Memory, Retry on Failure, and Appearance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The settings panel
&lt;/h2&gt;

&lt;p&gt;Nine sections down the side:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;General · Agents · Mounts · Skills · Providers · Models · MCP Servers · OpenAPI Servers · LSP Servers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You want &lt;strong&gt;Models&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One small thing that will bite you if nobody says it: &lt;strong&gt;Esc closes the whole settings panel&lt;/strong&gt;, not just the form you have open. Use the panel's own &lt;strong&gt;Back&lt;/strong&gt; button when you mean to back out of a form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add your first model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Models → + Add Model&lt;/strong&gt;. This opens as a full page inside the panel, not a popup dialog:&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%2Fz2wb7rn4qqgkovqsyydt.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%2Fz2wb7rn4qqgkovqsyydt.png" alt="Add model form" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt; — how it shows up in the model picker. Free text, so name it something you'll recognize at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt; — two buttons, &lt;strong&gt;User (Global)&lt;/strong&gt; and &lt;strong&gt;Workspace (Local)&lt;/strong&gt;. More on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL mode&lt;/strong&gt; — a dropdown, default &lt;em&gt;Full URL (auto-detect)&lt;/em&gt;. The alternatives are BaseUrl modes for OpenAI Chat Completions, OpenAI Responses, and Anthropic, plus dedicated Gemini and Ollama entries. Auto-detect covers the common cases; reach for a specific mode when your endpoint isn't a full completions URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API URL&lt;/strong&gt; — placeholder shows the shape it wants, e.g. &lt;code&gt;https://api.deepseek.com/v1/chat/completions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Key&lt;/strong&gt; — &lt;code&gt;sk-...&lt;/code&gt;, with a show/hide toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; — the provider's model identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context length&lt;/strong&gt; — a dropdown of common sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout&lt;/strong&gt; — 120s by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extra options&lt;/strong&gt; — a JSON field for things like &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;top_p&lt;/code&gt;, or reasoning flags, with a &lt;strong&gt;Format JSON&lt;/strong&gt; button. Empty is a perfectly good answer here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set as default model&lt;/strong&gt; — a checkbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filled in against a local Ollama instance:&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%2Fwgxxtmhh5g1aywh6z7rb.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%2Fwgxxtmhh5g1aywh6z7rb.png" alt="Add model form, filled" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the part people skip: &lt;strong&gt;press Test Connection before you press Save.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A model that's added but unreachable looks identical to a model that works, right up until your first task dies mid-run and you're reading logs to figure out whether the problem was your key, your URL, or your proxy. The test button is right next to Save. Ten seconds there removes a whole category of confusion.&lt;/p&gt;

&lt;p&gt;After you save, the model list in the chat area refreshes to match — no restart. If you've accumulated a long list, the picker supports search filtering, so you type a fragment instead of scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global or workspace
&lt;/h2&gt;

&lt;p&gt;The scope buttons in the form decide which file your model lands in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User (Global)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.soloncode/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your everyday model, shared across every project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workspace (Local)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.soloncode/settings.json&lt;/code&gt; in the project&lt;/td&gt;
&lt;td&gt;Models specific to this project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SolonCode reads the user-level file first, then the workspace file, and workspace config overrides or supplements what it found.&lt;/p&gt;

&lt;p&gt;Workspace scope has a second-order effect worth thinking about for a second. That file sits inside the project directory, which means it can be committed. Sharing endpoint and model choices with your team that way is genuinely useful. Sharing an API key that way is not. Decide which of those you're doing before you &lt;code&gt;git add&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providers, when one key serves many models
&lt;/h2&gt;

&lt;p&gt;If you're adding a second and third model against the same endpoint, the &lt;strong&gt;Providers&lt;/strong&gt; section is the better place to work. It manages vendor config — API address, keys — as one entry, with add, modify, and enable/disable per provider.&lt;/p&gt;

&lt;p&gt;The reason to care is maintenance rather than setup. When a key rotates, you want one place to change it, not four model entries that each remember their own copy. Same story when you're moving an endpoint. Set the provider up once; point models at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First conversation
&lt;/h2&gt;

&lt;p&gt;Don't lead with a code change. Lead with a question, because the answer tells you whether the model is actually seeing your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reply in English. What kind of project is this directory?
Check the build file and source layout before answering.
&lt;/code&gt;&lt;/pre&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%2Fx5gdeqjyd5lx6vmtie1j.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%2Fx5gdeqjyd5lx6vmtie1j.png" alt="First conversation" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watch the tool trace above the answer: &lt;code&gt;ls&lt;/code&gt;, a glob for &lt;code&gt;pom.xml&lt;/code&gt;, a read of that file, then a recursive listing of &lt;code&gt;src&lt;/code&gt;. It looked before it spoke. And what it reported back was that the skeleton exists — parent POM, one web dependency, standard Maven layout — but there's no source, no &lt;code&gt;app.yml&lt;/code&gt;, no tests, so nothing is actually written yet.&lt;/p&gt;

&lt;p&gt;That's the signal you're checking for. An agent that had described a working application there would be an agent you couldn't trust on a real repo. Note the footer too: token count, elapsed time, and running context usage, so you can see what a turn cost you.&lt;/p&gt;

&lt;p&gt;Once you're connected, give it a foundation to work from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate .soloncode/CODE.md for this project, including build commands,
test commands, and notes on what to be careful about when modifying code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that file already exists, SolonCode prefers what's in it. That's the point — it's the engineering contract for the repo, and having it written down means you stop re-explaining your build every session.&lt;/p&gt;

&lt;p&gt;Now a small task, scoped tightly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check whether the install instructions in README are out of date.
Docs only, no code changes. Tell me what you changed when you're done.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small and reviewable on the first pass. You're not testing whether the agent can do impressive things yet; you're testing whether you can read and trust its output.&lt;/p&gt;

&lt;p&gt;For anything bigger, four things belong in 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;Goal: fix the NPE in the user login endpoint.
Scope: only the user module.
Constraint: do not change the response structure.
Verification: run the related unit tests; if none exist, say so.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Goal, scope, constraint, verification. The last one matters more than it looks — asking for verification is what turns "I made a change" into "I made a change and here's the evidence."&lt;/p&gt;

&lt;h2&gt;
  
  
  What else the panel covers
&lt;/h2&gt;

&lt;p&gt;Model config is one section out of nine. The rest are there when a specific need shows up: permissions and tool allow/deny lists, mount pools that expose external directories as Skills or Agents resources, a Skills marketplace with install support, MCP servers over stdio/SSE/streamable with connection checks and per-tool disabling, OpenAPI servers imported from a JSON/YAML spec, and LSP servers for code intelligence.&lt;/p&gt;

&lt;p&gt;None of that is required today, and the pattern is the same each time: open the section, fill in the entry, test, save.&lt;/p&gt;

&lt;p&gt;Saving generally writes through to the running engine, so most changes don't need a restart. When you're running several instances against the same config, there's a &lt;strong&gt;Reload config from disk&lt;/strong&gt; button at the top of the panel to bring them in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things to set before you expose it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Web auth.&lt;/strong&gt; General settings has username and password fields for a login page, and they're empty by default — which is fine for &lt;code&gt;localhost&lt;/code&gt;, and not fine the moment you bind to an address other machines can reach. If you're running SolonCode on a dev box or a shared server, turn this on before anything else. An unauthenticated agent UI is a remote shell with a nicer font.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keys out of Git.&lt;/strong&gt; Global config lives in your home directory, nowhere near your repo. Workspace config lives in &lt;code&gt;.soloncode/settings.json&lt;/code&gt;, inside the project. Check what your &lt;code&gt;.gitignore&lt;/code&gt; does with it. The scan-for-leaked-keys conversation is much less pleasant than the two minutes this takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things you'll appreciate later
&lt;/h2&gt;

&lt;p&gt;Web-mode features that don't matter on day one and become daily habits by week two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mermaid rendering&lt;/strong&gt; — ask for an architecture diagram and read it as a diagram&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory management&lt;/strong&gt; — inspect, edit, and delete long-term memory from the UI, with scope switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File preview&lt;/strong&gt; — images, video, and Markdown, including fullscreen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversation management&lt;/strong&gt; — pin, fork (copy), and delete threads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice input&lt;/strong&gt; — hold to talk, release to send&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skins&lt;/strong&gt; — &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;eyecare&lt;/code&gt;, &lt;code&gt;contrast&lt;/code&gt;, plus custom skins you upload as a zip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the keyboard, since you'll live here:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl + Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;↑&lt;/code&gt; / &lt;code&gt;↓&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cycle input history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/&lt;/code&gt; then Tab&lt;/td&gt;
&lt;td&gt;Command completion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@&lt;/code&gt; then Tab&lt;/td&gt;
&lt;td&gt;Subagent completion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When you don't want a UI at all
&lt;/h2&gt;

&lt;p&gt;For scripts, CI, or a one-shot job, skip the interactive console entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode run &lt;span class="s2"&gt;"Summarize the current project structure"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same engine, same config, no browser. The settings you just made in the web panel apply here too — which is the actual reason the panel is worth using first. You configure once, visually, with a connection test to confirm it, and every other entry point inherits it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick start, install to first conversation — &lt;a href="https://solon.noear.org/article/1467" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1467&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;settings.json and the Web settings page — &lt;a href="https://solon.noear.org/article/1465" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1465&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Web interactive mode — &lt;a href="https://solon.noear.org/article/1442" rel="noopener noreferrer"&gt;https://solon.noear.org/article/1442&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SolonCode CLI index — &lt;a href="https://solon.noear.org/article/soloncode" rel="noopener noreferrer"&gt;https://solon.noear.org/article/soloncode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Screens captured live on SolonCode v2026.8.4, 2026-08-04, UI set to English&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>tools</category>
      <category>opensource</category>
    </item>
    <item>
      <title>SolonCode in 15 Minutes: Install, Configure a Model, and Land Your First Reviewable Diff</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 03 Aug 2026 16:02:56 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncode-in-15-minutes-install-configure-a-model-and-land-your-first-reviewable-diff-3pcm</link>
      <guid>https://dev.to/solonjava/soloncode-in-15-minutes-install-configure-a-model-and-land-your-first-reviewable-diff-3pcm</guid>
      <description>&lt;p&gt;If your experience with coding agents is "it installed and it chats," you haven't had a first success yet — not the kind you can screenshot and show your team. Most getting-started posts stop at "I talked to it once," which doesn't tell you whether the tool can actually touch a real repository. This guide fixes that with a different yardstick.&lt;/p&gt;

&lt;p&gt;SolonCode is an open-source coding agent written in Java, built on the Solon AI framework, and designed to run on Java 8 through Java 26. One honest caveat up front: its prompt system is built around Chinese-first interaction (the official docs state it's not recommended if you can't work with Chinese prompts) — which makes it an interesting pick for Chinese-speaking teams and for developers who want a model-agnostic agent they can run on their own gateway. Everything below is verified against the official docs as of SolonCode v2026.8.3.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "first success" actually means here
&lt;/h2&gt;

&lt;p&gt;Most short tutorials leave you with three failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model won't connect, and you can't tell whether that counts as a failure or a misconfiguration.&lt;/li&gt;
&lt;li&gt;You chatted, but the agent never modified a real file, so you still don't trust it.&lt;/li&gt;
&lt;li&gt;Something breaks and you don't know whether to check the API key, the proxy, or the workspace path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this guide sets three concrete pass criteria — L1, L2, L3 — and gives you a fixed path: install → configure a model in the Web UI → land a small reviewable change. The goal isn't to learn all of SolonCode in 15 minutes; it's to earn one screenshot-worthy first success that you can explain to a colleague.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites (2-minute self-check)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;How to verify&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;Java 8 or higher (official support: Java 8 ~ 26)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;java -version&lt;/code&gt; prints a version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;macOS / Linux / Windows (Harmony PC also supported)&lt;/td&gt;
&lt;td&gt;you can run a shell / PowerShell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model API&lt;/td&gt;
&lt;td&gt;any LLM endpoint in OpenAI / Anthropic / Gemini / Ollama-compatible form&lt;/td&gt;
&lt;td&gt;you have &lt;code&gt;apiUrl&lt;/code&gt;, &lt;code&gt;apiKey&lt;/code&gt; (if needed), and a model name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SolonCode does not bundle or bind to any vendor — you bring your own model (BYOK) or point it at an internal gateway. Without a working model configuration, the process starts but that's not a first success.&lt;/p&gt;

&lt;p&gt;Nice-to-haves (not blockers): Git (helps you view diffs in the Web UI), a small real project directory (the launch directory becomes the workspace), and stable network / correct proxy settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install: one command, then verify the version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;macOS / Linux / Harmony PC:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://solon.noear.org/soloncode/setup.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (PowerShell):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://solon.noear.org/soloncode/setup.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-running the same install command updates the program while keeping your config and definition files. Program and user-level config default to &lt;code&gt;~/.soloncode/&lt;/code&gt; (Windows: &lt;code&gt;.soloncode&lt;/code&gt; under your home directory).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimum success check:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it prints a version string, you're good to continue. If you get &lt;code&gt;command not found&lt;/code&gt;: re-run the installer, check that &lt;code&gt;soloncode&lt;/code&gt; is on your PATH, and open a new terminal window (PATH changes often require a fresh shell).&lt;/p&gt;

&lt;p&gt;For offline / intranet installs, the official path is: download &lt;code&gt;soloncode-cli-bin-*.tar.gz&lt;/code&gt; from &lt;a href="https://gitee.com/opensolon/soloncode/releases" rel="noopener noreferrer"&gt;Gitee Releases&lt;/a&gt; on a networked machine, copy it over, extract, and run &lt;code&gt;install.sh&lt;/code&gt; / &lt;code&gt;install.ps1&lt;/code&gt;. For your first success, do it online first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure your first model in the Web UI
&lt;/h2&gt;

&lt;p&gt;The officially recommended path for new users is the Web settings page — no need to hand-edit config files on day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soloncode web 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;web&lt;/code&gt; command behavior: &lt;code&gt;soloncode web&lt;/code&gt; uses default port &lt;strong&gt;4808&lt;/strong&gt;; &lt;code&gt;soloncode web 0&lt;/code&gt; picks an available port automatically (handy when 4808 is taken); &lt;code&gt;soloncode web 1212&lt;/code&gt; binds a specific port. The terminal prints something like &lt;code&gt;Web interface: http://localhost:xxxxx/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Settings → LLM&lt;/strong&gt; (or equivalent) in the Web UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a model&lt;/strong&gt; with at least: API URL (&lt;code&gt;apiUrl&lt;/code&gt;), API key (if the provider needs one), and model name (exactly as shown in your provider console).&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;"Test connection"&lt;/strong&gt; button — the official docs emphasize it works.&lt;/li&gt;
&lt;li&gt;Make sure the model is &lt;strong&gt;enabled&lt;/strong&gt; and appears in the conversation's model list.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Config lands in &lt;code&gt;settings.json&lt;/code&gt;: user-level at &lt;code&gt;~/.soloncode/settings.json&lt;/code&gt; (shared across projects), workspace-level at &lt;code&gt;.soloncode/settings.json&lt;/code&gt; (project-specific). Workspace settings are read after user-level and can override or extend them. Security note: keys are secrets — never commit &lt;code&gt;settings.json&lt;/code&gt; to Git; use a secret manager or per-developer local config for team setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model-configuration success checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Test connection passes&lt;/li&gt;
&lt;li&gt;[ ] A "hello" message gets a normal model reply (not timeout / 401 / empty)&lt;/li&gt;
&lt;li&gt;[ ] The current model name shows near the UI title (e.g. &lt;code&gt;Model:deepseek-v4-flash&lt;/code&gt; — whatever you configured)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only now are you "installed and talking." That's not yet a coding success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter your project (workspace = launch directory)
&lt;/h2&gt;

&lt;p&gt;The official docs recommend starting inside the project root; the launch directory becomes the current workspace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project
soloncode web 0
&lt;span class="c"&gt;# or&lt;/span&gt;
soloncode cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No project yet? You can explore conversation and file generation in any empty directory, but L2/L3 strongly benefit from a real repository — otherwise your "demo" has no persuasive power. When the CLI starts, the tips line looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tips: (esc) interrupt | /(tab) command | $(tab) skill | @(tab) agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;esc&lt;/code&gt; interrupts; commands / skills / subagents have tab completion. Natural language is enough for your first success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-level task: L1 → L2 → L3
&lt;/h2&gt;

&lt;p&gt;Suggested time budget (roughly 15 minutes total, model speed dependent): L1 ~3 min, L2 5–7 min, L3 5–7 min. If the model is slow or the repo is large, do L2 thoroughly rather than skipping review and rushing L3.&lt;/p&gt;

&lt;h3&gt;
  
  
  L1 — greeting and project scan
&lt;/h3&gt;

&lt;p&gt;Send (in Chinese — this is the product's first-class interaction language):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请先阅读当前项目结构，告诉我：
1）这是什么技术栈；
2）构建命令和测试命令分别可能是什么；
3）如果存在 .soloncode/CODE.md，请优先参考它。
不要修改任何文件。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the project has no &lt;code&gt;CODE.md&lt;/code&gt; yet, the official quick start suggests continuing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请根据当前项目生成 .soloncode/CODE.md，包含构建命令、测试命令和代码修改注意事项。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L1 pass criteria (all must hold):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] The tech-stack description roughly matches the repo (honest "needs further confirmation" is allowed)&lt;/li&gt;
&lt;li&gt;[ ] Build/test entry points are stated or inferred (or honestly reported as not found)&lt;/li&gt;
&lt;li&gt;[ ] When you asked for no file changes, &lt;code&gt;git status&lt;/code&gt; shows no unexpected dirty files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common L1 failures: not starting in the project root; model misconfigured; empty repo or unreadable permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  L2 — a small change + explain the diff (first "demoable" step)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Task A (docs, lowest risk):&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;请帮我检查 README 中是否有过时的安装说明，只修改文档，不改业务代码。
完成后：
1）列出改了哪些文件；
2）用简短条目说明每处为什么改；
3）不要提交 Git，交给我人工确认。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Task B (code, still small):&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;请新增一个简单的健康检查接口（或补全已有 health 相关说明），范围尽量小。
限制：
- 不要做大范围重构；
- 不要改无关模块；
- 改完说明如何本地验证；
- 不要 git commit。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L2 pass criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Pointable file changes exist (visible in Web Git Diff or &lt;code&gt;git diff&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] The agent explains in natural language what changed and why&lt;/li&gt;
&lt;li&gt;[ ] You scanned the diff yourself — no keys, no huge unrelated reformatting, nothing deleted by accident&lt;/li&gt;
&lt;li&gt;[ ] You can describe this change to a colleague in one sentence (that's "demoable")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Human review checklist (mandatory, 30s–2min):&lt;/strong&gt; did the change exceed your allowed scope? Any API keys, intranet addresses, or passwords? Any lockfiles or generated artifacts modified? Can you actually run the verification steps yourself?&lt;/p&gt;

&lt;p&gt;This is where SolonCode's value starts showing: it advances the implementation; you guard the boundary and the merge decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  L3 — a constrained small feature (must be reviewable)
&lt;/h3&gt;

&lt;p&gt;Only after L2 passes. The prompt template includes the four elements the official docs emphasize: &lt;strong&gt;goal, scope, constraints, verification&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;目标：{one sentence describing the deliverable behavior, e.g. 为用户模块增加按邮箱查询的只读接口}
范围：只允许修改 {包名/目录} 下的文件；文档仅在必要时更新 README 一小节。
限制：
- 不要改变既有接口的返回结构；
- 不要升级依赖版本；
- 不要执行删除文件、强制推送、修改 CI 密钥类操作；
- 不要 git commit / push。
验证：
- 修改后运行：{e.g. mvn -pl xxx test 或 npm test 或具体命令}；
- 若无法运行测试，请说明阻塞原因与你已做的静态检查。
交付：
1）变更文件列表；
2）行为说明（给 reviewer 看）；
3）你执行过的命令与结果摘要；
4）残留风险（若有）。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L3 pass criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All four deliverables present (list / explanation / command results / risks)&lt;/li&gt;
&lt;li&gt;[ ] The diff is reviewable by a third person (no mysterious large-scale reshuffling)&lt;/li&gt;
&lt;li&gt;[ ] Verification commands were run, or the blocker is credible&lt;/li&gt;
&lt;li&gt;[ ] You explicitly decide: accept / partially accept / roll back — the decision stays with a human&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;L3 explicitly should NOT be:&lt;/strong&gt; whole-site rewrites, multi-service migration in one shot, "refactoring as a side effect" without tests, production data operations, permission escalation, key rotation — and never treat "one conversation" as "can merge without review."&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot checklist (for your team channel)
&lt;/h2&gt;

&lt;p&gt;Four to six screenshots beat a paragraph of adjectives:&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;What to capture&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;soloncode version&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Web Settings → LLM + test connection success&lt;/td&gt;
&lt;td&gt;model wired up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;L1's reply about project structure&lt;/td&gt;
&lt;td&gt;reads the workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Git Diff or &lt;code&gt;git diff&lt;/code&gt; snippet (mask sensitive info)&lt;/td&gt;
&lt;td&gt;actually changed code/docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Agent's change summary + your verification output&lt;/td&gt;
&lt;td&gt;reviewable and verifiable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;(optional) Web file tree + workspace path&lt;/td&gt;
&lt;td&gt;right directory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Top 10 failures and where to look first
&lt;/h2&gt;

&lt;p&gt;Debugging hint from the official docs: check on-disk logs under the workspace &lt;strong&gt;&lt;code&gt;.soloncode/logs/&lt;/code&gt;&lt;/strong&gt;; docs: &lt;a href="https://solon.noear.org/article/1415" rel="noopener noreferrer"&gt;logs &amp;amp; troubleshooting&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;#&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;soloncode&lt;/code&gt; command not found&lt;/td&gt;
&lt;td&gt;PATH, installed for current user&lt;/td&gt;
&lt;td&gt;reinstall; new terminal; confirm &lt;code&gt;~/.soloncode/bin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Java not 8+ / missing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;java -version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;install any JDK 8–26 release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Web UI won't open&lt;/td&gt;
&lt;td&gt;terminal URL; port occupied&lt;/td&gt;
&lt;td&gt;use &lt;code&gt;web 0&lt;/code&gt; or another port; open the printed address manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Model connection fails&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;apiUrl&lt;/code&gt; / &lt;code&gt;apiKey&lt;/code&gt; / model name&lt;/td&gt;
&lt;td&gt;compare character-by-character with provider console; check proxy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;401 / 403&lt;/td&gt;
&lt;td&gt;key permissions &amp;amp; quota&lt;/td&gt;
&lt;td&gt;rotate key; check plan and IP restrictions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Timeout / spinner forever&lt;/td&gt;
&lt;td&gt;network, proxy, cross-border path&lt;/td&gt;
&lt;td&gt;switch to reachable gateway; increase timeout; try a lighter model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Chats but won't touch files&lt;/td&gt;
&lt;td&gt;prompt forbade changes; permission / sandbox&lt;/td&gt;
&lt;td&gt;L2: explicitly allow target paths; check tool permissions in settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Modified wrong directory&lt;/td&gt;
&lt;td&gt;launch dir ≠ repo root&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cd&lt;/code&gt; to the root containing &lt;code&gt;pom.xml&lt;/code&gt; / &lt;code&gt;package.json&lt;/code&gt; / &lt;code&gt;.git&lt;/code&gt; before starting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Ignoring instructions&lt;/td&gt;
&lt;td&gt;language expectations&lt;/td&gt;
&lt;td&gt;the product is Chinese-prompt-driven; write constraints in Chinese&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Weird behavior / stale state&lt;/td&gt;
&lt;td&gt;long or old session context&lt;/td&gt;
&lt;td&gt;start a new session; complex tasks → check task state under &lt;code&gt;.soloncode/sessions/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mental note on safety: HITL (human-in-the-loop) switches like &lt;code&gt;hitlEnabled&lt;/code&gt; exist in settings. During your first success, keep the default conservative posture — don't disable safety-related limits just to go faster on a production repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  After first success: only two compounding steps
&lt;/h2&gt;

&lt;p&gt;Don't dive straight into Loop / multi-agent / IM binding. The smallest next loop is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Write a 10-line project &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/strong&gt; at &lt;code&gt;.soloncode/AGENTS.md&lt;/code&gt; (workspace beats user-level &lt;code&gt;~/.soloncode/AGENTS.md&lt;/code&gt;). Minimal copy-paste template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 本项目 Agent 规约（精简）&lt;/span&gt;

&lt;span class="gu"&gt;## 必须&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 改代码前先读相关文件；小步修改
&lt;span class="p"&gt;-&lt;/span&gt; 优先运行项目既有测试命令（见 CODE.md）
&lt;span class="p"&gt;-&lt;/span&gt; 完成后给出：变更列表、验证方式、风险

&lt;span class="gu"&gt;## 禁止&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 无确认不删文件、不 force push、不改密钥与 CI 密钥
&lt;span class="p"&gt;-&lt;/span&gt; 不引入与任务无关的依赖升级
&lt;span class="p"&gt;-&lt;/span&gt; 不把 API Key 写进仓库

&lt;span class="gu"&gt;## 技术偏好&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; 语言/框架：{填写}
&lt;span class="p"&gt;-&lt;/span&gt; 代码风格：与现有文件保持一致
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official advice: keep it short so it doesn't eat your context; state identity, boundaries, and workflow clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install exactly one Skill.&lt;/strong&gt; Either via Web &lt;strong&gt;Settings → Skill Market&lt;/strong&gt; (browse, read the description, then install to global or workspace pool), or manually: drop a directory containing &lt;code&gt;SKILL.md&lt;/code&gt; into &lt;code&gt;~/.soloncode/skills/&lt;/code&gt; or &lt;code&gt;.soloncode/skills/&lt;/code&gt;. Trigger it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请使用 {技能名} 按它的规约帮我完成 {一件具体事}。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI skill completion: &lt;code&gt;$(tab)&lt;/code&gt;. Principle: read the &lt;code&gt;SKILL.md&lt;/code&gt; scenario and permissions before installing. More skills ≠ stronger agent; more &lt;strong&gt;relevant&lt;/strong&gt; skills = stronger agent. Official skills doc: &lt;a href="https://solon.noear.org/article/1408" rel="noopener noreferrer"&gt;agent skills&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundaries: what is NOT first success, and when to stop
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not first success yet:&lt;/strong&gt; you only opened the web page and the model test fails; you only chatted and never produced a reviewable diff in a real repo; you have a diff but never looked at it and can't explain it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop for today if:&lt;/strong&gt; the target is a production main branch with no tests and no one available to review; the ask is architecture-level rewrite; or you have no legitimate model access path (account / gateway / compliance).&lt;/p&gt;

&lt;p&gt;SolonCode is a delegatable coding agent, not an auto-disclaimer machine. &lt;strong&gt;People own direction, boundaries, and merge; the agent advances within the boundary.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15-minute path (one page)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. java -version            # 8+
2. curl ...setup.sh | bash  # or irm ...setup.ps1 | iex
3. soloncode version
4. soloncode web 0
5. Settings → LLM → Add model → Test connection → say "你好"
6. cd your-project &amp;amp;&amp;amp; soloncode web 0
7. L1: scan the project (no file changes)
8. L2: small change + you review the diff
9. (optional) L3: constrained small feature + verification commands
10. Write a 10-line .soloncode/AGENTS.md or install one skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;First success isn't about faith in AI; it's about four real things: a real environment (JDK + command on PATH), a real model (test the connection, don't just save the form), a real task (a reviewable small change in the project root), and real review (a human reads the diff and clicks merge).&lt;/p&gt;

&lt;p&gt;When you can repeat L2 reliably, SolonCode stops being "another chat window" and becomes the onboarding day of a digital employee in your workflow. The next steps — model routing &amp;amp; cost (series B2), codifying specs into &lt;code&gt;AGENTS.md&lt;/code&gt; (series D3), or remote delegation via IM (series C2) — all build on the pass criteria above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it now:&lt;/strong&gt; run the first 8 steps of the one-page path and keep the L2 diff screenshot. That's the most honest manual SolonCode can give you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;References (all official):&lt;/em&gt; &lt;a href="https://github.com/opensolon/soloncode" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/soloncode" rel="noopener noreferrer"&gt;Docs entry&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1467" rel="noopener noreferrer"&gt;Quick start: install to first conversation&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1404" rel="noopener noreferrer"&gt;Install / update / uninstall&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1465" rel="noopener noreferrer"&gt;Settings &amp;amp; Web UI&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1442" rel="noopener noreferrer"&gt;Web interaction mode&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1411" rel="noopener noreferrer"&gt;Workspace &amp;amp; AGENTS.md / CODE.md&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1408" rel="noopener noreferrer"&gt;Skills&lt;/a&gt; · &lt;a href="https://solon.noear.org/article/1415" rel="noopener noreferrer"&gt;Logs &amp;amp; troubleshooting&lt;/a&gt; · &lt;a href="https://gitee.com/opensolon/soloncode/releases" rel="noopener noreferrer"&gt;Offline releases (Gitee)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>Solon Server Threads: Zero-Config Auto-Tuning by CPU Cores — ioBound, coreThreads, maxThreads</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Mon, 03 Aug 2026 15:58:02 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-server-threads-zero-config-auto-tuning-by-cpu-cores-iobound-corethreads-maxthreads-153a</link>
      <guid>https://dev.to/solonjava/solon-server-threads-zero-config-auto-tuning-by-cpu-cores-iobound-corethreads-maxthreads-153a</guid>
      <description>&lt;p&gt;It was 2 AM, and the on-call chat was on fire again: the order service was healthy on every dashboard, but throughput had flatlined at ~800 req/s while P99 climbed past 4 seconds. The usual suspect? A thread pool sized by guesswork during a late-night deploy, six months earlier. We'd hand-tuned &lt;code&gt;maxThreads&lt;/code&gt; to "something that felt right," and it wasn't right anymore.&lt;/p&gt;

&lt;p&gt;That's the moment I started appreciating a different default: in Solon, all of those knobs ship as &lt;code&gt;0&lt;/code&gt; — meaning &lt;em&gt;auto&lt;/em&gt;, derived from your machine's actual CPU cores at runtime. You can go months without thinking about a single thread-pool property. This post walks through the five knobs that exist, how the auto-tuning math works, and the three failure modes that tell you it's time to touch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five knobs under the hood
&lt;/h2&gt;

&lt;p&gt;Solon exposes these on &lt;code&gt;app.yml&lt;/code&gt; (all values are the documented defaults):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Minimum threads for the http server (0 = auto; also accepts fixed values like 2, or core multiples like x2)&lt;/span&gt;
&lt;span class="na"&gt;server.http.coreThreads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Maximum threads for the http server (0 = auto; also accepts fixed values like 32, or core multiples like x32)&lt;/span&gt;
&lt;span class="na"&gt;server.http.maxThreads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Idle thread timeout in ms (0 = auto)   # supported since v1.10.13&lt;/span&gt;
&lt;span class="na"&gt;server.http.idleTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="c1"&gt;# Is this an IO-bound service? (default true)   # supported since v1.12.2&lt;/span&gt;
&lt;span class="na"&gt;server.http.ioBound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Enable the virtual thread pool (default false)   # supported since v2.7.3&lt;/span&gt;
&lt;span class="na"&gt;solon.threads.virtual.enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: no hard-coded defaults for &lt;code&gt;coreThreads&lt;/code&gt; or &lt;code&gt;maxThreads&lt;/code&gt;. &lt;code&gt;0&lt;/code&gt; means "figure it out from the hardware." That single decision removes a whole class of "copy-pasted tuning values" problems — the ones that were right for someone else's 32-core box and wrong for your 2-core container.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU-bound or IO-bound: the one question that matters
&lt;/h2&gt;

&lt;p&gt;The auto-tuner only needs you to answer one question: is your workload CPU-bound or IO-bound?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU-bound&lt;/strong&gt;: the work happens entirely in CPU and memory — think a "hello world" handler that returns a string. Responses are extremely fast. Here, &lt;em&gt;more&lt;/em&gt; threads just means more context-switch overhead; you're paying for nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IO-bound&lt;/strong&gt;: the work touches the network card or disk — a CRUD write to a database, an uploaded file flushed to disk. These requests can take 10+ seconds, and each one occupies a thread while it waits. Threads run out fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;server.http.ioBound&lt;/code&gt; flag tells the framework which world you live in, and the auto-tuner adjusts accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The auto-tuning math
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;server.http.ioBound: true&lt;/code&gt; (the default):&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;Formula&lt;/th&gt;
&lt;th&gt;Example: 2c4g box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;coreThreads&lt;/td&gt;
&lt;td&gt;CPU cores × 2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxThreads&lt;/td&gt;
&lt;td&gt;coreThreads × 32&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With &lt;code&gt;server.http.ioBound: false&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;Setting&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Example: 2c4g box&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;coreThreads&lt;/td&gt;
&lt;td&gt;CPU cores × 2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maxThreads&lt;/td&gt;
&lt;td&gt;coreThreads × 8&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;IO-bound services get 4× the headroom — because each thread spends most of its life &lt;em&gt;waiting&lt;/em&gt; on IO, you need more of them in flight to keep the machine busy. CPU-bound services stay lean, because the CPU is the bottleneck and extra threads only add switching costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing the math yourself, the two-minute version
&lt;/h2&gt;

&lt;p&gt;Before you override anything, it helps to estimate what a setting is worth:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput (QPS).&lt;/strong&gt; If one request takes 0.1 s to respond, one thread can serve ~10 requests per second. 100 threads → ~1000 QPS. Thread count is a direct throughput ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; Each thread costs at least 1–2 MB. 100 threads ≈ 200 MB just sitting there. A 200 KB request payload can exist in ~4 copies during processing (≈800 KB), so at 1000 QPS you're churning ~800 MB/s of request data — and if GC only frees it after ~5 s, that's ~4 GB/s of live pressure. Thread tuning and memory tuning are the same conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why coreThreads is the "do not touch" knob
&lt;/h2&gt;

&lt;p&gt;Two reasons, depending on the underlying transport:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;BIO&lt;/strong&gt; servers, a large &lt;code&gt;coreThreads&lt;/code&gt; means the pool never shrinks — your "minimum" becomes a permanent resident of RAM.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;NIO&lt;/strong&gt; servers, &lt;code&gt;coreThreads&lt;/code&gt; must stay small because the pool is two-staged: &lt;code&gt;coreThreads&lt;/code&gt; is stage one, &lt;code&gt;maxThreads&lt;/code&gt; is stage two. Bloating stage one defeats the whole design.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Default it, and forget it. If you're going to change anything, it's &lt;code&gt;maxThreads&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways "threads are exhausted" shows up
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;maxThreads&lt;/code&gt; is genuinely too small, the symptoms are distinct — and each one tells you &lt;em&gt;where&lt;/em&gt; the bottleneck is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It stalls, then recovers.&lt;/strong&gt; The pool rejects a submission and the main thread takes over the work. The main thread can't accept new requests while it's busy, so things pause — then resume once the backlog clears. Annoying, but survivable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The protocol returns an error / refuses service.&lt;/strong&gt; The server actively rejects requests with an error, so the client learns &lt;em&gt;immediately&lt;/em&gt; that the server is overwhelmed. This is the polite refusal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The connection just drops.&lt;/strong&gt; The threads that &lt;em&gt;parse the protocol&lt;/em&gt; are exhausted too — there's no thread left to even craft a proper error response, so the server slams the connection shut.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Symptom 3 is the emergency red line. Symptom 1 is your early warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  When (and how) to actually override maxThreads
&lt;/h2&gt;

&lt;p&gt;"Leave it at the default" is the honest starting point. The framework's auto values handle the vast majority of single-instance deployments. You reach for &lt;code&gt;maxThreads&lt;/code&gt; when you're &lt;strong&gt;single-instance with heavy traffic or slow requests&lt;/strong&gt; — and you size it &lt;em&gt;against your memory budget&lt;/em&gt;, not against vibes.&lt;/p&gt;

&lt;p&gt;Official guidance example: on a &lt;strong&gt;1c2g&lt;/strong&gt; container, you might configure &lt;code&gt;x256&lt;/code&gt; → 512 threads. At ~2 MB per thread, that's ~1 GB of thread memory alone, leaving ~1 GB for business data processing. Tight — and the docs say so plainly: it may be borderline, and it may blow up memory. The point isn't the exact number; it's that you should &lt;em&gt;compute the memory cost&lt;/em&gt; before you type a bigger number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual threads: one flag, when your Java allows it
&lt;/h2&gt;

&lt;p&gt;Since v2.7.3, Solon has a dedicated switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.threads.virtual.enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# default: false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One property. If your runtime supports virtual threads (Java 21+), this is the "more concurrency without more native threads" lever — the alternative to hand-inflating &lt;code&gt;maxThreads&lt;/code&gt;. It exists precisely because the answer to "threads not enough" should not always be "increase the pool."&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: how server configs relate
&lt;/h2&gt;

&lt;p&gt;If you're wondering where these keys fit in the wider config map: Solon splits server settings into four series — &lt;code&gt;server.*&lt;/code&gt; (the master config, reused by all signals), &lt;code&gt;server.http.*&lt;/code&gt;, &lt;code&gt;server.socket.*&lt;/code&gt;, and &lt;code&gt;server.websocket.*&lt;/code&gt;. When a signal-specific value is absent, it falls back to the master config. Ports are the special case: http uses the main port, socket defaults to main + 20000, websocket to main + 15000.&lt;/p&gt;

&lt;p&gt;And for the curious: Solon's http signals ship as tiny plugins — &lt;code&gt;solon-server-jdkhttp&lt;/code&gt; (0.1 MB, BIO-based) and &lt;code&gt;solon-server-smarthttp&lt;/code&gt; (0.5 MB, AIO-based). Your &lt;code&gt;server.http.*&lt;/code&gt; knobs tune whichever signal you pulled in. (For context, in the Spring world — general knowledge, not a Solon claim — you'd typically hand-edit something like &lt;code&gt;server.tomcat.threads.max&lt;/code&gt; and pick a fixed number yourself.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Threads aren't "more is better," and they aren't "fewer is better" either — they're a budget you should spend knowingly. Solon's approach is to make the default a &lt;em&gt;computed&lt;/em&gt; number (CPU cores × 2 → × 32 or × 8 depending on &lt;code&gt;ioBound&lt;/code&gt;) instead of a hard-coded guess, so you only think about threads when your workload genuinely asks for it. When it does: size &lt;code&gt;maxThreads&lt;/code&gt; against memory, leave &lt;code&gt;coreThreads&lt;/code&gt; alone, and remember the virtual-thread flag is one line away.&lt;/p&gt;

&lt;p&gt;Next time your on-call chat lights up at 2 AM, the answer might be a single &lt;code&gt;server.http.maxThreads&lt;/code&gt; line — or the discovery that &lt;code&gt;0&lt;/code&gt; was doing fine all along.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written while exploring Solon (v4.0.4), an Apache-2.0 Java framework. Facts checked against the official docs (solon.noear.org articles 513, 565, 131).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>performance</category>
    </item>
    <item>
      <title>Solon.cfg() Everything: The Programmatic Config Center Behind SolonProps — Typed Getters, Prefix Groups, and Bean Binding</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sun, 02 Aug 2026 10:07:35 +0000</pubDate>
      <link>https://dev.to/solonjava/soloncfg-everything-the-programmatic-config-center-behind-solonprops-typed-getters-prefix-1mig</link>
      <guid>https://dev.to/solonjava/soloncfg-everything-the-programmatic-config-center-behind-solonprops-typed-getters-prefix-1mig</guid>
      <description>&lt;p&gt;Most frameworks make you read config the same way: sprinkle &lt;code&gt;@Value&lt;/code&gt; annotations everywhere, or grab a string from &lt;code&gt;Environment&lt;/code&gt; and parse it yourself every single time. It works, but the code ends up with a hundred tiny, untestable couplings to a file somewhere.&lt;/p&gt;

&lt;p&gt;Solon's answer is different. Config is not scattered across annotations — it lives in one place, and you can reach it from anywhere in your app. That place is &lt;code&gt;SolonProps&lt;/code&gt;, the application's property collection (a.k.a. the config center). You grab it with a single static call:&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;SolonProps&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No injection, no context lookup. &lt;code&gt;Solon.cfg()&lt;/code&gt; is available anywhere, including in static contexts and utility classes. This post walks through the API surface you'll actually use — typed getters, prefix grouping, bean conversion, expression/template lookup, and dynamic loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Typed getters — no more manual parsing
&lt;/h2&gt;

&lt;p&gt;The most common thing you do with config is read a value. SolonProps gives you typed shortcuts so you never hand-parse a string:&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;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&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;"demo.name"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// String, or null&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&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;"demo.port"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"8080"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// with default&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="n"&gt;max&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.max"&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="c1"&gt;// int&lt;/span&gt;
&lt;span class="kt"&gt;long&lt;/span&gt;   &lt;span class="n"&gt;ttl&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&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;"demo.ttl"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000L&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// long&lt;/span&gt;
&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDouble&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.ratio"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// double&lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.on"&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="c1"&gt;// boolean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;getByKeys("demo.url", "demo.url2")&lt;/code&gt; which returns the first non-null value across several keys — handy when you're migrating config keys and want a graceful fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Prefix grouping — get a whole section at once
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. Spring has &lt;code&gt;@ConfigurationProperties(prefix=...)&lt;/code&gt;, but Solon does it at runtime without any annotation. Pass a key prefix and you get back a fresh &lt;code&gt;Props&lt;/code&gt; collection with just that section:&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;// app.yml&lt;/span&gt;
&lt;span class="c1"&gt;// demo.db:&lt;/span&gt;
&lt;span class="c1"&gt;//   url: jdbc:mysql://localhost:3306/demo&lt;/span&gt;
&lt;span class="c1"&gt;//   user: root&lt;/span&gt;
&lt;span class="c1"&gt;//   maxPool: 8&lt;/span&gt;

&lt;span class="nc"&gt;Props&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.db"&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="n"&gt;db&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;"url"&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;pool&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"maxPool"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three variants cover different shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getProp(keyStarts)&lt;/code&gt; → a single &lt;code&gt;Props&lt;/code&gt; (one section)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getGroupedProp(keyStarts)&lt;/code&gt; → &lt;code&gt;Map&amp;lt;String, Props&amp;gt;&lt;/code&gt; (multi-instance sections like &lt;code&gt;demo.db1&lt;/code&gt;, &lt;code&gt;demo.db2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getListedProp(keyStarts)&lt;/code&gt; → &lt;code&gt;Map&amp;lt;String, Props&amp;gt;&lt;/code&gt; (list-style indexed sections)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So multi-datasource or multi-client configs don't need custom parsing code. The framework splits them for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bean conversion — config straight into objects
&lt;/h2&gt;

&lt;p&gt;Instead of reading keys one by one, bind an entire prefix to a POJO:&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;// demo.mail:&lt;/span&gt;
&lt;span class="c1"&gt;//   host: smtp.example.com&lt;/span&gt;
&lt;span class="c1"&gt;//   port: 587&lt;/span&gt;
&lt;span class="c1"&gt;//   from: no-reply@example.com&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;MailConfig&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="n"&gt;host&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;int&lt;/span&gt;    &lt;span class="n"&gt;port&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="n"&gt;from&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;MailConfig&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.mail"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MailConfig&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or bind the whole property set to an existing bean instance:&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;MailConfig&lt;/span&gt; &lt;span class="n"&gt;cfg&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;MailConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bindTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// matches keys against bean fields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toBean(Class&amp;lt;T&amp;gt;)&lt;/code&gt; converts the entire collection; &lt;code&gt;toBean(prefix, Class&amp;lt;T&amp;gt;)&lt;/code&gt; scopes it to a prefix. &lt;code&gt;bindTo(bean)&lt;/code&gt; mutates an existing instance. No reflection boilerplate on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Expression &amp;amp; template lookup
&lt;/h2&gt;

&lt;p&gt;Config values can reference each other (Solon resolves &lt;code&gt;${...}&lt;/code&gt; inside values at load time), but you can also do it on demand:&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;// expr:  ${key} | key | ${key:def} | key:def&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo.db.url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// plain key&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.db.url}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// explicit expr&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;v3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByExpr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.db.port:3306}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// with default&lt;/span&gt;

&lt;span class="c1"&gt;// tmpl:  ${key} | aaa${key}bbb | ${key:def}/ccc&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getByTmpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"jdbc:${demo.db.url}/db"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getByExpr&lt;/code&gt; is for single expressions, &lt;code&gt;getByTmpl&lt;/code&gt; for embedding values inside larger strings. Both respect &lt;code&gt;:def&lt;/code&gt; defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Loading &amp;amp; extending at runtime
&lt;/h2&gt;

&lt;p&gt;Solon apps load &lt;code&gt;app.yml&lt;/code&gt; automatically, but you can pull in more files at any time:&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:demo-ext.yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// from classpath&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file:/etc/app/extra.yml"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// from URL&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadAdd&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;getProperties&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;          &lt;span class="c1"&gt;// merge JVM props&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;loadAddIfAbsent(...)&lt;/code&gt; for incremental loading that won't clobber existing keys — useful for layering environment overrides. And if you build a plugin that needs to contribute config, &lt;code&gt;loadAdd(Properties)&lt;/code&gt; plus &lt;code&gt;plugs()&lt;/code&gt; (the plugin config collection) are your integration points.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Environment variables — bridge 12-factor style
&lt;/h2&gt;

&lt;p&gt;Kubernetes, Docker, and CI all prefer env vars over files. Pull them into your props with a prefix filter:&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadEnv&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="c1"&gt;// all vars starting with DEMO_&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SOLON"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;         &lt;span class="c1"&gt;// custom predicate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loaded vars become normal props, readable through all the typed getters above. One line, no third-party dotenv library.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. App &amp;amp; server metadata
&lt;/h2&gt;

&lt;p&gt;The framework exposes its own identity as typed getters — useful in logs, health checks, and admin pages:&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// solon.app.name&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appGroup&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// solon.app.group&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appNamespace&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// solon.app.namespace&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appTitle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// solon.app.title&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appEnabled&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// solon.app.enabled (health)&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;         &lt;span class="c1"&gt;// solon.locale&lt;/span&gt;

&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverPort&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// server.port&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverHost&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// server.host&lt;/span&gt;
&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverContextPath&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// server.contextPath&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Runtime mode checks
&lt;/h2&gt;

&lt;p&gt;Instead of passing flags around, ask the config center directly:&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDebugMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* dev-time behavior */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSetupMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* first-run setup     */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isFilesMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* running from files  */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDriftMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* solon cloud drift   */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isWhiteMode&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;      &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* security whitelist  */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;          &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* under unit test     */&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEnabledVirtualThreads&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* JDK 21+        */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  9. One subtle gotcha: removing a key
&lt;/h2&gt;

&lt;p&gt;Because props are synced to &lt;code&gt;System.getProperties()&lt;/code&gt; at load time, removing a key requires a double delete:&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;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redis.onOff"&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;getProperties&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"redis.onOff"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forget the second line and the key can still surface through &lt;code&gt;System.getProperties()&lt;/code&gt; lookups. It's the one place where the two-way sync shows its face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits compared to Spring
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Value("${x}")&lt;/code&gt; → &lt;code&gt;props.get("x")&lt;/code&gt; or the typed getters — no annotation needed, works in any context including static methods&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Environment.getProperty("x")&lt;/code&gt; → &lt;code&gt;Solon.cfg().get("x")&lt;/code&gt; — available without injecting &lt;code&gt;Environment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@ConfigurationProperties(prefix="x")&lt;/code&gt; → &lt;code&gt;props.getProp("x")&lt;/code&gt; / &lt;code&gt;props.toBean("x", X.class)&lt;/code&gt; — runtime, no compile-time binding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spring's approach is declarative and compile-checked; Solon's is a single runtime object with the same power, minus the annotation ceremony. Pick whichever fits your style — but know that in Solon the config center is always one call away.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Solon.cfg()&lt;/code&gt; gives you the whole config center, from anywhere, no injection&lt;/li&gt;
&lt;li&gt;Typed getters (&lt;code&gt;getInt/getLong/getBool/getDouble&lt;/code&gt;) kill manual parsing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getProp/getGroupedProp/getListedProp&lt;/code&gt; split config by prefix at runtime&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toBean/bindTo&lt;/code&gt; convert config sections into POJOs without annotations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loadEnv(prefix)&lt;/code&gt; bridges env vars; &lt;code&gt;loadAdd/loadAddIfAbsent&lt;/code&gt; layer extra files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getByExpr/getByTmpl&lt;/code&gt; handle expression and template lookup with &lt;code&gt;:def&lt;/code&gt; defaults&lt;/li&gt;
&lt;li&gt;Deleting a key needs a double remove (props + &lt;code&gt;System.getProperties()&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full API reference: &lt;a href="https://solon.noear.org/article/910" rel="noopener noreferrer"&gt;SolonProps Interface Reference&lt;/a&gt; — part of the official &lt;a href="https://solon.noear.org/article/482" rel="noopener noreferrer"&gt;Solon config guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon Config Vault: ENC() Secrets, @VaultInject, and a Runtime Key — No Jasypt, No Config Server</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:48:39 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-config-vault-enc-secrets-vaultinject-and-a-runtime-key-no-jasypt-no-config-server-5hi5</link>
      <guid>https://dev.to/solonjava/solon-config-vault-enc-secrets-vaultinject-and-a-runtime-key-no-jasypt-no-config-server-5hi5</guid>
      <description>&lt;p&gt;Every team has that one &lt;code&gt;application.yml&lt;/code&gt; with a database password sitting in plain text, committed to a repo that half the company can read. You know it should not be there. But the fix usually means pulling in Jasypt, standing up a config server, or explaining to your ops team why they need to manage another secret store.&lt;/p&gt;

&lt;p&gt;Solon takes a much smaller route. There's a built-in plugin called &lt;code&gt;solon-security-vault&lt;/code&gt; that lets you encrypt sensitive values (database credentials, API keys, whatever) and mark them with an &lt;code&gt;ENC(...)&lt;/code&gt; prefix right in your config file. No external service. No new infrastructure. Just a dependency, a helper method, and one annotation.&lt;/p&gt;

&lt;p&gt;Full disclosure, straight from the official docs: this is "anti-honest-people, not anti-thieves." It keeps secrets from being &lt;em&gt;readily&lt;/em&gt; visible, it does not pretend to be a KMS. If you need defense-in-depth, pair it with a proper config center. But for the very common problem of "don't leave plaintext passwords in Git," it's a clean, pragmatic answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;You configure one vault password, then any value wrapped in &lt;code&gt;ENC(...)&lt;/code&gt; gets decrypted on injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.vault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;liylU9PhDq63tk1C"&lt;/span&gt;

&lt;span class="na"&gt;test.db1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jdbc:mysql://localhost:3306/demo"&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC(xo1zJjGXUouQ/CZac55HZA==)"&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENC(XgRqh3C00JmkjsPi4mPySA==)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The vault password itself can come from a JVM arg at runtime instead of living in the file (more on that below).&lt;/li&gt;
&lt;li&gt;The default algorithm expects a 16-character password — mixed case and digits are recommended.&lt;/li&gt;
&lt;li&gt;Only the values wrapped in &lt;code&gt;ENC(...)&lt;/code&gt; are affected. Everything else stays as normal config.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1 — Add the plugin
&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;org.noear&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;solon-security-vault&lt;span class="nt"&gt;&amp;lt;/artifactId&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;h2&gt;
  
  
  Step 2 — Generate ciphertext
&lt;/h2&gt;

&lt;p&gt;You don't hand-roll the encrypted strings. A tiny helper in your app prints them:&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;class&lt;/span&gt; &lt;span class="nc"&gt;TestApp&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&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="nc"&gt;Solon&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="nc"&gt;TestApp&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;args&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// prints the encrypted form of "root"&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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encrypt&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="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;Run it once, copy the output into your &lt;code&gt;ENC(...)&lt;/code&gt; values, and delete the helper. (Or keep it behind a flag for rotating secrets later.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Inject with @VaultInject
&lt;/h2&gt;

&lt;p&gt;The idiomatic way is the &lt;code&gt;@VaultInject&lt;/code&gt; annotation. It decrypts on the way in, so the rest of your code never sees the ciphertext:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"db2"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="nf"&gt;db2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@VaultInject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${test.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&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;ds&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;
  
  
  The manual route: VaultUtils.guard()
&lt;/h2&gt;

&lt;p&gt;Not everything fits an annotation. When you need to decrypt imperatively, &lt;code&gt;VaultUtils&lt;/code&gt; has you covered — either a whole config block or a single value:&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;// decrypt a whole config block&lt;/span&gt;
&lt;span class="nc"&gt;Props&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test.db1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HikariDataSource&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="c1"&gt;// decrypt a single value&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VaultUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&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;"test.demo.name"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This composes nicely with &lt;code&gt;Solon.cfg()&lt;/code&gt; — the same property-collection entry point you already use for everything else. No second API to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the key out of the file
&lt;/h2&gt;

&lt;p&gt;Hardcoding the vault password next to the ciphertext is only marginally better than hardcoding the plaintext. The docs suggest passing it at runtime instead:&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;-Dsolon&lt;/span&gt;.vault.password&lt;span class="o"&gt;=&lt;/span&gt;your-16-char-key &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're used to &lt;code&gt;--key=value&lt;/code&gt; style args, Solon treats both forms equivalently — any startup arg containing a dot also becomes an app property. So &lt;code&gt;--solon.vault.password=...&lt;/code&gt; works just the same. That means CI/CD pipelines can inject the key per environment without touching the repo at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring your own algorithm
&lt;/h2&gt;

&lt;p&gt;The plugin ships with an &lt;code&gt;AesVaultCoder&lt;/code&gt;, and the &lt;code&gt;VaultCoder&lt;/code&gt; interface is your extension point. Either implement it as a component or register an existing implementation 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VaultCoderImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;VaultCoder&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;VaultCoderImpl&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// even the password's config key can be changed&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&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;"solon.vault.password"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;encrypt&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;str&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="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;decrypt&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;str&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="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;Or swap in a ready-made coder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;VaultCoder&lt;/span&gt; &lt;span class="nf"&gt;vaultCoderInit&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;AesVaultCoder&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;
  
  
  The honest boundary
&lt;/h2&gt;

&lt;p&gt;Let me quote the official line again, because it matters: this feature is "to prevent the gentleman, not the villain." It stops sensitive values from being &lt;em&gt;directly visible&lt;/em&gt; — a stray screenshot, a careless paste, a config dump in CI logs. It is not encryption at rest for your whole config system, and the vault password itself must be protected.&lt;/p&gt;

&lt;p&gt;So my take, after using it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use it for&lt;/strong&gt;: keeping DB passwords, tokens, and API keys out of plaintext in shared repos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't use it as&lt;/strong&gt;: a replacement for a KMS or a proper config center when you operate at that scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pair it with&lt;/strong&gt;: a runtime-injected &lt;code&gt;solon.vault.password&lt;/code&gt;, and treat the key like you'd treat any other secret — rotate it, restrict it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've come from Spring Boot, this is the niche that Jasypt usually fills — except here it's a first-party Solon plugin, the annotation is native, and the whole thing hangs off &lt;code&gt;Solon.cfg()&lt;/code&gt; with zero extra services. For a framework that prides itself on being restrained, it's a surprisingly complete little feature.&lt;/p&gt;

&lt;p&gt;Now go find that plaintext password in your repo. You know the one.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>devops</category>
      <category>java</category>
      <category>security</category>
    </item>
    <item>
      <title>Solon Config Injection: @Inject, @BindProps, and Auto-Refresh — No @Value, No @ConfigurationProperties</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:45:49 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-config-injection-inject-bindprops-and-auto-refresh-no-value-no-2bfp</link>
      <guid>https://dev.to/solonjava/solon-config-injection-inject-bindprops-and-auto-refresh-no-value-no-2bfp</guid>
      <description>&lt;p&gt;I've been using Solon for a while now, and one of the first things I had to unlearn coming from Spring was how configuration gets into my code. There is no &lt;code&gt;@Value&lt;/code&gt; scatter, no &lt;code&gt;@ConfigurationProperties&lt;/code&gt; class hierarchy — but honestly, after a week I stopped missing both.&lt;/p&gt;

&lt;p&gt;Here's how config injection and binding actually work in Solon, with the exact APIs I use daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three expression forms you'll meet
&lt;/h2&gt;

&lt;p&gt;Solon's &lt;code&gt;@Inject&lt;/code&gt; uses a &lt;code&gt;${...}&lt;/code&gt; expression syntax. Three forms cover almost everything:&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;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.name}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;              &lt;span class="c1"&gt;// plain property lookup&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// with a default value&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${classpath:user.config.yml}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// inject a whole resource file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${xxx:def}&lt;/code&gt; default only works for single values — don't try to default a collection or an entity with it. For those, you inject a whole property block instead (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Field injection, the everyday case
&lt;/h2&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;DemoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// static field injection, supported since v3.0&lt;/span&gt;
    &lt;span class="c1"&gt;// autoRefreshed = the value updates when config changes (singleton only!)&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoRefreshed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;trackName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// without a match, the field initial value is kept&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.url}"&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;trackUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://x.x.x/track"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// inject a whole config block as Properties&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;trackDbCfg&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// or let Solon build a real bean from the config block&lt;/span&gt;
    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;trackDs&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two lines are the ones that surprised me most. Give &lt;code&gt;@Inject&lt;/code&gt; a target type and a property prefix, and Solon constructs the object from the config block — &lt;code&gt;HikariDataSource&lt;/code&gt; gets built straight from &lt;code&gt;track.db1.*&lt;/code&gt; keys. No manual &lt;code&gt;setJdbcUrl(...)&lt;/code&gt; wiring.&lt;/p&gt;

&lt;p&gt;One caveat that bit me: &lt;code&gt;autoRefreshed = true&lt;/code&gt; is only safe on &lt;strong&gt;field injection into singleton beans&lt;/strong&gt;. If the bean is prototype-scoped, leave auto-refresh off, or you'll get surprising mid-lifecycle value swaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting into &lt;a class="mentioned-user" href="https://dev.to/bean"&gt;@bean&lt;/a&gt; method parameters and constructors
&lt;/h2&gt;



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

    &lt;span class="c1"&gt;// inject a datasource built from config, into a @Bean method&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="nf"&gt;db1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${track.db1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&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;ds&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// conditional bean: only created when the property exists and is true&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@Condition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onExpression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${cache.enable:false} == true"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${cache.config}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CacheServiceSupplier&lt;/span&gt; &lt;span class="n"&gt;supplier&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;supplier&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="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;Constructor injection works the same way on &lt;code&gt;@Component&lt;/code&gt; classes:&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;DemoConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;demoName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DemoConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.name}"&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;name&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;demoName&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="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;
  
  
  Binding a whole config class: @BindProps
&lt;/h2&gt;

&lt;p&gt;If you prefer a typed config object you can reuse (closer to Spring's &lt;code&gt;@ConfigurationProperties&lt;/code&gt;), bind it once and inject it anywhere:&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;// Option A: bind with @Inject + @Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${user.config}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&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="n"&gt;name&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="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Option B: bind with @BindProps (since v3.0.7) — also generates&lt;/span&gt;
&lt;span class="c1"&gt;// config metadata for IDE hints&lt;/span&gt;
&lt;span class="nd"&gt;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user.config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&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="n"&gt;name&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="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Or bind on a @Bean method&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user.config"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="nf"&gt;userProperties&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;UserProperties&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;Then reuse it anywhere with a plain &lt;code&gt;@Inject&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="nd"&gt;@Inject&lt;/span&gt;
&lt;span class="nc"&gt;UserProperties&lt;/span&gt; &lt;span class="n"&gt;userProperties&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail I appreciate: &lt;code&gt;@Inject&lt;/code&gt; on classes has &lt;strong&gt;required detection&lt;/strong&gt; — if the config is missing, Solon raises an exception instead of silently injecting nulls. That catches typos in &lt;code&gt;app.yml&lt;/code&gt; at startup, not at 3am in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual access when you don't want injection
&lt;/h2&gt;

&lt;p&gt;Sometimes injection is overkill — a util class, a static context, a one-off lookup. &lt;code&gt;Solon.cfg()&lt;/code&gt; is your gateway:&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;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&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;"track.name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"demoApi"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// with default&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;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&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;"track.url"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// plain&lt;/span&gt;

&lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;dbCfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.db1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// whole block as Properties&lt;/span&gt;
&lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getBean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.db1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&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="c1"&gt;// as bean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer method-call form for frequently-read values so you always get the freshest state:&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;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;trackName&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;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&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;"track.name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"demoApi"&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;
  
  
  Change subscription: two ways
&lt;/h2&gt;

&lt;p&gt;Config can change at runtime (config service push, code updates). Two mechanisms exist:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Auto-refresh on injected fields&lt;/strong&gt; (singletons only):&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;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${track.name:demoApi}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoRefreshed&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="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;trackName&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;2. Manual subscription&lt;/strong&gt; — you get every changed key and filter 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="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&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;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"track.name"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// refresh whatever depends on it&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;
  
  
  The mental model
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to...&lt;/th&gt;
&lt;th&gt;Spring way&lt;/th&gt;
&lt;th&gt;Solon way&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inject a scalar&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Value("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${x}")&lt;/code&gt; or &lt;code&gt;@Inject("${x:def}")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inject a config block&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ConfigurationProperties&lt;/code&gt; + class&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${prefix}")&lt;/code&gt; into &lt;code&gt;Properties&lt;/code&gt; or a typed target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusable typed config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ConfigurationProperties&lt;/code&gt; + registration&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject&lt;/code&gt;/&lt;code&gt;@BindProps&lt;/code&gt; + &lt;code&gt;@Configuration&lt;/code&gt; class, plain &lt;code&gt;@Inject&lt;/code&gt; to reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build a bean from config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Bean&lt;/code&gt; + manual mapping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject("${prefix}") HikariDataSource&lt;/code&gt; or &lt;code&gt;Solon.cfg().getBean(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React to config change&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@RefreshScope&lt;/code&gt; (cloud)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;autoRefreshed=true&lt;/code&gt; or &lt;code&gt;Solon.cfg().onChange(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing key handling&lt;/td&gt;
&lt;td&gt;silently null / error varies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Inject&lt;/code&gt; throws on missing (required detection)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@Inject&lt;/code&gt; doubles as the bean-injection annotation.&lt;/strong&gt; In Spring, &lt;code&gt;@Autowired&lt;/code&gt; and &lt;code&gt;@Value&lt;/code&gt; are separate concerns. In Solon, one annotation handles bean references (&lt;code&gt;@Inject DataSource&lt;/code&gt;) and config values (&lt;code&gt;@Inject("${...}")&lt;/code&gt;) — and when both are present (&lt;code&gt;@Inject("${track.db1}") HikariDataSource&lt;/code&gt;), it builds a bean &lt;em&gt;from&lt;/em&gt; config. Fewer annotations, one consistent rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config block is a first-class citizen.&lt;/strong&gt; &lt;code&gt;getProp()&lt;/code&gt; / &lt;code&gt;getBean()&lt;/code&gt; on any prefix means config-to-object conversion isn't locked inside a DI annotation — it's available anywhere &lt;code&gt;Solon.cfg()&lt;/code&gt; is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole surface. No profile classes to register, no &lt;code&gt;@ConfigurationProperties&lt;/code&gt; hierarchy to maintain — a prefix, a target type, and Solon does the rest.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>java</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon Multi-Environment Config: Six Loading Tiers, an In-File Segment Trick, and No Profiles API</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:05:29 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-multi-environment-config-six-loading-tiers-an-in-file-segment-trick-and-no-profiles-api-1id5</link>
      <guid>https://dev.to/solonjava/solon-multi-environment-config-six-loading-tiers-an-in-file-segment-trick-and-no-profiles-api-1id5</guid>
      <description>&lt;p&gt;If you've spent time with Spring Boot, you know the &lt;code&gt;spring.profiles.active&lt;/code&gt; routine — one property, multiple &lt;code&gt;application-{profile}.properties&lt;/code&gt; files, and a creeping suspicion that somewhere a prod config is silently falling back to dev defaults.&lt;/p&gt;

&lt;p&gt;Solon's answer is &lt;code&gt;solon.env&lt;/code&gt;. Same concept, different design choices — and one pattern (&lt;code&gt;solon.env.on&lt;/code&gt; in-file segments) that Spring has no direct equivalent for.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Single Key: &lt;code&gt;solon.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everything in Solon's multi-environment system pivots on one property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set this, and Solon automatically loads &lt;code&gt;app-dev.yml&lt;/code&gt; alongside &lt;code&gt;app.yml&lt;/code&gt;. Remove it, and only &lt;code&gt;app.yml&lt;/code&gt; loads. No profiles API, no &lt;code&gt;@ActiveProfiles&lt;/code&gt;, no annotation scanning — just a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment-Specific Files
&lt;/h2&gt;

&lt;p&gt;The naming convention is &lt;code&gt;app-{env}.yml&lt;/code&gt; (or &lt;code&gt;.properties&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;app.yml          ← always loaded&lt;/span&gt;
&lt;span class="s"&gt;app-dev.yml      ← loaded when solon.env=dev&lt;/span&gt;
&lt;span class="s"&gt;app-pro.yml      ← loaded when solon.env=pro&lt;/span&gt;
&lt;span class="s"&gt;app-test.yml     ← loaded when solon.env=test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keys in &lt;code&gt;app-{env}.yml&lt;/code&gt; override the same keys in &lt;code&gt;app.yml&lt;/code&gt;. Everything else in &lt;code&gt;app.yml&lt;/code&gt; is inherited.&lt;/p&gt;

&lt;p&gt;One constraint worth knowing: &lt;code&gt;app-{env}.yml&lt;/code&gt; cannot itself declare another &lt;code&gt;solon.env&lt;/code&gt;. The env chain is intentionally one level deep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Ways to Activate an Environment
&lt;/h2&gt;

&lt;p&gt;You don't have to hardcode &lt;code&gt;solon.env&lt;/code&gt; in the file. You can set it at any of four levels, with higher numbers winning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 (lowest)&lt;/td&gt;
&lt;td&gt;In &lt;code&gt;app.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon.env: dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;JVM system property&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-Dsolon.env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Launch argument&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 (highest)&lt;/td&gt;
&lt;td&gt;OS environment variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;export solon.env=pro&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice: set &lt;code&gt;solon.env: dev&lt;/code&gt; in &lt;code&gt;app.yml&lt;/code&gt; as a safe default for local development, then override at deploy time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Jar deployment&lt;/span&gt;
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro

&lt;span class="c"&gt;# Docker run&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'solon.env=pro'&lt;/span&gt; demo_image

&lt;span class="c"&gt;# Kubernetes pod spec&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt;:
  - name: solon.env
    value: pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three launch forms below are equivalent:&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;-Dsolon&lt;/span&gt;.env&lt;span class="o"&gt;=&lt;/span&gt;pro &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--solon&lt;/span&gt;.env&lt;span class="o"&gt;=&lt;/span&gt;pro
java &lt;span class="nt"&gt;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Six Loading Tiers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;solon.env&lt;/code&gt; controls which env file loads, but that's just the first two tiers of a six-tier config loading stack. Later tiers override earlier ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Source&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;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.yml&lt;/code&gt; + &lt;code&gt;app-{env}.yml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Base config, inside the jar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; classpath entries&lt;/td&gt;
&lt;td&gt;v2.2.7+, supports &lt;code&gt;${solon.env}&lt;/code&gt; variables; wildcard &lt;code&gt;*&lt;/code&gt; from v2.7.6+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.add&lt;/code&gt; external files&lt;/td&gt;
&lt;td&gt;Files placed beside the jar at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;JVM props / env vars / launch args&lt;/td&gt;
&lt;td&gt;Runtime overrides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.cfg().loadAdd()&lt;/code&gt; in startup code&lt;/td&gt;
&lt;td&gt;Programmatic additions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Solon Cloud Config (e.g., Nacos)&lt;/td&gt;
&lt;td&gt;Remote config center&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern: static defaults live inside the jar, runtime overrides live outside it, remote config sits at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading More Configs: &lt;code&gt;solon.config.load&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;For apps with per-module configs, &lt;code&gt;solon.config.load&lt;/code&gt; lets you declare additional classpath resources with variable substitution and wildcard support (v2.7.6+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;solon.config.load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:${solon.env}/jdbc.yml"&lt;/span&gt;        &lt;span class="c1"&gt;# resolves to classpath:dev/jdbc.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:${solon.env}/*.yml"&lt;/span&gt;           &lt;span class="c1"&gt;# all yml in dev/ folder (v2.7.6+)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:app-ds-${solon.env}.yml"&lt;/span&gt;      &lt;span class="c1"&gt;# e.g., app-ds-dev.yml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:common/base.yml"&lt;/span&gt;              &lt;span class="c1"&gt;# environment-agnostic shared config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you split config by concern (database, cache, auth) rather than cramming everything into one file per environment.&lt;/p&gt;

&lt;p&gt;For external files placed beside the jar, use &lt;code&gt;solon.config.add&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.config.add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./local-overrides.yml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or at launch time:&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;-jar&lt;/span&gt; demo.jar &lt;span class="nt"&gt;--solon&lt;/span&gt;.config.add&lt;span class="o"&gt;=&lt;/span&gt;./local-overrides.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The In-File Segment Trick (v2.5.5+)
&lt;/h2&gt;

&lt;p&gt;This is the feature that has no Spring Boot equivalent. Solon lets you embed multiple environment-specific segments inside a single &lt;code&gt;app.yml&lt;/code&gt;, separated by &lt;code&gt;---&lt;/code&gt; and guarded by &lt;code&gt;solon.env.on&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pro&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;solon.env.on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pro&lt;/span&gt;
&lt;span class="na"&gt;demo.auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${AUTH_PASSWORD}"&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;solon.env.on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev|test&lt;/span&gt;
&lt;span class="na"&gt;demo.auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo1234"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the segment matching the current &lt;code&gt;solon.env&lt;/code&gt; is applied. The &lt;code&gt;|&lt;/code&gt; syntax lets one segment cover multiple environments.&lt;/p&gt;

&lt;p&gt;This pattern suits small services where maintaining separate env files feels heavy, but you still want clear boundaries in the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programmatic Loading
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to load config based on conditions known only at startup. Two clean hooks:&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;@SolonMain&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;App&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&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="nc"&gt;App&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;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Tier 5: added before beans initialize, merged into the config stack&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app-jdbc-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".yml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadAdd&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app-cache-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;".yml"&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;Or via &lt;code&gt;@Import&lt;/code&gt; on the startup class:&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;@Import&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"classpath:module-defaults.yml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@SolonMain&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;App&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&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="nc"&gt;App&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;args&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;For Docker/Kubernetes, &lt;code&gt;loadEnv()&lt;/code&gt; pulls in environment variables by prefix:&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;Solon&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="nc"&gt;App&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;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;loadEnv&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="c1"&gt;// pulls in all env vars starting with "demo."&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Solon automatically syncs &lt;code&gt;solon.*&lt;/code&gt; environment variables into the config store, so &lt;code&gt;solon.env=pro&lt;/code&gt; as a container env var works without &lt;code&gt;loadEnv()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Injecting Config Values
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@Inject&lt;/code&gt; with &lt;code&gt;${}&lt;/code&gt; syntax wires config values into beans:&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;DataSourceConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${db.url}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dbUrl&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${db.username}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For structured config objects, &lt;code&gt;@BindProps&lt;/code&gt; binds an entire prefix:&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;@BindProps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"app.mail"&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;MailProperties&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="n"&gt;host&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;int&lt;/span&gt; &lt;span class="n"&gt;port&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="n"&gt;username&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;Variable references also work inside YAML values themselves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;db.server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.1"&lt;/span&gt;
&lt;span class="na"&gt;db.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders"&lt;/span&gt;
&lt;span class="na"&gt;db.url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jdbc:mysql://${db.server}/${db.name}"&lt;/span&gt;  &lt;span class="c1"&gt;# composed from other keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A Realistic Project Layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/main/resources/
├── app.yml                   # base config + solon.env: dev
├── app-dev.yml               # dev overrides
├── app-pro.yml               # prod overrides
└── config/
    ├── common/
    │   └── base.yml          # shared constants (env-agnostic)
    ├── dev/
    │   └── jdbc.yml          # dev DB connection
    └── pro/
        └── jdbc.yml          # prod DB connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;

&lt;span class="na"&gt;solon.config.load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:config/common/base.yml"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classpath:config/${solon.env}/jdbc.yml"&lt;/span&gt;

&lt;span class="na"&gt;solon.app.name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-service"&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app-dev.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.logging.level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debug&lt;/span&gt;
&lt;span class="na"&gt;solon.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app-pro.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.logging.level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy to prod:&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;-jar&lt;/span&gt; order-service.jar &lt;span class="nt"&gt;--env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pro
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One flag flips the entire config stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Reference (from Spring Boot)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spring Boot&lt;/th&gt;
&lt;th&gt;Solon&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;&lt;code&gt;spring.profiles.active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon.env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same concept, different key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;application-{profile}.properties&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app-{env}.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@Profile("dev")&lt;/code&gt; on beans&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.env.on: dev&lt;/code&gt; in YAML&lt;/td&gt;
&lt;td&gt;Config-layer isolation vs code-layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@PropertySource&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; in YAML&lt;/td&gt;
&lt;td&gt;Declarative import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@ConfigurationProperties(prefix="x")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@BindProps(prefix="x")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prefix binding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Value("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Inject("${x}")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Value injection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mental model is nearly identical. The key difference: Solon keeps environment logic in config (via &lt;code&gt;solon.env.on&lt;/code&gt; segments or separate files) rather than scattering &lt;code&gt;@Profile&lt;/code&gt; annotations across bean definitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;solon.env&lt;/code&gt; is the single activation key — set it in the file, at the JVM, or as a container env var&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app-{env}.yml&lt;/code&gt; is loaded automatically; no registration step required&lt;/li&gt;
&lt;li&gt;Six loading tiers let you layer static defaults with runtime overrides cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;solon.config.load&lt;/code&gt; with &lt;code&gt;${solon.env}&lt;/code&gt; handles per-module, per-environment config splitting&lt;/li&gt;
&lt;li&gt;In-file &lt;code&gt;---&lt;/code&gt; segments with &lt;code&gt;solon.env.on&lt;/code&gt; (v2.5.5+) are unique to Solon — useful for small services&lt;/li&gt;
&lt;li&gt;Config injection uses &lt;code&gt;@Inject("${...}")&lt;/code&gt; and &lt;code&gt;@BindProps&lt;/code&gt; — both standard Solon IoC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loading order is deterministic and documented. Deploy with confidence.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>software</category>
    </item>
    <item>
      <title>Solon's 10 HTTP Servers: Pick Your Engine With One Dependency Change</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Fri, 31 Jul 2026 04:34:01 +0000</pubDate>
      <link>https://dev.to/solonjava/solons-10-http-servers-pick-your-engine-with-one-dependency-change-55oi</link>
      <guid>https://dev.to/solonjava/solons-10-http-servers-pick-your-engine-with-one-dependency-change-55oi</guid>
      <description>&lt;p&gt;I've been running Solon apps in production for a while now, and one thing that keeps surprising people is how the server engine works.&lt;/p&gt;

&lt;p&gt;Not "which server does Solon use" — because the answer is: &lt;strong&gt;you choose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Solon doesn't bundle a hardcoded HTTP server. The framework core is about 0.3MB. The HTTP engine is a separate plugin. You pick one. Or two. Or swap them between environments without touching a single line of application code.&lt;/p&gt;

&lt;p&gt;Here's a quick rundown of the 10 HTTP server options, what they're good for, and how to switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Dependency Swap
&lt;/h2&gt;

&lt;p&gt;In Solon, changing your HTTP server means changing one dependency in &lt;code&gt;pom.xml&lt;/code&gt;. Nothing else.&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="c"&gt;&amp;lt;!-- Default: jdkhttp (built-in JDK, 0.3MB) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-web&lt;span class="nt"&gt;&amp;lt;/artifactId&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;Want Undertow instead?&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;org.noear&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;solon-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-jdkhttp&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/exclusions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-undertow&lt;span class="nt"&gt;&amp;lt;/artifactId&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;That's it. Your &lt;code&gt;@Controller&lt;/code&gt;, &lt;code&gt;@Mapping("/hello")&lt;/code&gt;, filters, interceptors — everything stays the same. The routing layer is framework-owned; the server underneath is a pluggable adapter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 Engines at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Protocols&lt;/th&gt;
&lt;th&gt;Reactive&lt;/th&gt;
&lt;th&gt;JDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jdkhttp&lt;/td&gt;
&lt;td&gt;0.3MB&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;smarthttp&lt;/td&gt;
&lt;td&gt;0.8MB&lt;/td&gt;
&lt;td&gt;http, ws&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;grizzly&lt;/td&gt;
&lt;td&gt;1.8MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vertx&lt;/td&gt;
&lt;td&gt;6.3MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jetty&lt;/td&gt;
&lt;td&gt;2.7MB&lt;/td&gt;
&lt;td&gt;http, ws&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undertow&lt;/td&gt;
&lt;td&gt;4.6MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tomcat&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jetty-jakarta&lt;/td&gt;
&lt;td&gt;3.9MB&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undertow-jakarta&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tomcat-jakarta&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;http, ws, http2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus two dedicated WebSocket-only servers (&lt;code&gt;solon-server-websocket&lt;/code&gt; at 0.4MB, &lt;code&gt;solon-server-websocket-netty&lt;/code&gt; at 3.6MB) and one Socket.D server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For microservices and edge services: jdkhttp or smarthttp
&lt;/h3&gt;

&lt;p&gt;If you're building a lightweight API service or a microservice that needs to start in under a second, stick with &lt;code&gt;jdkhttp&lt;/code&gt;. It's 0.3MB, requires zero external dependencies, and supports reactive I/O. The JDK built-in &lt;code&gt;com.sun.net.httpserver&lt;/code&gt; is perfectly adequate for most API workloads.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;smarthttp&lt;/code&gt; (0.8MB, Chinese-contributed) adds WebSocket support on the same port — useful if you need both REST and WebSocket without adding a second server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SolonMain&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;App&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&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="nc"&gt;App&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;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableWebSocket&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="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When you need HTTP/2: grizzly, undertow, vertx, or tomcat
&lt;/h3&gt;

&lt;p&gt;All four support HTTP/2. Enable it with a single line:&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;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&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;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableHttp2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// v2.3.8+&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undertow is a solid middle ground — 4.6MB, Apache 2.0 license, good community adoption. Vert.x is heavier (6.3MB) but gives you the full Vert.x event-loop ecosystem if you're already using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traditional Java shops: Jetty or Tomcat
&lt;/h3&gt;

&lt;p&gt;If your team is migrating from Spring Boot + embedded Tomcat, &lt;code&gt;solon-server-tomcat&lt;/code&gt; is the most natural drop-in. It uses the same Tomcat v9 engine underneath — your ops team's existing tuning knowledge carries over.&lt;/p&gt;

&lt;p&gt;Jetty is a great alternative if you prefer Eclipse ecosystem. It also has explicit JSP support via &lt;code&gt;solon-server-jetty-add-jsp&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jakarta EE migration: jetty-jakarta, undertow-jakarta, tomcat-jakarta
&lt;/h3&gt;

&lt;p&gt;If you're on JDK 17+ and targeting Jakarta namespace (&lt;code&gt;jakarta.*&lt;/code&gt;), use the Jakarta variants. They're paired with the corresponding Jakarta server versions (Jetty 12, Undertow 2.3, Tomcat 11).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code That Never Changes
&lt;/h2&gt;

&lt;p&gt;This is the part I like most. No matter which server engine you pick, your application code looks identical:&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;@Controller&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;DemoController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/hello"&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;hello&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 world!"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/greet/{name}"&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;greet&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;name&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;name&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;The same code runs on jdkhttp (0.3MB, no dependencies), Undertow (4.6MB, HTTP/2), or Tomcat (production-hardened). Zero changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced: Multiple Ports, SSL, and Debug Mode
&lt;/h2&gt;

&lt;p&gt;All server adapters share the same configuration surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.yml&lt;/span&gt;
&lt;span class="na"&gt;server.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="na"&gt;server.http.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;          &lt;span class="c1"&gt;# alias&lt;/span&gt;
&lt;span class="na"&gt;server.ssl.keyStore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/data/ca/demo.jks"&lt;/span&gt;
&lt;span class="na"&gt;server.ssl.keyPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or programmatically:&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;Solon&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="nc"&gt;App&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;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Add a second HTTP port (useful when main port is HTTPS)&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&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;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addHttpPort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableDebug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// debug mode for smarthttp/jdkhttp&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;Custom SSLContext (v2.5.9+):&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;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServerConfigure&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;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SSLContext&lt;/span&gt; &lt;span class="n"&gt;sslContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildMyCustomSSLContext&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableSsl&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;sslContext&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 About the "Default" Default?
&lt;/h2&gt;

&lt;p&gt;If you add &lt;code&gt;solon-web&lt;/code&gt; without any explicit server exclusion, Solon uses &lt;code&gt;jdkhttp&lt;/code&gt; — the built-in JDK HTTP server. It's the lightest option (0.3MB), requires zero external JARs, and works for 90% of development scenarios.&lt;/p&gt;

&lt;p&gt;In production, you might want something more tuned. But the beauty is: &lt;strong&gt;you don't decide on day one.&lt;/strong&gt; Start with jdkhttp, benchmark, then swap to Undertow or Tomcat when you need HTTP/2 or WebSocket — all without rewriting a single route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tomcat&lt;/strong&gt; (v9) doesn't support WebSocket directly — you need the &lt;code&gt;solon-server-tomcat-add-websocket&lt;/code&gt; sub-plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;smarthttp&lt;/strong&gt; had a file upload size cap (~2.1GB) before v3.0.3 — fixed now, but worth noting if you're on an older version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vert.x&lt;/strong&gt; is the heaviest at 6.3MB — great for reactive workloads but overkill for a simple CRUD API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jakarta variants&lt;/strong&gt; (jetty-jakarta, undertow-jakarta, tomcat-jakarta) require JDK 17+.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Solon's pluggable server architecture is one of those features that seems small on paper but makes a big difference in practice. One dependency change, zero code impact, and a full spectrum of engines from 0.3MB to full-featured production servers.&lt;/p&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;Pick&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lightest API server&lt;/td&gt;
&lt;td&gt;jdkhttp (0.3MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST + WebSocket on same port&lt;/td&gt;
&lt;td&gt;smarthttp (0.8MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP/2 + solid performance&lt;/td&gt;
&lt;td&gt;undertow (4.6MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring Boot migration&lt;/td&gt;
&lt;td&gt;tomcat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reactive event-loop&lt;/td&gt;
&lt;td&gt;vert.x or grizzly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jakarta EE (JDK 17+)&lt;/td&gt;
&lt;td&gt;jetty-jakarta / undertow-jakarta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone WebSocket&lt;/td&gt;
&lt;td&gt;solon-server-websocket (0.4MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server is infrastructure. The framework is your code. Solon keeps them separate — and that's a good thing.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon Validation: @Valid, 20+ Annotations, and Custom Validators Without the JSR 380 Baggage</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Fri, 31 Jul 2026 02:00:18 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-validation-valid-20-annotations-and-custom-validators-without-the-jsr-380-baggage-15e0</link>
      <guid>https://dev.to/solonjava/solon-validation-valid-20-annotations-and-custom-validators-without-the-jsr-380-baggage-15e0</guid>
      <description>&lt;p&gt;I've been burned by JSR 380 enough times to appreciate a validation framework that doesn't drag in a whole ecosystem of dependencies. Solon's validation system lives in &lt;code&gt;solon-security-validation&lt;/code&gt; — a single plugin that gives you 20+ annotations, entity validation, and custom validators, all without pulling in &lt;code&gt;javax.validation&lt;/code&gt; or &lt;code&gt;jakarta.validation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let me walk through what I've found useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two-Phase Validation Model
&lt;/h2&gt;

&lt;p&gt;Solon separates validation into two phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Context validation&lt;/strong&gt; (pre-injection) — runs before method parameters are resolved. Annotations on the method itself check headers, cookies, IP, or request-level constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameter validation&lt;/strong&gt; (post-injection) — runs after parameters are resolved. Annotations on individual parameters or entity fields.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This maps to two different implementation mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context validation uses &lt;code&gt;@Addition(Filter.class)&lt;/code&gt; under the hood&lt;/li&gt;
&lt;li&gt;Parameter validation uses &lt;code&gt;@Around(Interceptor.class)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to think about this most of the time, but it explains why some annotations go on the method and others on the parameter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: &lt;a class="mentioned-user" href="https://dev.to/valid"&gt;@valid&lt;/a&gt; on a Controller
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;@Valid&lt;/code&gt; to your controller class (or its base class), then annotate the parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Valid&lt;/span&gt;
&lt;span class="nd"&gt;@Controller&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;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/add"&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;addUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nd"&gt;@NotNull&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Email&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Pattern&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"^https?://"&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;avatarUrl&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nd"&gt;@Length&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&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="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// All parameters validated before this runs&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No need to configure a &lt;code&gt;ValidatorFactory&lt;/code&gt; or declare a bean. &lt;code&gt;@Valid&lt;/code&gt; on the class enables validation for all handler methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  @Validated for Entity (Nested) Validation
&lt;/h2&gt;

&lt;p&gt;When your parameter is a DTO with its own validation rules, use &lt;code&gt;@Validated&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="nd"&gt;@Valid&lt;/span&gt;
&lt;span class="nd"&gt;@Controller&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;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/register"&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;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Validated&lt;/span&gt; &lt;span class="nc"&gt;RegisterRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// req fields are validated&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Data&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;RegisterRequest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;
    &lt;span class="nd"&gt;@Length&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&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;max&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Email&lt;/span&gt;
    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Validated&lt;/span&gt;  &lt;span class="c1"&gt;// Nested entity validation&lt;/span&gt;
    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;
    &lt;span class="nd"&gt;@Size&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orderList&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;
  
  
  Group Validation
&lt;/h2&gt;

&lt;p&gt;For update vs create scenarios, use groups:&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;UpdateGroup&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;@Data&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;User&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UpdateGroup&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="c1"&gt;// Only required on update&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Controller&lt;/span&gt;
&lt;span class="nd"&gt;@Valid&lt;/span&gt;
&lt;span class="nd"&gt;@Controller&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;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/update"&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;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Validated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UpdateGroup&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;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Only validates fields in UpdateGroup&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;
  
  
  The 20+ Annotations in One Table
&lt;/h2&gt;

&lt;p&gt;Solon's validation plugin ships with a comprehensive set of annotations. Here's the full list:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Valid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller class&lt;/td&gt;
&lt;td&gt;Enable validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Validated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;Validate entity fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotNull&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Method/param/field&lt;/td&gt;
&lt;td&gt;Not null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Method/param/field&lt;/td&gt;
&lt;td&gt;Is null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotBlank&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Method/param/field&lt;/td&gt;
&lt;td&gt;Not blank string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotEmpty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Method/param/field&lt;/td&gt;
&lt;td&gt;Not empty string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotZero&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Method/param/field&lt;/td&gt;
&lt;td&gt;Not zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Min(value)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;&amp;gt;= value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Max(value)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;&amp;lt;= value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DecimalMin(value)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;&amp;gt;= decimal value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@DecimalMax(value)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;&amp;lt;= decimal value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Length(min, max)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;String length range&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;Parameter/field&lt;/td&gt;
&lt;td&gt;Collection size range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;Email format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Pattern(value)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;Regex match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;Date format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Numeric&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parameter/field&lt;/td&gt;
&lt;td&gt;Numeric format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Logined&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller/method&lt;/td&gt;
&lt;td&gt;User is logged in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NoRepeatSubmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller/method&lt;/td&gt;
&lt;td&gt;No duplicate submission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@Whitelist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller/method&lt;/td&gt;
&lt;td&gt;IP in whitelist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@NotBlacklist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Controller/method&lt;/td&gt;
&lt;td&gt;IP not in blacklist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few of these (&lt;code&gt;@Logined&lt;/code&gt;, &lt;code&gt;@NoRepeatSubmit&lt;/code&gt;, &lt;code&gt;@Whitelist&lt;/code&gt;, &lt;code&gt;@NotBlacklist&lt;/code&gt;) are "business-aware" — they need you to implement a checker interface. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation on Non-Web Components
&lt;/h2&gt;

&lt;p&gt;This surprised me: Solon's validation also works on plain &lt;code&gt;@Component&lt;/code&gt; classes, not just controllers:&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;@Valid&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;UserService&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;addUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@NotNull&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@Email&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Validation works here too&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;
  
  
  Manual Validation with ValidUtils
&lt;/h2&gt;

&lt;p&gt;You can also validate programmatically anywhere:&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;User&lt;/span&gt; &lt;span class="n"&gt;user&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;User&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;ValidUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validateEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// throws ValidatorException if invalid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling Validation Errors
&lt;/h2&gt;

&lt;p&gt;Catch &lt;code&gt;ValidatorException&lt;/code&gt; in a filter:&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;ValidationFilter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;FilterChain&lt;/span&gt; &lt;span class="n"&gt;chain&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;Throwable&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;chain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidatorException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCode&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&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;By default, validation stops at the first error. If you want to collect all errors, set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;solon.validation.validateAll&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Business-Aware Validators (The Four Checkers)
&lt;/h2&gt;

&lt;p&gt;Four annotations need you to provide a checker implementation. This is where Solon's design shines — the framework handles the interception, you just plug in the business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  @NoRepeatSubmit
&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;@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;NoRepeatSubmitCheckerImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;NoRepeatSubmitChecker&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NoRepeatSubmit&lt;/span&gt; &lt;span class="n"&gt;anno&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&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;submitHash&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;limitSeconds&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;LockUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryLock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Solon&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;appName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;submitHash&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limitSeconds&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  @Whitelist
&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;@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;WhitelistCheckerImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;WhitelistChecker&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Whitelist&lt;/span&gt; &lt;span class="n"&gt;anno&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;realIp&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;CloudClient&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;span class="na"&gt;inListOfIp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"whitelist"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  @NotBlacklist
&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;@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;NotBlacklistCheckerImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;NotBlacklistChecker&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NotBlacklist&lt;/span&gt; &lt;span class="n"&gt;anno&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;realIp&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="nc"&gt;CloudClient&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;span class="na"&gt;inListOfIp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blacklist"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  @Logined
&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;@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;LoginedCheckerImpl&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;LoginedChecker&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Logined&lt;/span&gt; &lt;span class="n"&gt;anno&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&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;userKeyName&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;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionAsLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Custom Validators: Creating Your Own Annotation
&lt;/h2&gt;

&lt;p&gt;The framework is designed to be extended. Here's a complete example of creating a custom &lt;code&gt;@Phone&lt;/code&gt; validator:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the annotation
&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;@Target&lt;/span&gt;&lt;span class="o"&gt;({&lt;/span&gt;&lt;span class="nc"&gt;ElementType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PARAMETER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ElementType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FIELD&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@Retention&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RetentionPolicy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nd"&gt;@interface&lt;/span&gt; &lt;span class="nc"&gt;Phone&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="s"&gt;"Invalid phone number"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;[]&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;{};&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Implement the validator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PhoneValidator&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Validator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Phone&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;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Pattern&lt;/span&gt; &lt;span class="no"&gt;PHONE_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;Pattern&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"^1[3-9]\\d{9}$"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Phone&lt;/span&gt; &lt;span class="n"&gt;anno&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;anno&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;[]&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Phone&lt;/span&gt; &lt;span class="n"&gt;anno&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;anno&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="nf"&gt;validateOfValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Phone&lt;/span&gt; &lt;span class="n"&gt;anno&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;tmp&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;val&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&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;val&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;String&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PHONE_PATTERN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&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="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matches&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;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&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;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt; &lt;span class="nf"&gt;validateOfContext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Phone&lt;/span&gt; &lt;span class="n"&gt;anno&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;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StringBuilder&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PHONE_PATTERN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;matches&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;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;succeed&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;Result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;failure&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="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Register it
&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;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ValidationConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&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;registerValidators&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;ValidatorManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Phone&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PhoneValidator&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Use it
&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;@Valid&lt;/span&gt;
&lt;span class="nd"&gt;@Controller&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;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/phone"&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;setPhone&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Phone&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Validated&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 I'd Change
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No built-in &lt;code&gt;@Phone&lt;/code&gt; or &lt;code&gt;@URL&lt;/code&gt;&lt;/strong&gt; — common ones you'd expect. Easy to write yourself, but would be nice as pre-built options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;@Validated&lt;/code&gt; annotation name&lt;/strong&gt; — it does something different from both JSR 380's &lt;code&gt;@Valid&lt;/code&gt; and Spring's &lt;code&gt;@Validated&lt;/code&gt;. In Solon, &lt;code&gt;@Validated&lt;/code&gt; is specifically for triggering entity field validation on a parameter. It's fine once you know, but the naming overlap with JSR 380 caused me a moment of confusion.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Solon's validation is a complete, self-contained system. You get 20+ annotations out of the box, entity validation, group validation, and a clean extension point for custom validators — all without a single &lt;code&gt;javax.validation&lt;/code&gt; or &lt;code&gt;jakarta.validation&lt;/code&gt; dependency.&lt;/p&gt;

&lt;p&gt;The business-aware annotations (&lt;code&gt;@Whitelist&lt;/code&gt;, &lt;code&gt;@NoRepeatSubmit&lt;/code&gt;, &lt;code&gt;@Logined&lt;/code&gt;) are the standout feature. They let you enforce security and idempotency constraints at the validation layer rather than scattering checks throughout your handlers.&lt;/p&gt;

&lt;p&gt;For a project that wants to keep its dependency graph lean, Solon's validation is a solid choice.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
    </item>
    <item>
      <title>Solon Cache: Three Annotations, One Interface, and a Tags Trick You&amp;#39;ll Use Every Day</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Fri, 31 Jul 2026 01:12:45 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-cache-three-annotations-one-interface-and-a-tags-trick-you39ll-use-every-day-4aa0</link>
      <guid>https://dev.to/solonjava/solon-cache-three-annotations-one-interface-and-a-tags-trick-you39ll-use-every-day-4aa0</guid>
      <description>&lt;p&gt;I've been writing Solon for a while now, and one thing that surprised me early on was how clean the caching layer is. Not because it does anything revolutionary — it doesn't — but because it &lt;em&gt;stays out of your way&lt;/em&gt; while still handling the edge cases you actually hit in production.&lt;/p&gt;

&lt;p&gt;Let me walk through what I've learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Annotations
&lt;/h2&gt;

&lt;p&gt;Solon gives you exactly three annotations for caching. No more, no less:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@Cache&lt;/code&gt; — read-through cache. If the key exists, return it; otherwise run the method and store the result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@CachePut&lt;/code&gt; — always run the method, then update the cache with the result.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@CacheRemove&lt;/code&gt; — run the method, then evict one or more cache entries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a minimal example that shows all three:&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;@Controller&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;CacheController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${userId}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seconds&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="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/{userId}"&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;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&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;userId&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;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@CachePut&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${userId}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/{userId}/refresh"&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;User&lt;/span&gt; &lt;span class="nf"&gt;refreshUser&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;userId&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;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@CacheRemove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/{userId}/delete"&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;deleteUser&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;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;userService&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="n"&gt;userId&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;"deleted"&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;The &lt;code&gt;@CacheRemove(tags = "users")&lt;/code&gt; line is worth a closer look — it's doing something that's surprisingly hard with most annotation-based caching systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tags Trick
&lt;/h2&gt;

&lt;p&gt;Most caching frameworks give you key-based eviction. You know the key, you remove it. Fine for simple cases, but what about "invalidate all user-related caches"?&lt;/p&gt;

&lt;p&gt;That's where tags come in. When you annotate a method with &lt;code&gt;@Cache(tags = "users")&lt;/code&gt;, Solon doesn't just store the value under the computed key — it also records that key in a tag index. Later, when you call &lt;code&gt;@CacheRemove(tags = "users")&lt;/code&gt;, it finds all keys associated with that tag and evicts them all.&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;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${userId}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/{userId}"&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;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&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;userId&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="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${userId}:profile"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/user/{userId}/profile"&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;Profile&lt;/span&gt; &lt;span class="nf"&gt;getProfile&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;userId&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="c1"&gt;// Both caches get cleared in one shot&lt;/span&gt;
&lt;span class="nd"&gt;@CacheRemove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin/users/flush"&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;flushUsers&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;I use this pattern constantly for paginated lists. You cache page 1, page 2, page 3 of search results — all tagged with &lt;code&gt;"search"&lt;/code&gt;. When new data comes in, one &lt;code&gt;@CacheRemove(tags = "search")&lt;/code&gt; wipes them all.&lt;/p&gt;

&lt;p&gt;The docs do warn you about one thing: tags work by storing the collected keys as a list, so don't tag thousands of keys with the same tag. For most real-world scenarios — a few dozen to a few hundred keys per tag — it's perfectly fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Templates: Beyond Simple Strings
&lt;/h2&gt;

&lt;p&gt;The key parameter supports &lt;code&gt;${}&lt;/code&gt; expressions that resolve against method parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simple parameter&lt;/span&gt;
&lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${userId}"&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;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&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;userId&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="c1"&gt;// Nested property&lt;/span&gt;
&lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"user:${user.id}"&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;User&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDto&lt;/span&gt; &lt;span class="n"&gt;user&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="c1"&gt;// Map key&lt;/span&gt;
&lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"result:${params.type}"&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;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&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;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;params&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;If you don't specify a key, Solon auto-generates one from an MD5 of all parameter names and values. It works, but I'd recommend always providing an explicit key — it makes debugging and manual inspection much easier.&lt;/p&gt;

&lt;p&gt;There's also a special &lt;code&gt;${.property}&lt;/code&gt; syntax for referencing the return value, which is mainly useful with &lt;code&gt;@CachePut&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="nd"&gt;@CachePut&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order:${.orderId}"&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;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... create order, return it&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Interface That Makes It Swappable
&lt;/h2&gt;

&lt;p&gt;Under the hood, all caching goes through &lt;code&gt;CacheService&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;store&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;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;obj&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;seconds&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;get&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;key&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&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;key&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;Three methods. That's it. The default implementation is &lt;code&gt;LocalCacheService&lt;/code&gt; — an in-memory cache that works fine for single-instance apps.&lt;/p&gt;

&lt;p&gt;But the real power is that you can swap it out with a single bean definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${cache}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Properties&lt;/span&gt; &lt;span class="n"&gt;props&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;MemCacheService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you define a &lt;code&gt;CacheService&lt;/code&gt; bean, it replaces the default globally. All &lt;code&gt;@Cache&lt;/code&gt;, &lt;code&gt;@CachePut&lt;/code&gt;, and &lt;code&gt;@CacheRemove&lt;/code&gt; annotations automatically use the new backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching Backends by Config
&lt;/h2&gt;

&lt;p&gt;Solon also provides a &lt;code&gt;CacheServiceSupplier&lt;/code&gt; that picks the backend based on config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;demo.cache1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;driverType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local"&lt;/span&gt;
  &lt;span class="c1"&gt;# driverType: "redis"&lt;/span&gt;
  &lt;span class="c1"&gt;# driverType: "memcached"&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="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cache1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.cache1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CacheServiceSupplier&lt;/span&gt; &lt;span class="n"&gt;supplier&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;supplier&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported backends out of the box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Plugin&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;Local&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon-data&lt;/code&gt; (built-in)&lt;/td&gt;
&lt;td&gt;Default, no extra dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;solon-cache-jedis&lt;/code&gt; or &lt;code&gt;solon-cache-redisson&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pick your client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memcached&lt;/td&gt;
&lt;td&gt;&lt;code&gt;solon-cache-spymemcached&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Old but still around&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Second-Level Cache: Local + Remote
&lt;/h2&gt;

&lt;p&gt;One pattern I've found useful in read-heavy services is the second-level cache. It layers a local in-memory cache in front of a remote one (Redis, Memcached):&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="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cache1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.cache1}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;CacheServiceSupplier&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;CacheService&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;supplier&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;LocalCacheService&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;remote&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Local L1 + remote L2, with a 5-second buffer&lt;/span&gt;
    &lt;span class="nc"&gt;LocalCacheService&lt;/span&gt; &lt;span class="n"&gt;local&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;LocalCacheService&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;SecondCacheService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "buffer" means: when the local cache misses but the remote cache hits, the result is stored locally for 5 seconds. Subsequent reads within those 5 seconds hit the local cache directly — no network round trip. For hot data, this can cut Redis load significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partition Design
&lt;/h2&gt;

&lt;p&gt;Once you have multiple services sharing the same Redis instance, you'll want to partition your caches. Solon supports this through named cache services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;demo.cache.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;keyHeader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user"&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:6379"&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

&lt;span class="na"&gt;demo.cache.order&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;keyHeader&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order"&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost:6379"&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Config&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache_user"&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;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cacheUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.cache.user}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;RedisCacheService&lt;/span&gt; &lt;span class="n"&gt;cache&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;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableMd5key&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="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cache_order"&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;CacheService&lt;/span&gt; &lt;span class="nf"&gt;cacheOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${demo.cache.order}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;RedisCacheService&lt;/span&gt; &lt;span class="n"&gt;cache&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;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableMd5key&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use them in your services:&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;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Cache&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cache_order"&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;hello&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;name&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;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello %s!"&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="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;Different databases, different key prefixes, and the annotation just needs a &lt;code&gt;service&lt;/code&gt; attribute to pick the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Change
&lt;/h2&gt;

&lt;p&gt;If I had one nit to pick, it's that the auto-generated key (when you don't specify one) is an MD5 of all parameters. In practice, I always specify an explicit key, so this hasn't been an issue for me. But for someone new to the framework, it might cause confusion when they see opaque hash strings in their cache store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Solon's caching layer is refreshingly simple. Three annotations, one interface, and a tags system that handles batch invalidation better than most frameworks I've used. The &lt;code&gt;CacheService&lt;/code&gt; interface is trivial to implement, which means you can plug in basically any backend you want — or write a custom one for your specific use case.&lt;/p&gt;

&lt;p&gt;If you're already using Solon, the caching annotations are in the &lt;code&gt;solon-data&lt;/code&gt; module, which you probably already have as a dependency. Just add &lt;code&gt;@Cache&lt;/code&gt; and you're done.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Solon WebSocket: Listener Interface Over Annotation Maze</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 30 Jul 2026 17:03:33 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-websocket-listener-interface-over-annotation-maze-3eja</link>
      <guid>https://dev.to/solonjava/solon-websocket-listener-interface-over-annotation-maze-3eja</guid>
      <description>&lt;p&gt;Solon's WebSocket support is one of those areas where the design diverges clearly from what most Java developers expect. If you've built WebSocket endpoints with JSR-356 — the standard that Spring, Tomcat, and most Java EE containers follow — Solon does it differently. Not worse, just different. And once the pattern clicks, it's actually cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Key Difference: Listeners, Not Annotations
&lt;/h2&gt;

&lt;p&gt;JSR-356 WebSocket development looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/chat"&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;ChatEndpoint&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@OnOpen&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;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt; &lt;span class="n"&gt;session&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="nd"&gt;@OnMessage&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;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt; &lt;span class="n"&gt;session&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;message&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="nd"&gt;@OnClose&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;onClose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solon replaces the &lt;code&gt;@OnOpen&lt;/code&gt;, &lt;code&gt;@OnMessage&lt;/code&gt;, &lt;code&gt;@OnClose&lt;/code&gt;, &lt;code&gt;@OnError&lt;/code&gt; annotations with a &lt;code&gt;WebSocketListener&lt;/code&gt; interface. You keep &lt;code&gt;@ServerEndpoint&lt;/code&gt; for path declaration, but lifecycle handling is interface-based:&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;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/chat/{roomId}"&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;ChatEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SimpleWebSocketListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"roomId"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// path variable&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"token"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// query param ?token=...&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;isValidToken&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@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;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Echo: "&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="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;SimpleWebSocketListener&lt;/code&gt; lets you override only the methods you need. For the full interface, implement &lt;code&gt;WebSocketListener&lt;/code&gt; directly — it exposes &lt;code&gt;onOpen&lt;/code&gt;, &lt;code&gt;onMessage&lt;/code&gt; (text + binary), &lt;code&gt;onClose&lt;/code&gt;, and &lt;code&gt;onError&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Deployment Modes
&lt;/h2&gt;

&lt;p&gt;Solon WebSocket works in two configurations depending on which server plugin you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared port&lt;/strong&gt; — HTTP and WebSocket traffic share the same port. Most server plugins support this out of the box:&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="c"&gt;&amp;lt;!-- smart-http (built-in WS, no extra plugin needed) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-smarthttp&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Jetty v9 (javax) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-jetty&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-jetty-add-websocket&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Tomcat v9 (javax, requires v3.7.3+) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-tomcat&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-tomcat-add-websocket&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Independent port&lt;/strong&gt; — a dedicated WebSocket server runs on a separate port (default: main port + 10000):&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="c"&gt;&amp;lt;!-- Java-WebSocket (nio), 0.4Mb, jdk 8+ --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-websocket&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Netty (nio), 3.6Mb, jdk 8+, available since v2.3.5 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.noear&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;solon-server-websocket-netty&lt;span class="nt"&gt;&amp;lt;/artifactId&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;Configure the WS port in &lt;code&gt;app.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server.websocket.port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No version numbers needed in dependency declarations — the Solon BOM manages them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Startup
&lt;/h2&gt;

&lt;p&gt;Enable WebSocket in your main class:&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;class&lt;/span&gt; &lt;span class="nc"&gt;App&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Solon&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="nc"&gt;App&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;args&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableWebSocket&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="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the only wiring required. Your &lt;code&gt;@ServerEndpoint&lt;/code&gt; classes are auto-discovered through Solon's IoC container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Path Variables and Query Parameters
&lt;/h2&gt;

&lt;p&gt;Both are accessed through &lt;code&gt;socket.param()&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="nd"&gt;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/room/{id}"&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;RoomEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SimpleWebSocketListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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="c1"&gt;// from /ws/room/42&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// from ?userId=abc&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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connected: room="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;roomId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" user="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// broadcast would go here&lt;/span&gt;
        &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[room:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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="s"&gt;"] "&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="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;)&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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Disconnected: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-Channel Routing
&lt;/h2&gt;

&lt;p&gt;Register multiple endpoints to separate concerns by path:&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;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/user/"&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;UserChannel&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SimpleWebSocketListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user channel: "&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="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/admin/"&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;AdminChannel&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SimpleWebSocketListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@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;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"admin channel: "&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="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;
  
  
  Advanced: Pipeline Routing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PipelineWebSocketListener&lt;/code&gt; and &lt;code&gt;PathWebSocketListener&lt;/code&gt; let you build layered routing within a single endpoint — useful for middleware-style pre-processing plus path-specific handlers:&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;@ServerEndpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/app/**"&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;AppEndpoint&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PipelineWebSocketListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;AppEndpoint&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// First layer: auth interceptor (runs for all paths under /ws/app/**)&lt;/span&gt;
        &lt;span class="n"&gt;next&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;SimpleWebSocketListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;@Override&lt;/span&gt;
            &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"token"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&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="c1"&gt;// Second layer: path-based dispatch&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&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;PathWebSocketListener&lt;/span&gt;&lt;span class="o"&gt;()&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;"/ws/app/chat/{id}"&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;SimpleWebSocketListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;@Override&lt;/span&gt;
                &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&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="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chat["&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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="s"&gt;"]: "&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="o"&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;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ws/app/notify"&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;SimpleWebSocketListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;@Override&lt;/span&gt;
                &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"notification stream connected"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;})&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Global Interceptors via WebSocketRouter
&lt;/h2&gt;

&lt;p&gt;For cross-cutting logic that applies to all WebSocket connections regardless of path, inject &lt;code&gt;WebSocketRouter&lt;/code&gt; in a &lt;code&gt;@Configuration&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@Bean&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;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Inject&lt;/span&gt; &lt;span class="nc"&gt;WebSocketRouter&lt;/span&gt; &lt;span class="n"&gt;webSocketRouter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;webSocketRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;before&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;SimpleWebSocketListener&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;@Override&lt;/span&gt;
            &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOpen&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WebSocket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// e.g., rate limiting, logging, connection tracking&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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WS connected from: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remoteAddress&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Choosing Your Plugin
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plugin&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;JDK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solon-server-smarthttp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;smart-http (aio)&lt;/td&gt;
&lt;td&gt;0.8 Mb&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;solon-server-jetty&lt;/code&gt; + &lt;code&gt;add-websocket&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Jetty v9 (nio)&lt;/td&gt;
&lt;td&gt;2.7 Mb&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;solon-server-jetty-jakarta&lt;/code&gt; + &lt;code&gt;add-websocket-jakarta&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Jetty v12 (nio)&lt;/td&gt;
&lt;td&gt;3.9 Mb&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;17+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;solon-server-tomcat&lt;/code&gt; + &lt;code&gt;add-websocket&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tomcat v9&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solon-server-undertow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Undertow (nio)&lt;/td&gt;
&lt;td&gt;4.6 Mb&lt;/td&gt;
&lt;td&gt;shared&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solon-server-websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Java-WebSocket (nio)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4 Mb&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;independent&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solon-server-websocket-netty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Netty (nio)&lt;/td&gt;
&lt;td&gt;3.6 Mb&lt;/td&gt;
&lt;td&gt;independent&lt;/td&gt;
&lt;td&gt;8+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most services where HTTP and WS share the same host, &lt;code&gt;solon-server-smarthttp&lt;/code&gt; is the lowest-friction option — WS support is built in with no additional dependency. If you need a standalone WebSocket service with minimal footprint, &lt;code&gt;solon-server-websocket&lt;/code&gt; at 0.4 Mb is hard to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Not There
&lt;/h2&gt;

&lt;p&gt;Solon WebSocket doesn't implement the JSR-356 API. If you're migrating code that directly uses &lt;code&gt;javax.websocket.Session&lt;/code&gt;, &lt;code&gt;@ServerEndpoint&lt;/code&gt; in the &lt;code&gt;javax.websocket&lt;/code&gt; namespace, or JSR-356 encoders/decoders — that code needs to be rewritten to the listener interface pattern. It's not a large rewrite, but it's a real one.&lt;/p&gt;

&lt;p&gt;The flip side is that the listener pattern is easier to test (just instantiate the listener class and call its methods) and easier to compose (the pipeline/path listeners give you explicit, readable routing without annotation scanning surprises).&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;WebSocket architecture overview: &lt;a href="https://solon.noear.org/article/332" rel="noopener noreferrer"&gt;https://solon.noear.org/article/332&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Core listener API: &lt;a href="https://solon.noear.org/article/643" rel="noopener noreferrer"&gt;https://solon.noear.org/article/643&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pipeline and path routing: &lt;a href="https://solon.noear.org/article/645" rel="noopener noreferrer"&gt;https://solon.noear.org/article/645&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Plugin index: &lt;a href="https://solon.noear.org/article/family-solon-server" rel="noopener noreferrer"&gt;https://solon.noear.org/article/family-solon-server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Solon State Machine: When if-else Spaghetti Meets a Cleaner Pattern</title>
      <dc:creator>Solon Framework</dc:creator>
      <pubDate>Thu, 30 Jul 2026 16:17:32 +0000</pubDate>
      <link>https://dev.to/solonjava/solon-state-machine-when-if-else-spaghetti-meets-a-cleaner-pattern-1ca6</link>
      <guid>https://dev.to/solonjava/solon-state-machine-when-if-else-spaghetti-meets-a-cleaner-pattern-1ca6</guid>
      <description>&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;A state machine has four building blocks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&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;strong&gt;State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A condition the system is in (e.g., "CREATED", "PAID")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Event&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Something that happens (e.g., "PAY", "SHIP")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What runs when a transition happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The rule: from state X, on event Y, go to state Z (optionally if condition W, and do action A)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Solon's &lt;code&gt;StateMachine&amp;lt;State, Event, Payload&amp;gt;&lt;/code&gt; ties them together with a fluent DSL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello World: A Light Switch
&lt;/h2&gt;

&lt;p&gt;Add the dependency:&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;org.noear&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;solon-statemachine&lt;span class="nt"&gt;&amp;lt;/artifactId&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;Now, a light switch that toggles between OFF and ON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.statemachine.EventContext&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.noear.solon.statemachine.StateMachine&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;LightSwitchApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="no"&gt;PRESS&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="no"&gt;OFF&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ON&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stateMachine&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;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// OFF + PRESS -&amp;gt; ON&lt;/span&gt;
        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTransition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&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;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OFF&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PRESS&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;-&amp;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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Light is ON"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// ON + PRESS -&amp;gt; OFF&lt;/span&gt;
        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTransition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&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;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ON&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PRESS&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OFF&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;-&amp;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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Light is OFF"&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;

        &lt;span class="c1"&gt;// Test it&lt;/span&gt;
        &lt;span class="nc"&gt;State&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;OFF&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;currentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PRESS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;EventContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&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="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="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New state: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;currentState&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;Run it, and the light toggles back and forth. The &lt;code&gt;sendEvent()&lt;/code&gt; returns the new state -- you don't manage a state variable manually; you just hand over the current state and let the machine compute the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DSL Breakdown
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;StateTransitionDecl&lt;/code&gt; has five methods:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Required&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;.from(state)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Source state(s) -- can have multiple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.to(state)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Target state (one)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.on(event)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Triggering event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.when(condition)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional guard condition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.then(action)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Optional action to execute&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;when&lt;/code&gt; and &lt;code&gt;then&lt;/code&gt; are optional. If you just need a simple state transition without side effects:&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;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTransition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;t&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;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DRAFT&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PUBLISHED&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PUBLISH&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: Order State Machine
&lt;/h2&gt;

&lt;p&gt;Let's model an order lifecycle with states &lt;code&gt;NONE -&amp;gt; CREATED -&amp;gt; PAID -&amp;gt; SHIPPED -&amp;gt; DELIVERED&lt;/code&gt;, with optional cancellation paths.&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;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderEvent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="no"&gt;CREATE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PAY&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SHIP&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;DELIVER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CANCEL&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;enum&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="no"&gt;NONE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;PAID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SHIPPED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;DELIVERED&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CANCELLED&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define an Order payload that implements &lt;code&gt;EventContext&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;EventContext&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OrderState&lt;/span&gt; &lt;span class="nf"&gt;getState&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;state&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;getPayload&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;this&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;setState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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;this&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="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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setStatus&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;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now build the state machine by extending &lt;code&gt;StateMachine&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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStateMachine&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderEvent&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="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderStateMachine&lt;/span&gt;&lt;span class="o"&gt;()&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="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NONE&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&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="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPayload&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Created"&lt;/span&gt;&lt;span class="o"&gt;);&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="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CREATED&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PAY&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPayload&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Paid"&lt;/span&gt;&lt;span class="o"&gt;);&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="nc"&gt;OrderState&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="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SHIP&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPayload&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&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="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Shipped"&lt;/span&gt;&lt;span class="o"&gt;);&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="nc"&gt;OrderState&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="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DELIVER&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DELIVERED&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPayload&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DELIVERED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Delivered"&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;Test it:&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;class&lt;/span&gt; &lt;span class="nc"&gt;OrderTest&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;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;OrderStateMachine&lt;/span&gt; &lt;span class="n"&gt;stateMachine&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;OrderStateMachine&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&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;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"iPhone 16 Pro Max"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CREATE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PAY&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SHIP&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;stateMachine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DELIVER&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EventContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getState&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;order&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;
  
  
  Two Usage Styles
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Lambda style (composition)
&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;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Payload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sm&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;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTransition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&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="no"&gt;X&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;span class="o"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Inheritance style
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyMachine&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;StateMachine&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;State&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Payload&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="nf"&gt;MyMachine&lt;/span&gt;&lt;span class="o"&gt;()&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="no"&gt;X&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;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;Both produce the same result. Use composition when you need multiple independent machines; use inheritance when the machine is a self-contained domain concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use It (and When Not To)
&lt;/h2&gt;

&lt;p&gt;Solon State Machine is designed as a &lt;strong&gt;complement&lt;/strong&gt; to Solon Flow, not a replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use State Machine when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have discrete states and events (order lifecycle, document workflow, game state)&lt;/li&gt;
&lt;li&gt;You want a declarative alternative to nested if-else or switch statements&lt;/li&gt;
&lt;li&gt;You don't need persistence -- the state is maintained in memory via the event context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Solon Flow when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need persistent, resumable long-running processes&lt;/li&gt;
&lt;li&gt;You need a visual designer for business users&lt;/li&gt;
&lt;li&gt;You need complex branching, parallel execution, or sub-processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two can coexist in the same project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Picture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No persistence.&lt;/strong&gt; The state machine is stateless by design -- you pass the current state in via EventContext, and the machine returns the new state. You own the storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No visual designer.&lt;/strong&gt; Unlike Solon Flow which has a web-based designer, the state machine is code-only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No built-in event bus integration.&lt;/strong&gt; State transitions trigger &lt;code&gt;.then()&lt;/code&gt; actions synchronously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple but not powerful for complex workflows.&lt;/strong&gt; If your state logic involves parallel states or nested state machines, Solon Flow is the better choice.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dependency&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;org.noear:solon-statemachine&lt;/code&gt; (since v3.4.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StateMachine&amp;lt;State, Event, Payload&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DSL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.from().on().to().when().then()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EventContext.of(state, payload)&lt;/code&gt; or custom impl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;None (caller-owned, context-passed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Order lifecycle, document workflow, game state, toggles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All examples in this article are from the official documentation at &lt;a href="https://solon.noear.org/article/1236" rel="noopener noreferrer"&gt;solon.noear.org&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>java</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
