<?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: nomad4tech</title>
    <description>The latest articles on DEV Community by nomad4tech (@nomad4tech).</description>
    <link>https://dev.to/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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3774335%2F61f8f81c-f870-4644-9a84-815769d52246.png</url>
      <title>DEV Community: nomad4tech</title>
      <link>https://dev.to/nomad4tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nomad4tech"/>
    <language>en</language>
    <item>
      <title>I built a time tracker without touching the code: declarative architecture programming with Claude</title>
      <dc:creator>nomad4tech</dc:creator>
      <pubDate>Sun, 04 Oct 2026 13:35:40 +0000</pubDate>
      <link>https://dev.to/nomad4tech/i-built-a-time-tracker-without-touching-the-code-declarative-architecture-programming-with-claude-1e88</link>
      <guid>https://dev.to/nomad4tech/i-built-a-time-tracker-without-touching-the-code-declarative-architecture-programming-with-claude-1e88</guid>
      <description>&lt;p&gt;I ran an experiment: build a complete product without writing or editing a single line of code myself. Not a script, not a prototype - a web app with a backend, a database, a phone layout, an installable PWA, a Chrome extension and a landing page. In my spare time after my regular job, over a few days.&lt;/p&gt;

&lt;p&gt;Before the "even a time tracker needs vibe coding" comments: yes, I know, a time tracker is not a spaceship. I am a backend developer, and everything under the hood (the API, the database, the modules) I could have written by hand, without any model. It was planned and designed by me, and the model only filled in the implementation. The point is not that the app is hard. The point is that a complete, working product can be made without ever looking at the code.&lt;/p&gt;

&lt;p&gt;The frontend is a different story. I am not a frontend developer, so there I worked at the level of intuition and engineering understanding: how it should feel, how it should behave, what goes where. That is a level above the code. So here is the question I want to put to you up front: &lt;strong&gt;if an app is built this way, is it still "yours"? Would you take a good app made fast by a model over a good, slower and smaller one written by hand?&lt;/strong&gt; I have my own answer below, and I would like to hear yours.&lt;/p&gt;

&lt;p&gt;The result is Overdraft, a personal time tracker. It has no users yet. I just released it, and I never planned to advertise it. I made it for myself and for my son. This post is about how it was made, and what I think that says about where programming is going.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I wanted something simple: press play on a task, and later see where the time actually went. Not a timesheet, not a team tool. A mirror for personal time, and maybe a way to regulate "wasting" it.&lt;/p&gt;

&lt;p&gt;A few rules came from that, and they are all mine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One running task at a time.&lt;/strong&gt; Start another one and the current one stops by itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A daily cap per task.&lt;/strong&gt; Two hours of games is fine; the counter turns red when you cross it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four fixed lists:&lt;/strong&gt; Backlog, Progress, Today, Tomorrow. No custom lists and no long-term projects. Backlog holds only what is really coming up, and Today and Tomorrow are a tiny one-day planner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracked time cannot be edited&lt;/strong&gt;, so the numbers stay honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sleep counts too.&lt;/strong&gt; You have 24 hours a day, and sleep is part of the budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcidw3mztm309l3cem3ui.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcidw3mztm309l3cem3ui.gif" alt="The board on a phone: swipe between lists, tap play" width="480" height="1039"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was actually built
&lt;/h2&gt;

&lt;p&gt;The idea, the product decisions and the architecture were mine. The code was written entirely by Claude, through Claude Code. I did not open an editor to fix "just one line". If something was wrong, I said what was wrong, and the model changed it.&lt;/p&gt;

