<?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>Forem: nomad4tech</title>
    <description>The latest articles on Forem by nomad4tech (@nomad4tech).</description>
    <link>https://forem.com/nomad4tech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3774335%2F61f8f81c-f870-4644-9a84-815769d52246.png</url>
      <title>Forem: nomad4tech</title>
      <link>https://forem.com/nomad4tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nomad4tech"/>
    <language>en</language>
    <item>
      <title>SOLID was written for this moment</title>
      <dc:creator>nomad4tech</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:35:31 +0000</pubDate>
      <link>https://forem.com/nomad4tech/solid-was-written-for-this-moment-225f</link>
      <guid>https://forem.com/nomad4tech/solid-was-written-for-this-moment-225f</guid>
      <description>&lt;p&gt;&lt;strong&gt;If You dont write code - who you are?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f2hge002wsr87opouui.jpg" alt="aq9cmj" width="800" height="329"&gt;
&lt;/h2&gt;

&lt;p&gt;Everyone is talking about prompts. About token budgets. About cognitive surface. But I think we're still looking at the wrong unit.&lt;/p&gt;

&lt;p&gt;The unit is the &lt;strong&gt;interface boundary&lt;/strong&gt;. Always was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Back before any of this existed
&lt;/h2&gt;

&lt;p&gt;Back when I was writing SQL scripts and mappers by hand, I already had this principle baked into how I worked: every module should know only its contract. Not where the data came from. Not where it goes. Just: here's input, here's output, here's the guarantee.&lt;/p&gt;

&lt;p&gt;Think of it like a bakery. The baker doesn't care if the flour came from the warehouse or yesterday's leftover stock. Doesn't care where the bread ends up - the store shelf, a restaurant, a catering order. He just bakes. His job is defined by its boundaries, not by the full supply chain.&lt;/p&gt;

&lt;p&gt;That's how I tried to design every system. Each module gets a clear spec. It does its one job. It exposes a clean interface. It knows nothing about its neighbors' internals.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it worked then
&lt;/h2&gt;

&lt;p&gt;The benefits were practical and immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easier to test&lt;/strong&gt; - validate each piece in isolation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to mock&lt;/strong&gt; - swap out any dependency without touching the rest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to hand off&lt;/strong&gt; - a new developer can own a module without understanding the whole system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to explain&lt;/strong&gt; - narrow scope means clear documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to replace&lt;/strong&gt; - swap implementations without breaking contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different developers could work on different modules in parallel. The system remained understandable even as it grew. Each piece was small enough to reason about, flexible enough to move.&lt;/p&gt;

&lt;p&gt;This was the goal before generative AI existed. Before vibe-coding was a word. Before anyone was talking about agents.&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.amazonaws.com%2Fuploads%2Farticles%2Fo9v4taep2cfkfena0lr1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo9v4taep2cfkfena0lr1.jpg" alt="aq9e6n" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed with LLMs
&lt;/h2&gt;

&lt;p&gt;The developer is now Claude Code, Cursor, or whatever agent you're using.&lt;/p&gt;

&lt;p&gt;And here's the thing - a senior AI developer needs exactly what a senior human developer needs: a &lt;strong&gt;clear spec&lt;/strong&gt;, a &lt;strong&gt;narrow scope&lt;/strong&gt;, and &lt;strong&gt;no hidden dependencies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The same reasons this worked with human developers make it work with agents. The principles didn't change. The developer did.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principle&lt;/th&gt;
&lt;th&gt;With AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easier to test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Generated code lives in one small module - trivial to test without touching the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easier to mock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Try different implementations, swap models, experiment freely - the contract holds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easier to hand off&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drop 5 files into context instead of scanning an entire repo. The model gets exactly what it needs, nothing more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easier to explain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A narrow module is a narrow prompt. Less ambiguity, fewer hallucinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Easier to replace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If the generated code doesn't fit - throw it away and regenerate. The interface survives&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvis0yxz3j4q1cx1ar8z3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvis0yxz3j4q1cx1ar8z3.jpg" alt="aq9d56" width="500" height="500"&gt;&lt;/a&gt;&lt;br&gt;
A well-isolated module with a clean interface can be handed to an agent with a minimal prompt and a clear contract. The agent doesn't need to understand your entire codebase. It needs to understand &lt;em&gt;this boundary&lt;/em&gt; - what comes in, what goes out, what the guarantee is.&lt;/p&gt;

&lt;p&gt;The rest of the system doesn't care how that module was built. The interface is the guarantee.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real shift
&lt;/h2&gt;

&lt;p&gt;We're not developers anymore. We're &lt;strong&gt;architects&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The generative AI is our senior dev. Our job is to design the factory floor - the contracts, the flows, the boundaries, the guarantees. Not to run the machines.&lt;/p&gt;

&lt;p&gt;This is a mindset shift, not just a workflow change. When you stop thinking "how do I write this code" and start thinking "how do I design this boundary so that anyone - or anything - can implement it correctly", the whole game changes.&lt;/p&gt;

&lt;p&gt;Your value is no longer in writing the implementation. It's in designing a system where every implementation slot is small, clear, and replaceable.&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.amazonaws.com%2Fuploads%2Farticles%2F8qw96tlwijaz5qih8un6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8qw96tlwijaz5qih8un6.jpg" alt="aq9dju" width="717" height="349"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The founding fathers weren't early. They were right.
&lt;/h2&gt;

&lt;p&gt;SOLID didn't become obsolete with generative AI and vibe-coding.&lt;/p&gt;

&lt;p&gt;It became &lt;strong&gt;urgent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Robert Martin wrote about single responsibility, open/closed design, dependency inversion - principles that make systems maintainable, modular, and understandable. At the time, these were best practices for teams of human developers.&lt;/p&gt;

&lt;p&gt;Turns out they're also the exact principles that make a system agent-friendly.&lt;/p&gt;

&lt;p&gt;Small responsibilities. Clean interfaces. No hidden coupling. Contracts over implementations.&lt;/p&gt;

&lt;p&gt;If you haven't revisited Uncle Bob lately, maybe now is the time. Not because the principles are new. Because we finally have a use case that makes their value undeniable.&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.amazonaws.com%2Fuploads%2Farticles%2Flpluw0093cwandtx8i70.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpluw0093cwandtx8i70.jpg" alt="Bob" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The prompt is not the unit of architecture. The interface boundary is.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;It always was.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>I Tired of Trusting Bash Scripts With My Backups</title>
      <dc:creator>nomad4tech</dc:creator>
      <pubDate>Mon, 27 Apr 2026 11:24:56 +0000</pubDate>
      <link>https://forem.com/nomad4tech/i-tired-of-trusting-bash-scripts-with-my-backups-4ke4</link>
      <guid>https://forem.com/nomad4tech/i-tired-of-trusting-bash-scripts-with-my-backups-4ke4</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I created a self-hosted web UI tool for PostgreSQL/MySQL/MariaDB backups from Docker containers. Auto-discovers containers, runs dumps inside them (so pg_dump always matches your DB version), no credentials in config files. Free, MIT licensed, Linux only&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One docker compose up - your databases, backed up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/nomad4tech/backup-manager" rel="noopener noreferrer"&gt;https://github.com/nomad4tech/backup-manager&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Hub:&lt;/strong&gt; &lt;a href="https://hub.docker.com/r/nomad4tech/backup-manager" rel="noopener noreferrer"&gt;https://hub.docker.com/r/nomad4tech/backup-manager&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short Demo:&lt;/strong&gt; &lt;a href="https://youtu.be/3rXkPmOpDNc" rel="noopener noreferrer"&gt;https://youtu.be/3rXkPmOpDNc&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How I got here
&lt;/h2&gt;

&lt;p&gt;I've used and seen others use bash scripts for backups. Usually it's a massive script, or a set of scripts and pipelines - at minimum: create a dump, compress it, upload to S3, delete old files. All wired up via cron or systemd.&lt;/p&gt;

&lt;p&gt;I've watched this approach fail in ways that hurt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;someone deleted the crontab. ("crontab -r" instead of "crontab -e" its terrifyingly easy to mistype, and you won't even notice)&lt;/li&gt;
&lt;li&gt;someone deleted the backup script itself during a cleanup&lt;/li&gt;
&lt;li&gt;dump silently failed because there wasn't enough disk space, or the database container was restarted mid-dump&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every case, we found out the same way: when a restore was actually needed, and the last backup was five months old.&lt;/p&gt;