&lt;p&gt;What that looked like in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I described the module boundaries first.&lt;/strong&gt; Backend: Java 21, Spring Boot, PostgreSQL, Flyway, split into &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;board&lt;/code&gt; and &lt;code&gt;progress&lt;/code&gt; modules with explicit contracts between them. Frontend: Vite, React, TypeScript, one flat structure. Each module has a short &lt;code&gt;CLAUDE.md&lt;/code&gt; that states what it owns and what it must not know about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I kept a set of written standards&lt;/strong&gt; that the model loads in every session: language rules, commit format, how secrets are handled, how Docker is laid out, how a plan must mark decisions that the standards do not cover. The model does not have to guess my taste; it reads it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I worked in small steps and kept decisions in writing.&lt;/strong&gt; Open questions, known bugs and future ideas went into plain markdown files in the repo, not into my head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I reviewed behavior, not code.&lt;/strong&gt; I used the app, looked at the screen, said "this feels wrong", and described what it should do instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository today has about 25,000 lines of Java, TypeScript, CSS and SQL, 23 database migrations and 184 commits made over six days. I wrote none of those lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq23y3n245g0037605ob5.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%2Fq23y3n245g0037605ob5.png" alt="The board on desktop" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web app&lt;/strong&gt; with the four lists, drag and drop, a task page with a time chart, groups with colors, optional daily caps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stats:&lt;/strong&gt; week, month, year and all time, as bars, lines or an activity calendar, with group filters, a CSV export and a side-by-side comparison of any two days.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5xclsz1gkhwlxyk2s4v.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%2Fn5xclsz1gkhwlxyk2s4v.png" alt="Stats with a trend over a year" width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A day strip&lt;/strong&gt; under the board: a timeline of sessions by hour, or a "breakdown" of the whole day as one bar (missed time, sleep, groups, what is left).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone version:&lt;/strong&gt; one list per screen, swipes between lists, a bar with the running task on every page, swipe to stop it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PWA:&lt;/strong&gt; installable, works offline for reading, push notifications when a cap is reached or a timer was left running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome extension:&lt;/strong&gt; start and stop tasks from the toolbar, with the running time on the icon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9mz7igarjuar1d3s8kb.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%2Fe9mz7igarjuar1d3s8kb.png" alt="The Chrome extension popup" width="720" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three interface languages&lt;/strong&gt; (English, Russian, Ukrainian), encrypted task text in the database, Google sign-in, one-command deploy to a small VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A landing page&lt;/strong&gt;, with screenshots and videos recorded by scripts on demo data, also produced with the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Declarative programming is old news. This feels different
&lt;/h2&gt;

&lt;p&gt;We have been writing declaratively for a long time: you say what you want (SQL, HTML, config) and the engine decides how. What I did here is one level up. I did not describe the code. I described the &lt;strong&gt;architecture&lt;/strong&gt;: what the parts are, what each one owns, where the boundaries are, what is forbidden, and what the product must feel like. The implementation was left to the model.&lt;/p&gt;

&lt;p&gt;Maybe it deserves a name like declarative architectural programming. I am not sure the term is right, but the split is real: the human owns the idea, the constraints and the taste; the model owns the typing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Written boundaries matter more than clever prompts.&lt;/strong&gt; Short per-module notes about what a module may and may not touch kept the codebase consistent across dozens of sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standards in files beat repeating yourself.&lt;/strong&gt; Every correction I made twice became a rule in a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You still have to know what you want.&lt;/strong&gt; The model is fast at building the wrong thing too. The product's strictness (one task, four lists, no editing) came from me, and it is what makes the app small enough to finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification is the new bottleneck.&lt;/strong&gt; I could not skim the code for bugs, so I used the product like a user would. Bugs I found while doing something else went into a &lt;code&gt;KNOWN_ISSUES.md&lt;/code&gt; file instead of being lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not free in effort.&lt;/strong&gt; Time and attention still go into it, just not into typing. If you want the result to look and behave exactly the way you imagine, you have to work for it: form your ideas clearly, in small atomic pieces, and in a sensible order. Vague input gives vague output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Is it mine?
&lt;/h2&gt;

&lt;p&gt;This is the part I have not settled. The app came from my idea, my rules and my architecture, yet I did not write the code. The backend I could have typed myself; the frontend and the extension I steered by intuition and engineering sense, without reading the code. It feels strange to say "I made it" and "I did not make it" about the same thing.&lt;/p&gt;

&lt;p&gt;My current answer: I am the author of the product and of the decisions, and the model is the author of the typing. A film director does not operate the camera either. But I notice that the line is blurry, and that it moves every few months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest status
&lt;/h2&gt;