&lt;p&gt;I'm a Java developer and an aspiring vibe coder (not sure whether to be proud or embarrassed about that). At some point, in my free time, I decided to solve this for myself - it was supposed to be a simple JAR: a config file, a backup pipeline, notifications. That's it.&lt;/p&gt;

&lt;p&gt;Then I added an API. Then a frontend. Then it kept going...&lt;/p&gt;

&lt;p&gt;The app grew into a proper self-hosted tool. I played around with Docker Hub and GitHub,and at some point I wanted to take it further - to build something useful for the community. Something approachable, lightweight, and worth maintaining as an open-source project.&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.amazonaws.com%2Fuploads%2Farticles%2Fx1iqpubvw7t3gkxkxi9w.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.amazonaws.com%2Fuploads%2Farticles%2Fx1iqpubvw7t3gkxkxi9w.png" alt="Backup Manager task list page" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "just use pgBackRest/restic/Barman" wasn't my answer
&lt;/h2&gt;

&lt;p&gt;My answer: those are better tools for serious workloads. If you need incremental backups, WAL archiving, point-in-time recovery - go use pgBackRest. It's excellent - I have a lot to learn from it.&lt;/p&gt;

&lt;p&gt;But my situation was simpler: multiple servers, multiple Docker-based projects, databases living in containers. I wanted to open a UI, create a task, and not think about it again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;bash script&lt;/strong&gt; - I write it, I maintain it, I remember to add rotation, error handling, disk space checks. I already did that. Then I built this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restic&lt;/strong&gt; - powerful, but no Docker-native workflow, no UI, setup per-project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgBackRest&lt;/strong&gt; - its own config system, its own repository concept. Overkill for "give me a daily dump and email me if it fails."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backup Manager is for people who want to &lt;strong&gt;click a button, not write a config.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea: one place to manage all your backups
&lt;/h2&gt;

&lt;p&gt;The original problem wasn't just "automate a dump." It was: multiple servers, multiple Docker containers, no single place to see what's backed up and what isn't.&lt;/p&gt;

&lt;p&gt;The solution I wanted was simple - connect to any server, pick a container and DB, set a schedule, done. Everything in one UI. No SSH-ing into machines to check cron logs, no wondering if the script on server #14526 is still running.&lt;/p&gt;

&lt;p&gt;The technical decision that makes this work cleanly: Backup Manager runs dumps inside the container via Docker exec API, not on the host. Three things fall out of this naturally:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. pg_dump always matches your database version.&lt;/strong&gt; No "client/server version mismatch" errors. Ever. The binary is the one that shipped with your database image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. No credentials in config files.&lt;/strong&gt; You don't even need to know database credentials at all. PostgreSQL resolves &lt;code&gt;$POSTGRES_USER&lt;/code&gt;/&lt;code&gt;$PGPASSWORD&lt;/code&gt; from the container's environment. MySQL uses &lt;code&gt;$MYSQL_ROOT_PASSWORD&lt;/code&gt;. The backup tool never sees them - they're resolved at dump time, inside the container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data streams directly to the app host.&lt;/strong&gt; No temporary files on the database server. No memory buffering regardless of database size. For large databases this matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  A few other things worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Auto-discovery.&lt;/strong&gt; The app finds database containers automatically - you pick from a list, not a config file. Database size is shown in the selection screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disk space pre-flight.&lt;/strong&gt; Before every dump, free space is checked against 1.5× the size of the previous dump. If there isn't enough room, the dump won't start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File rotation.&lt;/strong&gt; Set how many backups to keep. Old files are deleted automatically after each successful backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote servers via SSH.&lt;/strong&gt; Add an SSH connection and all databases on that host appear in the same UI. The Docker socket is proxied through the SSH tunnel - it's never exposed directly to the network. This works the same way Portainer and Watchtower handle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 upload is optional.&lt;/strong&gt; AWS, MinIO, Yandex Cloud, or any S3-compatible storage. If S3 is unavailable, the dump still runs and saves locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email notifications on success and failure.&lt;/strong&gt; Multiple recipients supported - useful when a backup task belongs to a shared server and more than one person needs to know if something breaks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heartbeat monitoring.&lt;/strong&gt; The app pings healthchecks.io (or any compatible service) by schedule. If the app goes down - you get an alert. If the monitoring service itself goes down - Backup Manager emails you. Silence is a signal, not a green light.&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.amazonaws.com%2Fuploads%2Farticles%2Fvfmc6ljpzhf5gv8hsb1u.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvfmc6ljpzhf5gv8hsb1u.gif" alt="Task Wizard" width="760" height="668"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flye33hl1pc7kaqrpck29.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flye33hl1pc7kaqrpck29.gif" alt="Task Run" width="760" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backup-manager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nomad4tech/backup-manager:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&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;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./backups:/app/backups&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;SPRING_PROFILES_ACTIVE=docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local Docker socket is detected automatically. Default login: &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt; - change it immediately in Settings -&amp;gt; Account.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else is included
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Gzip compression enabled by default, per task&lt;/li&gt;
&lt;li&gt;Test S3, email, and heartbeat integrations directly from the UI before saving&lt;/li&gt;
&lt;li&gt;Inline task editing - no need to recreate tasks to change a schedule&lt;/li&gt;
&lt;li&gt;Full backup history with details&lt;/li&gt;
&lt;li&gt;REST API with Swagger documentation&lt;/li&gt;
&lt;li&gt;Container re-creation via &lt;code&gt;docker-compose down/up&lt;/code&gt; doesn't break tasks - containers are resolved by name, not ID&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Limitations (being honest)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux only&lt;/strong&gt; - Windows and macOS are on the roadmap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early stage&lt;/strong&gt; - tested on databases up to ~500 GB, but edge cases are still being found&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Involvement&lt;/strong&gt; - I used AI coding assistants to speed up development. However, all architectural decisions, core logic, and testing strategies are entirely mine

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;💡 A Note on the UI&lt;/strong&gt; I’m a Java dev, not a frontend engineer. I’ll be honest: the web interface was &lt;strong&gt;100% generated by AI assistants&lt;/strong&gt;. My role was defining the UX flow, API contracts, and wiring it all together. It’s a testament to how far developer tools have come&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Single maintainer&lt;/strong&gt; - that's me! Response times may vary depending on my availability, but feedback is always welcome&lt;/li&gt;

&lt;/ul&gt;




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

&lt;p&gt;UI-based restore, backup encryption, webhooks, MongoDB support, incremental backups...&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm sharing this now
&lt;/h2&gt;

&lt;p&gt;This is my first self-hosted tool I'm putting out publicly. It's not perfect. But I use it on my own production servers, and it does exactly what I built it to do.&lt;/p&gt;

&lt;p&gt;The best thing that can happen to it at this stage is real-world usage on setups I didn't anticipate - databases I haven't tested, network configs I haven't seen, edge cases I haven't hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this looks useful - try it and break it.&lt;/strong&gt; -&amp;gt; &lt;a href="https://github.com/nomad4tech/backup-manager" rel="noopener noreferrer"&gt;https://github.com/nomad4tech/backup-manager&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Issues, stars, and feedback all help. If something breaks or doesn't work the way you expect - open an issue. That's genuinely the most valuable thing anyone can do for the project right now.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>opensource</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Building a Minimal Telegram Bot Library for Java - Handler Chain Pattern</title>
      <dc:creator>nomad4tech</dc:creator>
      <pubDate>Sun, 15 Feb 2026 18:51:12 +0000</pubDate>
      <link>https://forem.com/nomad4tech/building-a-minimal-telegram-bot-library-for-java-handler-chain-pattern-3kmb</link>
      <guid>https://forem.com/nomad4tech/building-a-minimal-telegram-bot-library-for-java-handler-chain-pattern-3kmb</guid>
      <description>&lt;p&gt;I needed a Telegram bot for a side project. Looked at existing Java libraries - they're packed with features I didn't need. All I wanted was to send API requests and route updates to handlers based on logic I control. So I built my own thin wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most Telegram bot libraries for Java come with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heavy abstractions (command frameworks, conversation flows, state machines)&lt;/li&gt;