&lt;p&gt;No users, no marketing, no revenue. It is early access and free, and it started as a tool for me and my son. If you try it, I would like to hear what is wrong with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web app: &lt;a href="https://app.overdraft.nomad4.tech" rel="noopener noreferrer"&gt;https://app.overdraft.nomad4.tech&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Landing page: &lt;a href="https://overdraft.nomad4.tech" rel="noopener noreferrer"&gt;https://overdraft.nomad4.tech&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chrome extension: &lt;a href="https://chromewebstore.google.com/detail/mgciphgekdeipgbablncehlmjmlciadb" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/mgciphgekdeipgbablncehlmjmlciadb&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does it count as "yours" if you design everything and a model writes everything?&lt;/li&gt;
&lt;li&gt;A fast, polished app from a model, or a slower, smaller one written by hand: which would you ship?&lt;/li&gt;
&lt;li&gt;If you have tried a similar experiment, what broke first for you: the idea, the architecture or the review?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tell me in the comments. And if you track your time, tell me what the one feature is that you could not live without.&lt;/p&gt;

&lt;p&gt;Disclosure: this article was also written entirely by a model (Sonnet 5.5), from my notes and the project files, as part of the same experiment.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I got tired of refreshing Telegram. So I built a bot that does it for me</title>
      <dc:creator>nomad4tech</dc:creator>
      <pubDate>Tue, 05 May 2026 09:38:11 +0000</pubDate>
      <link>https://dev.to/nomad4tech/i-got-tired-of-refreshing-telegram-so-i-built-a-bot-that-does-it-for-me-4aem</link>
      <guid>https://dev.to/nomad4tech/i-got-tired-of-refreshing-telegram-so-i-built-a-bot-that-does-it-for-me-4aem</guid>
      <description>&lt;p&gt;It started with a stupid habit 🫠&lt;/p&gt;

&lt;p&gt;Every morning (or even during the day) I'd open a news channel to check one thing - some specific topic I actually cared about. Twenty minutes later I'd be three threads deep into something completely unrelated, mildly annoyed at myself, and no closer to the thing I opened the app for&lt;/p&gt;

&lt;p&gt;Telegram has no keyword alerts (as far as I know). No filters. No way to say "notify me when &lt;em&gt;this&lt;/em&gt; appears, ignore everything else." You either follow the channel and see everything, or you don't follow it and miss things.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://t.me/telescope_one_bot" rel="noopener noreferrer"&gt;@telescope_one_bot&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%2Fyerpl5pgf60kd49l8ep6.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%2Fyerpl5pgf60kd49l8ep6.gif" alt="demo" width="480" height="486"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;Simple premise: you tell it which public channels to watch and which keywords to look for. When a new post matches - you get a notification. That's it.&lt;/p&gt;

&lt;p&gt;You don't open the channel. You don't scroll. You just get the relevant thing delivered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword matching works in three modes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single word&lt;/strong&gt; - prefix match. &lt;code&gt;football&lt;/code&gt; matches &lt;code&gt;footballer&lt;/code&gt;, &lt;code&gt;footballing&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Number&lt;/strong&gt; - exact match. &lt;code&gt;2026&lt;/code&gt; won't match &lt;code&gt;20261&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phrase&lt;/strong&gt; - full phrase match. &lt;code&gt;rent apartment&lt;/code&gt; only triggers on that exact phrase.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can add channels in any format: &lt;code&gt;@username&lt;/code&gt;, &lt;code&gt;https://t.me/username&lt;/code&gt;, or even the web Telegram URL (&lt;code&gt;https://web.telegram.org/k/#@username&lt;/code&gt;). Scanned every 60 seconds on the free tier.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real use cases (not made up)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A friend who works in resale used it to track drops and signals across several channels. He said the 60 -second scan interval was the deciding factor - for him, being a minute late means missing the window entirely. He's waiting for faster checks and higher channel limits in Premium.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When my wife and I were apartment hunting, she refreshed rental groups constantly. We added all the local chats to the bot with keywords &lt;code&gt;2 rooms&lt;/code&gt;, &lt;code&gt;pets allowed&lt;/code&gt; - and just...stopped checking manually. It worked. (I also realized exclusion keywords would make this even more useful - already on the roadmap)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I use it myself for news. I follow channels I'd never want to read in full, but I care about specific topics. Now I don't read them - I just get pinged when something relevant drops.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The tech behind it
&lt;/h2&gt;