&lt;li&gt;Opinionated architectures&lt;/li&gt;
&lt;li&gt;Dependencies you might not want&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a simple bot that monitors groups and sends notifications, this felt like overkill. I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct access to Telegram API&lt;/li&gt;
&lt;li&gt;Custom routing logic without fighting a framework&lt;/li&gt;
&lt;li&gt;Minimal dependencies (just HTTP client + JSON)&lt;/li&gt;
&lt;li&gt;Works standalone or drops into Spring Boot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the real issue wasn't features - it was &lt;strong&gt;cognitive overhead&lt;/strong&gt;. Every library has its own mental model: "our way of structuring commands," "our conversation state system," "our middleware pipeline." You spend time learning the library's abstractions, hunting through docs for configuration options, debugging implicit behaviors that aren't mentioned anywhere.&lt;/p&gt;

&lt;p&gt;And then you hit the edge cases. The library does 90% of what you need, but that last 10% requires fighting the framework. You're three layers deep in someone else's architecture trying to figure out why your handler isn't firing, or why metrics are being logged to a format you don't use.&lt;/p&gt;

&lt;p&gt;Sometimes it's easier to write your own "bicycle" - but one that takes you from point A to point B, not one that tries to perform heart surgery and deliver a newspaper on the way. I just wanted to call Telegram API and route updates. Why learn someone else's architecture for that?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Handler Chain
&lt;/h2&gt;

&lt;p&gt;The core idea is simple - each handler decides if it processes an update:&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;@FunctionalInterface&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UpdateHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Update&lt;/span&gt; &lt;span class="n"&gt;update&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;Return &lt;code&gt;true&lt;/code&gt; → "I handled it, stop the chain"&lt;br&gt;&lt;br&gt;
Return &lt;code&gt;false&lt;/code&gt; → "Not mine, try next handler"&lt;/p&gt;

&lt;p&gt;That's the entire pattern. No magic, no annotations, no forced structure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Example Handler
&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;StartCommandHandler&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;UpdateHandler&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;TelegramApiClient&lt;/span&gt; &lt;span class="n"&gt;apiClient&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;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Update&lt;/span&gt; &lt;span class="n"&gt;update&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;update&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="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="s"&gt;"/start"&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;update&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="na"&gt;getText&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="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not my update&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;apiClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatId&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="k"&gt;return&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;// handled, stop chain&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No base classes. No decorators. Just: check condition → do work → return boolean.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Single Responsibility:&lt;/strong&gt; Each handler has one job. &lt;code&gt;StartCommandHandler&lt;/code&gt; only cares about &lt;code&gt;/start&lt;/code&gt;. &lt;code&gt;ButtonsCallbackHandler&lt;/code&gt; only cares about button clicks. They don't know about each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composable:&lt;/strong&gt; Dispatcher is itself an &lt;code&gt;UpdateHandler&lt;/code&gt;. You can nest dispatchers, filter updates through pre-handlers, build trees of logic - it's just function composition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring-friendly:&lt;/strong&gt; In Spring Boot, all handlers auto-wire:&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;UpdateDispatcher&lt;/span&gt; &lt;span class="nf"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpdateHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handlers&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;UpdateDispatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Spring injects all beans&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring collects every &lt;code&gt;@Component&lt;/code&gt; implementing &lt;code&gt;UpdateHandler&lt;/code&gt; and passes them in. No manual registration needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Philosophy: Obvious Defaults
&lt;/h2&gt;

&lt;p&gt;I wanted zero cognitive load to get started. Call &lt;code&gt;new TelegramApiClient(token)&lt;/code&gt; and it just works. Connection pooling? Configured. Retry logic? Enabled. Polling timeout edge cases? Handled automatically.&lt;/p&gt;

&lt;p&gt;You only configure when defaults don't fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need a proxy? Pass a custom &lt;code&gt;OkHttpClient&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Want different timeouts? Use &lt;code&gt;TelegramApiConfig.builder()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Otherwise? Just use the constructor, it works&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same with handlers - no decorators, no registration APIs, no config files. Implement the interface, return a boolean, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding features means adding classes, not complexity.&lt;/strong&gt; Want button handling? Write &lt;code&gt;ButtonsCallbackHandler&lt;/code&gt;. Want admin commands? Write &lt;code&gt;AdminCommandHandler&lt;/code&gt;. Each new feature is a new handler class - the core stays unchanged.&lt;/p&gt;

&lt;p&gt;This means the library has limited built-in functionality. But that's intentional. I'd rather give you 5 simple building blocks than one complicated configuration system that tries to handle everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Details
&lt;/h2&gt;