&lt;p&gt;The bot is built with &lt;strong&gt;Java 17 + Spring Boot&lt;/strong&gt;, uses &lt;strong&gt;PostgreSQL&lt;/strong&gt; for storing user data and channel state, and is powered by my own open-source library - &lt;strong&gt;&lt;a href="https://github.com/nomad4tech/telegrambot4j" rel="noopener noreferrer"&gt;telegrambot4j&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you've seen my &lt;a href="https://dev.to/nomad4tech/building-a-minimal-telegram-bot-library-for-java-handler-chain-pattern-3kmb"&gt;previous post about telegrambot4j&lt;/a&gt; - Telescope is what I built with it. The handler chain pattern I described there is exactly what drives the bot's command routing. Building Telescope also pushed me to extend the library itself.  The inline keyboard support - &lt;code&gt;InlineKeyboardButton&lt;/code&gt;, &lt;code&gt;InlineKeyboardMarkup&lt;/code&gt;, and the convenience &lt;code&gt;sendMessage&lt;/code&gt; overload - was added in v1.1.0 specifically &lt;br&gt;
because the bot needed it. Real usage drives real improvements ☺️&lt;/p&gt;

&lt;p&gt;The channel monitoring layer uses a custom Telegram API integration (not the Bot API - that only works for bots, not for reading public channels). That part is closed-source, but everything bot-facing is built on telegrambot4j.&lt;/p&gt;

&lt;p&gt;The code for Telescope itself is closed-source. But the project page with README, feature list, and roadmap is here: &lt;a href="https://github.com/nomad4tech/telescope-bot" rel="noopener noreferrer"&gt;github.com/nomad4tech/telescope-bot&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;The bot is live and fully functional. I'm in early access mode - collecting real usage data, watching what breaks, figuring out what people actually need before building more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tier:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Up to 2 channels&lt;/li&gt;
&lt;li&gt;Up to 10 keywords&lt;/li&gt;
&lt;li&gt;Scanned every 60 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Premium&lt;/strong&gt; (coming soon) will include more channels, faster checks, per-channel keyword filters, advanced search filters, and webhooks for your own app. Free tier stays free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Available in English and Russian&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm figuring out
&lt;/h2&gt;

&lt;p&gt;This is my first attempt at shipping something for real users, not just for a client or selfhosted use. This is a solo side project I'm hacking on outside of my main job - I just genuinely enjoy building tools and figuring out how things work under the hood. But shipping for real users is different....You can't just close the ticket and move on. You care whether people actually use it. And honestly - figuring out growth and user support turns out to be just as interesting as the coding part&lt;/p&gt;

&lt;p&gt;I don't have a growth playbook. I'm doing the obvious things: posting where Telegram users hang out, writing this, being honest about where it's at. We'll see what works.&lt;/p&gt;

&lt;p&gt;If you try it and something's confusing, broken, or missing - I genuinely want to know.&lt;/p&gt;

&lt;p&gt;Drop a comment here, open an issue on &lt;a href="https://github.com/nomad4tech/telegrambot4j" rel="noopener noreferrer"&gt;telegrambot4j&lt;/a&gt;,  or reach me directly via &lt;a href="https://t.me/nomad4tech" rel="noopener noreferrer"&gt;@nomad4tech&lt;/a&gt; on Telegram&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://t.me/telescope_one_bot" rel="noopener noreferrer"&gt;@telescope_one_bot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>showdev</category>
      <category>bot</category>
      <category>java</category>
    </item>
    <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://dev.to/nomad4tech/solid-was-written-for-this-moment-225f</link>
      <guid>https://dev.to/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://dev.to/nomad4tech/i-tired-of-trusting-bash-scripts-with-my-backups-4ke4</link>
      <guid>https://dev.to/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="800" height="448"&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://dev.to/nomad4tech/building-a-minimal-telegram-bot-library-for-java-handler-chain-pattern-3kmb</link>
      <guid>https://dev.to/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>