&lt;p&gt;A minimal library still needs production-grade internals:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection pooling:&lt;/strong&gt; OkHttp with configurable pool size, keep-alive, timeouts. Defaults tuned for Telegram API (single host, long-lived connections).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry logic:&lt;/strong&gt; Exponential backoff for transient errors (network issues, 5xx responses). But NOT for &lt;code&gt;getUpdates&lt;/code&gt; - long polling already has offset-based recovery built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polling timeout quirk:&lt;/strong&gt; This took me hours to debug. Telegram holds the long polling connection for up to 30 seconds if no updates arrive. If OkHttp's &lt;code&gt;readTimeout ≤ pollingTimeout&lt;/code&gt;, it kills the connection before Telegram responds, and the polling loop stops.&lt;/p&gt;

&lt;p&gt;The library handles this automatically - it dynamically adjusts &lt;code&gt;readTimeout&lt;/code&gt; per-request to always exceed the polling timeout by 35 seconds. Works regardless of what timeout value you configure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful shutdown:&lt;/strong&gt; &lt;code&gt;stopPolling()&lt;/code&gt; interrupts the polling thread immediately via &lt;code&gt;thread.interrupt()&lt;/code&gt;. No waiting for the 30-second timeout to expire.&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;TelegramBotPollingService&lt;/span&gt; &lt;span class="n"&gt;service&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;TelegramBotPollingService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;apiClient&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;// autoStart&lt;/span&gt;
&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Polling runs in background, shutdown hook stops it on exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Simplicity scales.&lt;/strong&gt; The library is ~500 lines of code. No command router, no conversation state, no wizard builders. Just: here's the API, here's the update, return true/false. Users build their own patterns on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OkHttp internals matter.&lt;/strong&gt; That &lt;code&gt;readTimeout &amp;gt; pollingTimeout&lt;/code&gt; bug cost me hours. The fix is simple once you know it, but debugging "why does my bot stop every 30 seconds" was painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring's auto-wiring is powerful.&lt;/strong&gt; Letting Spring inject &lt;code&gt;List&amp;lt;UpdateHandler&amp;gt;&lt;/code&gt; means users just write &lt;code&gt;@Component&lt;/code&gt; handlers and they auto-register. Zero configuration boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open source infrastructure is real work.&lt;/strong&gt; I thought "publish to GitHub" meant pushing code. Turns out you also need: proper Maven POM, package repository setup, README with examples, social preview images, release notes. The code was maybe 60% of the effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Open Source Library
&lt;/h2&gt;

&lt;p&gt;This is my first time publishing an open source project. I built it for myself, then spent time cleaning it up, writing documentation, and setting up proper packaging. Not gonna lie - there's some impostor syndrome around releasing code publicly, even though I use it in production and it works fine.&lt;/p&gt;

&lt;p&gt;But that's kind of the point of open source, right? Share what works for you, maybe it helps someone else. If it doesn't, no harm done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/nomad4tech/telegrambot4j" rel="noopener noreferrer"&gt;https://github.com/nomad4tech/telegrambot4j&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Example project:&lt;/strong&gt; &lt;a href="https://github.com/nomad4tech/telegrambot4j-demo" rel="noopener noreferrer"&gt;https://github.com/nomad4tech/telegrambot4j-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo shows handlers for commands, inline buttons, and callback handling. Copy-paste starting point if you want to try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to Use This
&lt;/h2&gt;

&lt;p&gt;If you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex conversation flows with state tracking → use a framework&lt;/li&gt;
&lt;li&gt;Built-in command parsing, permissions, middleware → heavier libraries have this&lt;/li&gt;
&lt;li&gt;Webhooks → currently only long polling supported&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This library is for people who want direct API access with minimal abstraction. If you're building a complex chatbot with branching conversations, you probably want more structure than this provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Not every project needs a framework. Sometimes the best abstraction is almost none at all - just enough structure to avoid repeating yourself, not so much that it dictates how you work.&lt;/p&gt;

&lt;p&gt;If you're building a Telegram bot in Java and existing libraries feel too heavy, give this a shot. And if you find bugs or want features, PRs welcome - learning as I go here.&lt;/p&gt;

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