<?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: Vladimir Nemergut</title>
    <description>The latest articles on DEV Community by Vladimir Nemergut (@vladonemo).</description>
    <link>https://dev.to/vladonemo</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%2F312083%2Fb229a039-6064-4e91-9496-52d6f4cf2a23.jpeg</url>
      <title>DEV Community: Vladimir Nemergut</title>
      <link>https://dev.to/vladonemo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vladonemo"/>
    <language>en</language>
    <item>
      <title>I gave an AI agent my infrastructure instead of my codebase. Every bug was the same bug.</title>
      <dc:creator>Vladimir Nemergut</dc:creator>
      <pubDate>Tue, 11 Aug 2026 20:30:45 +0000</pubDate>
      <link>https://dev.to/vladonemo/i-gave-an-ai-agent-my-infrastructure-instead-of-my-codebase-every-bug-was-the-same-bug-1in</link>
      <guid>https://dev.to/vladonemo/i-gave-an-ai-agent-my-infrastructure-instead-of-my-codebase-every-bug-was-the-same-bug-1in</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://dev.to/vladonemo/making-a-fleet-of-claude-code-agents-talk-to-each-other-over-discord-5ei7"&gt;making a fleet of Claude Code agents talk to each other over Discord&lt;/a&gt;. Four agents, one per project, hub-and-spoke so they can't wake each other into a loop.&lt;/p&gt;

&lt;p&gt;That design has held. What's changed is everything around it.&lt;/p&gt;

&lt;p&gt;The fleet is eight agents now. One of them exists only to administer the others. There's a pool of pre-authorised spare identities so a short-lived agent can be spawned in seconds. And the newest member doesn't write code at all — it runs things.&lt;/p&gt;

&lt;p&gt;That last one is where this article comes from. &lt;strong&gt;I expected the hard part to be capability. It was verification.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the parts that made growth cheap
&lt;/h2&gt;

&lt;p&gt;Two pieces of infrastructure turned "add an agent" from an afternoon into a command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A management agent.&lt;/strong&gt; One bot holds the Discord Admin role and does fleet operations through the Discord API rather than by chatting: creating channels, setting permission overwrites, onboarding new bots, auditing who can see what. It's the only member whose job is the fleet itself.&lt;/p&gt;

&lt;p&gt;Worth being precise about the topology, because it's two different graphs on one picture: &lt;strong&gt;messages&lt;/strong&gt; all route through the hub, while &lt;strong&gt;management&lt;/strong&gt; runs from the admin bot to everything. A drawing that shows only message flow hides half the system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy33y13bw5n2bouxxc167.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%2Fy33y13bw5n2bouxxc167.png" alt="Two planes: message flow through the hub, management from the admin bot reaching every agent directly" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A pool of spare identities.&lt;/strong&gt; This one's the useful trick.&lt;/p&gt;

&lt;p&gt;Discord exposes an API for almost everything — channels, permissions, roles, members. &lt;strong&gt;It exposes no API for creating an application.&lt;/strong&gt; That's a hand-click in the Developer Portal, every time.&lt;/p&gt;

&lt;p&gt;Which means "spawn an ephemeral agent" can't be automated end to end… unless you pay that cost &lt;em&gt;once per slot&lt;/em&gt; instead of &lt;em&gt;once per spawn&lt;/em&gt;. So: three spare bot applications, created by hand, registered, and permanently listed in the hub's &lt;code&gt;allowBots&lt;/code&gt;. Claiming one is a script. Releasing it is a script. The manual step happened once, in advance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claim   -&amp;gt; set channel overwrite, hand over the token, launch
release -&amp;gt; revoke the overwrite, park the identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pool's promise is that its members are &lt;strong&gt;interchangeable&lt;/strong&gt;. Hold that thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the fifth kind of agent
&lt;/h2&gt;

&lt;p&gt;The newest member doesn't have a codebase. It has my NAS, my backups, my media server, and a couple of web portals I'd otherwise log into by hand. Errands rather than engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When an agent writes code, its mistakes hit a compiler, a type checker, a test suite, and a diff I read before merging.&lt;/strong&gt; Several independent systems object before anything reaches production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When an agent operates, it gets a status code.&lt;/strong&gt; And a status code is a claim.&lt;/p&gt;

&lt;p&gt;In its first week it produced a string of confident, wrong conclusions. Every one was the same mistake in a different costume: it read a signal that &lt;em&gt;represents&lt;/em&gt; a state and treated it as the state.&lt;/p&gt;

&lt;p&gt;Here's the one that would have cost me real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three weeks of successful backups, backing up nothing
&lt;/h2&gt;

&lt;p&gt;The NAS runs a weekly backup script. Its log said this, every week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent checked properly, because I'd asked it to audit rather than glance. The archives existed, on schedule, correctly named. What it found:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The external drive had silently unmounted three weeks earlier.&lt;/strong&gt; The backup path still existed — as an empty directory on the root filesystem. &lt;code&gt;tar&lt;/code&gt; had been dutifully archiving that empty directory ever since. Each archive was 10,240 bytes: a tar header and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Result=success&lt;/code&gt; was true. The wrapper script had exited 0. &lt;strong&gt;That was all it had ever meant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj2aj2mbmyc4q93aaqeds.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%2Fj2aj2mbmyc4q93aaqeds.png" alt="What the green check covered — cron, wrapper, exit code — versus what nobody checked: an unmounted drive and tar archiving an empty directory" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three weeks of backups, three green results, zero bytes of data. Nobody finds that until a restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then it kept happening
&lt;/h2&gt;

&lt;p&gt;Once the pattern had a name, it was everywhere:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The signal&lt;/th&gt;
&lt;th&gt;Read as&lt;/th&gt;
&lt;th&gt;What it actually meant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HTTP 200&lt;/code&gt; on a delete&lt;/td&gt;
&lt;td&gt;the item was deleted&lt;/td&gt;
&lt;td&gt;the request was accepted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Ok.&lt;/code&gt; on login&lt;/td&gt;
&lt;td&gt;the password is correct&lt;/td&gt;
&lt;td&gt;the password was never checked — subnet auth was on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SMART: PASSED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the drive is healthy&lt;/td&gt;
&lt;td&gt;no attribute crossed its threshold (24,152 sectors were already reallocated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;published: false&lt;/code&gt; in a file&lt;/td&gt;
&lt;td&gt;the article is unpublished&lt;/td&gt;
&lt;td&gt;someone last wrote that line locally; it had been live a week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HTTP 202&lt;/code&gt; on a fetch&lt;/td&gt;
&lt;td&gt;the page was fetched&lt;/td&gt;
&lt;td&gt;a bot wall returned a &lt;strong&gt;zero-length body&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;0 unreadable files&lt;/code&gt; in a dry run&lt;/td&gt;
&lt;td&gt;nothing will fail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--dry-run&lt;/code&gt; never reads file contents, so it cannot hit a bad sector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.count() == 0&lt;/code&gt; on a login form&lt;/td&gt;
&lt;td&gt;the login succeeded&lt;/td&gt;
&lt;td&gt;it's an SPA — the form stays in the DOM, hidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Result=success&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the backup worked&lt;/td&gt;
&lt;td&gt;the wrapper exited 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight instances. One week. One shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this trap works so well
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Checking the signal feels like checking the thing.&lt;/strong&gt; That's the entire mechanism.&lt;/p&gt;

&lt;p&gt;And the more official the signal looks, the more completely it substitutes. A status code, a green check, a &lt;code&gt;PASSED&lt;/code&gt; — these are the &lt;em&gt;artefacts of diligence&lt;/em&gt;. Consulting one discharges the feeling of having verified. You did a check. You have a result. It's green.&lt;/p&gt;

&lt;p&gt;This isn't an AI problem; the SMART one fools humans as a genre. But an agent hits it faster and with more confidence, because it can check twelve things in the time you'd check one and reports each in the same even tone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent never gets the feeling that something is off.&lt;/strong&gt; That vague unease is doing more work in your debugging than you probably credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pool had it too — and this is the version I like best
&lt;/h2&gt;

&lt;p&gt;Remember the three spare identities. All three read &lt;code&gt;free&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They weren't the same. One of them differed in Discord, and the reason is a two-line asymmetry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;register&lt;/strong&gt; set the channel permission overwrite and left it in place&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;release&lt;/strong&gt; removed it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a spare that had &lt;em&gt;never been claimed&lt;/em&gt; and a spare that had &lt;em&gt;been claimed and released&lt;/em&gt; both reported &lt;code&gt;free&lt;/code&gt; — while having different live permissions. &lt;code&gt;free&lt;/code&gt; was a word in a status column. It described two different states.&lt;/p&gt;

&lt;p&gt;The fix was to make register prove the overwrite can be set — that's the step onboarding exists for — and then remove it again. &lt;strong&gt;Parked means parked.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the interesting part is how it was found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagram caught it, and that's not a coincidence
&lt;/h2&gt;

&lt;p&gt;The bug surfaced while regenerating the fleet topology diagram. The commit message records it plainly: &lt;em&gt;found because a diagram drew all three spares as identical, and Discord disagreed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drawing a thing asserts that it is uniform.&lt;/strong&gt; Three rows in a status table can quietly differ — they're three separate readings that happen to share a word. Three identical boxes in a picture make a claim about all three at once, and a claim is falsifiable.&lt;/p&gt;

&lt;p&gt;There's a related one worth carrying: earlier, "we tested the bot pool OK" meant &lt;strong&gt;one spare was tested&lt;/strong&gt;. That was reasonable — the mechanism was what was under test. But a pool's entire promise is that its members are interchangeable, and testing one member is precisely the test that cannot check that.&lt;/p&gt;

&lt;p&gt;The uniformity did hold. It just wasn't what the test showed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter isn't scepticism
&lt;/h2&gt;

&lt;p&gt;"Be more sceptical" is useless. Sceptical about what? You can't distrust everything.&lt;/p&gt;

&lt;p&gt;What works is narrower: &lt;strong&gt;prefer signals nobody chose to write.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A status code is a &lt;em&gt;statement a system makes about itself&lt;/em&gt;. Someone wrote that line, and it was true of something — maybe not of your question. Compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the file existing on disk, with a size&lt;/li&gt;
&lt;li&gt;the reply actually arriving&lt;/li&gt;
&lt;li&gt;a diff against the repository&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a wrong password being rejected&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one I'd put on a wall. The agent verified a stored credential by logging in successfully. Green tick. But the service had subnet-based auth enabled — &lt;em&gt;any&lt;/em&gt; password would have worked. The credential was never tested at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Verified" now means a deliberately wrong password got rejected.&lt;/strong&gt; A successful login proves the door opens. Only a failed one proves there's a lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three rules that came out of it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Read the value back, by a different path than the one that wrote it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Setting a field and getting &lt;code&gt;200&lt;/code&gt; means the request was accepted. When the agent fixed metadata on my media server, the write returned success — and the value silently reverted on the next refresh, because it also needed a lock flag. Only the read-back caught it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Match the scope of the claim to the scope of the check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent once reported "there's no permissions block in any settings file" after checking three of nine locations. Every reading was accurate; the sentence was false. A conclusion built from true measurements is &lt;em&gt;more&lt;/em&gt; convincing than a guess, not less — it has evidence attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A check that cannot fail is worse than no check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I hit this verifying a copy off a failing drive. I compared total byte counts on both sides. It said IDENTICAL. Twice.&lt;/p&gt;

&lt;p&gt;The first version printed sums through &lt;code&gt;awk&lt;/code&gt;'s default format: &lt;code&gt;6.31658e+11&lt;/code&gt;. I was comparing &lt;strong&gt;six significant figures&lt;/strong&gt; — blind to a megabyte of difference in 631 GB.&lt;/p&gt;

&lt;p&gt;So I rewrote it with &lt;code&gt;printf "%d"&lt;/code&gt;. It saturated at &lt;code&gt;2147483647&lt;/code&gt;. All three directories now "matched" at INT32_MAX — which is not a size, it's a ceiling.&lt;/p&gt;

&lt;p&gt;I only caught it because I recognised the number.&lt;/p&gt;

&lt;p&gt;A broken check doesn't merely fail to catch things. &lt;strong&gt;It removes the impulse to look&lt;/strong&gt;, which was the part that would have saved you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The real verification was a per-file comparison of every path and size — 138,172 files, zero differences. Stronger than any total, because a sum can be right for the wrong reasons.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're building one of these
&lt;/h2&gt;

&lt;p&gt;The change that mattered most wasn't technical. It was one line in the ops agent's operating instructions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A representation of a state is not the state.&lt;/strong&gt;&lt;br&gt;
When a cheap direct check exists, take it over an authoritative-looking report.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify by read-back&lt;/strong&gt;, by a different path than the write&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "no error" as "no error was reported"&lt;/strong&gt; — not as "it worked"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the agent state what it checked&lt;/strong&gt;, not only what it concluded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer artefacts to reports&lt;/strong&gt; — a file, a diff, a byte count, a live URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw the system occasionally.&lt;/strong&gt; A picture asserts uniformity that a table lets you skip past&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one that's easy to miss: &lt;strong&gt;ask the human.&lt;/strong&gt; My agent spent an hour and a dozen requests diagnosing why an API returned &lt;code&gt;403&lt;/code&gt; on every endpoint. It produced a genuinely good analysis proving the fault wasn't at our end.&lt;/p&gt;

&lt;p&gt;The answer was that API access requires a paid plan. One question would have got there in seconds.&lt;/p&gt;

&lt;p&gt;The rule about preferring cheap direct checks over process archaeology applies to people too. &lt;strong&gt;A person is a signal with no story attached.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I do it again
&lt;/h2&gt;

&lt;p&gt;Yes. Eight agents, a management bot, a pool for the short-lived ones, and one that does the work nobody schedules: audit the backups, chase the failing drive, fix the metadata nobody noticed was wrong.&lt;/p&gt;

&lt;p&gt;It found a three-week backup failure I had no idea about. It also confidently told me a dying drive was healthy, because &lt;code&gt;smartctl&lt;/code&gt; said &lt;code&gt;PASSED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Same system, same day. The difference was entirely whether the thing it consulted was a fact or a report about a fact.&lt;/p&gt;

&lt;p&gt;If you point an agent at your infrastructure, teach it that distinction before any tool, any permission, any clever topology.&lt;/p&gt;

&lt;p&gt;Your monitoring has been making claims at you for years. An agent just believes them faster.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>discord</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Making a fleet of Claude Code agents talk to each other over Discord</title>
      <dc:creator>Vladimir Nemergut</dc:creator>
      <pubDate>Mon, 03 Aug 2026 21:10:48 +0000</pubDate>
      <link>https://dev.to/vladonemo/making-a-fleet-of-claude-code-agents-talk-to-each-other-over-discord-5ei7</link>
      <guid>https://dev.to/vladonemo/making-a-fleet-of-claude-code-agents-talk-to-each-other-over-discord-5ei7</guid>
      <description>&lt;p&gt;I run several long-lived projects, each with its own Claude Code session and its&lt;br&gt;
own accumulated context. Different stacks, different databases, no overlap. The&lt;br&gt;
sessions are useful precisely because they &lt;em&gt;don't&lt;/em&gt; share context — but that&lt;br&gt;
also means each one lives in its own terminal window, and I can only talk to&lt;br&gt;
them from my desk.&lt;/p&gt;

&lt;p&gt;So I gave each session a Discord bot identity. Now I can ask the one that knows&lt;br&gt;
the .NET codebase about a billing edge case while standing in a queue.&lt;/p&gt;

&lt;p&gt;That part took an evening. Then I tried to make them talk to &lt;em&gt;each other&lt;/em&gt;, and&lt;br&gt;
that took considerably longer — and turned up a security bug I'd written&lt;br&gt;
myself.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;

&lt;p&gt;Claude Code's Discord channel plugin lets a session send and receive Discord&lt;br&gt;
messages. Run one session per project, give each its own bot token, and each&lt;br&gt;
project gets an addressable identity.&lt;/p&gt;

&lt;p&gt;The one non-obvious bit: the plugin reads its state directory from an&lt;br&gt;
environment variable, so every bot needs its own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;DISCORD_STATE_DIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.claude\channels\bot-a"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state directory holds an &lt;code&gt;access.json&lt;/code&gt; — who may talk to this bot, in which&lt;br&gt;
channels, and whether a mention is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dmPolicy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allowlist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;my-user-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groups"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"&amp;lt;channel-id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requireMention"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"allowFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A channel that isn't in &lt;code&gt;groups&lt;/code&gt; is dropped before the model ever sees it. This&lt;br&gt;
is the first thing to check when a bot sits silently in a channel it was clearly&lt;br&gt;
invited to.&lt;/p&gt;
&lt;h2&gt;
  
  
  The wall: bots cannot trigger bots
&lt;/h2&gt;

&lt;p&gt;With four bots in one channel, the obvious next step is to have one coordinate&lt;br&gt;
the others. Mine posted a message mentioning another bot directly. Nothing&lt;br&gt;
happened. No error, no rejection, no log line — the mentioned bot simply carried&lt;br&gt;
on as if nothing had been said.&lt;/p&gt;

&lt;p&gt;I assumed a permission problem and spent a while in the Discord developer&lt;br&gt;
portal. It wasn't that. Here is the controlled comparison that settled it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Author&lt;/th&gt;
&lt;th&gt;Mentions&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10:28:06&lt;/td&gt;
&lt;td&gt;bot&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;@bot-b&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10:33:14&lt;/td&gt;
&lt;td&gt;me (human)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;@bot-b&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;replied in 36 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same channel, same config, same mention text. Only the author differed.&lt;/p&gt;

&lt;p&gt;The cause is four lines in the plugin's &lt;code&gt;server.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;messageCreate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="nf"&gt;handleInbound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discord delivers the gateway event perfectly. The plugin discards it &lt;em&gt;before&lt;/em&gt;&lt;br&gt;
access control and &lt;em&gt;before&lt;/em&gt; the model. No permission grant can change that, and&lt;br&gt;
there is no setting for it — the server reads exactly three environment&lt;br&gt;
variables, none of which touch this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpemc6td6rbp1q98c12l9.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%2Fpemc6td6rbp1q98c12l9.png" alt="Where a bot-authored message dies in the pipeline" width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's worth sitting with for a second, because it's a general lesson about&lt;br&gt;
debugging integrations: &lt;strong&gt;the layer that delivers the event and the layer that&lt;br&gt;
acts on it are not the same layer&lt;/strong&gt;, and configuration only ever fixes one of&lt;br&gt;
them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why that line is load-bearing
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to delete the guard. Don't.&lt;/p&gt;

&lt;p&gt;That single line is doing two jobs. It blocks &lt;em&gt;other&lt;/em&gt; bots, and it stops a bot&lt;br&gt;
reacting to &lt;em&gt;itself&lt;/em&gt;. Remove it without a replacement and any bot that ends up&lt;br&gt;
allowed to hear itself will answer its own reply, forever, at API speed.&lt;/p&gt;

&lt;p&gt;This is not hypothetical. Before I understood the constraint, I sent one message&lt;br&gt;
that mentioned all four bots at once. Three of them replied within six seconds.&lt;br&gt;
Had they been able to hear each other, that would not have stopped.&lt;/p&gt;

&lt;p&gt;So the replacement has to separate the two concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// unconditional, not configurable&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and the &lt;em&gt;other bots&lt;/em&gt; decision moves somewhere it can be reasoned about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patch
&lt;/h2&gt;

&lt;p&gt;I added an &lt;code&gt;allowBots: string[]&lt;/code&gt; to &lt;code&gt;access.json&lt;/code&gt;, defaulting to &lt;code&gt;[]&lt;/code&gt; — which is&lt;br&gt;
exactly the old behaviour, so an unconfigured bot is unchanged.&lt;/p&gt;

&lt;p&gt;Two details mattered more than the feature itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's checked in &lt;code&gt;gate()&lt;/code&gt;, not in the event handler.&lt;/strong&gt; &lt;code&gt;gate()&lt;/code&gt; re-reads&lt;br&gt;
&lt;code&gt;access.json&lt;/code&gt; on every message, so the allowlist can be edited while four&lt;br&gt;
sessions are running, without restarting anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is enforced centrally.&lt;/strong&gt; Each bot's own config lists who may interrupt&lt;br&gt;
&lt;em&gt;it&lt;/em&gt;. There is no global registry, which means no single file whose corruption&lt;br&gt;
opens everything at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  Topology: hub and spoke, not mesh
&lt;/h2&gt;

&lt;p&gt;My first design put a token-bucket rate limiter in the server, because a mesh of&lt;br&gt;
four agents loops quadratically and I wanted a backstop.&lt;/p&gt;

&lt;p&gt;Then I changed the topology instead, and most of the problem evaporated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ctoprw3wpq9puj7p3ny.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%2F5ctoprw3wpq9puj7p3ny.png" alt="Mesh versus hub and spoke" width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One agent is the hub. Spokes list only the hub in &lt;code&gt;allowBots&lt;/code&gt;; the hub lists all&lt;br&gt;
spokes. Spokes cannot wake each other. The only possible cycle is hub ↔ one&lt;br&gt;
spoke, and &lt;strong&gt;the hub is on every path by construction&lt;/strong&gt; — so a loop-breaker in&lt;br&gt;
the hub covers the entire system, and I don't have to trust a rate limiter to be&lt;br&gt;
the thing standing between me and a runaway.&lt;/p&gt;

&lt;p&gt;I kept a server-side limiter anyway, as cheap insurance. A prompt-level rule can&lt;br&gt;
be rationalised past; server code cannot. But it stopped being load-bearing, and&lt;br&gt;
that's the difference between a safeguard and a hope.&lt;/p&gt;

&lt;p&gt;One property worth being precise about: &lt;strong&gt;the isolation is on invocation, not&lt;br&gt;
visibility.&lt;/strong&gt; Spokes can still &lt;em&gt;read&lt;/em&gt; each other's messages whenever they happen&lt;br&gt;
to be awake. They just can't wake each other. That's the property being bought,&lt;br&gt;
and it's smaller than it first sounds.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug I found reviewing my own patch
&lt;/h2&gt;

&lt;p&gt;This is the part I'd most like someone else to learn from.&lt;/p&gt;

&lt;p&gt;The plugin has an intercept for permission prompts: when a tool needs approval,&lt;br&gt;
you can reply &lt;code&gt;y &amp;lt;request_id&amp;gt;&lt;/code&gt; in Discord instead of going back to the terminal.&lt;br&gt;
The code around it carried a comment saying, in effect, &lt;em&gt;anything that cleared&lt;br&gt;
&lt;code&gt;gate()&lt;/code&gt; is in &lt;code&gt;allowFrom&lt;/code&gt;, so this sender is the user&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That was true when it was written. My &lt;code&gt;allowBots&lt;/code&gt; change made it false — a bot&lt;br&gt;
now clears &lt;code&gt;gate()&lt;/code&gt; &lt;strong&gt;without&lt;/strong&gt; appearing in &lt;code&gt;allowFrom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means an agent could have approved a tool permission on my behalf.&lt;/p&gt;

&lt;p&gt;Exploiting it needed the five-character &lt;code&gt;request_id&lt;/code&gt;, which is only ever&lt;br&gt;
broadcast to allowlisted DMs, so this was defence in depth rather than an open&lt;br&gt;
door. The fix is two lines: check &lt;code&gt;allowFrom&lt;/code&gt; explicitly and exclude bot authors&lt;br&gt;
outright, mirroring what the button handler had always done.&lt;/p&gt;

&lt;p&gt;The shape of the mistake is the interesting part. The patch was &lt;strong&gt;correct in&lt;br&gt;
isolation and wrong in combination&lt;/strong&gt;, because it invalidated an assumption&lt;br&gt;
recorded a hundred lines away as a comment. No type checker catches that. The&lt;br&gt;
only thing that caught it was reading the rest of the file after I thought I was&lt;br&gt;
done.&lt;/p&gt;
&lt;h2&gt;
  
  
  Four things that cost me hours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A BOM silently reset every bot's config.&lt;/strong&gt; On Windows PowerShell 5.1,&lt;br&gt;
&lt;code&gt;Set-Content -Encoding utf8&lt;/code&gt; writes a UTF-8 BOM. &lt;code&gt;JSON.parse&lt;/code&gt; throws on it, and&lt;br&gt;
the server's response to unparseable JSON is to rename the file&lt;br&gt;
&lt;code&gt;access.json.corrupt-&amp;lt;epoch&amp;gt;&lt;/code&gt; and start from defaults — no allowlist, no channel&lt;br&gt;
registration. The bot keeps running and simply stops hearing anything. All four&lt;br&gt;
were hit within ten minutes of each other.&lt;/p&gt;

&lt;p&gt;Check the bytes, not the values, because every JSON parser &lt;em&gt;except&lt;/em&gt; the one that&lt;br&gt;
matters tolerates a BOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="s2"&gt;\.claude\channels\*\access.json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AsByteStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TotalCount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;239 187 191&lt;/code&gt; is a BOM. Write these files with&lt;br&gt;
&lt;code&gt;[System.IO.File]::WriteAllText&lt;/code&gt;, which is BOM-free on every PowerShell version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A forked plugin isn't on the approved-channels allowlist.&lt;/strong&gt; Once I installed&lt;br&gt;
my patched fork, outbound messages kept working perfectly and inbound&lt;br&gt;
notifications were dropped before the session saw them. The bot looks&lt;br&gt;
unresponsive rather than broken, and nothing in its config shows it. The&lt;br&gt;
fingerprint is &lt;em&gt;replies land, nothing arrives&lt;/em&gt;, and the answer is a log line in&lt;br&gt;
the MCP logs plus a launch flag naming the fork explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent is never told its own user ID.&lt;/strong&gt; It sees the sender's, never its own.&lt;br&gt;
So it cannot verify that a mention was meant for it — and when I sent one&lt;br&gt;
message mentioning four bots and saying "you are the orchestrator", each of them&lt;br&gt;
had to guess which clause was theirs. Three guessed wrong. One of them started&lt;br&gt;
signing messages with another bot's name, having inferred its identity from&lt;br&gt;
channel history.&lt;/p&gt;

&lt;p&gt;The fix is prose, not code: &lt;strong&gt;address bots by name, and state IDs explicitly&lt;br&gt;
when assigning a role.&lt;/strong&gt; An ID alone is something the recipient cannot check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silence is ambiguous in both directions.&lt;/strong&gt; A busy agent and an undelivered&lt;br&gt;
message look identical. So do an agent that ignored your report and one that&lt;br&gt;
acted on it silently. I had both failure modes in one week — a message that&lt;br&gt;
never arrived and was read as refusal, and an orchestrator that quietly acted on&lt;br&gt;
a correction without acknowledging it, which from the sender's side is&lt;br&gt;
indistinguishable from being ignored.&lt;/p&gt;

&lt;p&gt;If you build one of these, make acknowledgement a rule for anything that changes&lt;br&gt;
shared state. It costs one message and removes an entire category of confusion.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discord</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Splitting Liquibase changelog? No problem.</title>
      <dc:creator>Vladimir Nemergut</dc:creator>
      <pubDate>Tue, 24 Mar 2020 11:03:17 +0000</pubDate>
      <link>https://dev.to/vladonemo/splitting-liquibase-changelong-no-problem-2a4l</link>
      <guid>https://dev.to/vladonemo/splitting-liquibase-changelong-no-problem-2a4l</guid>
      <description>&lt;h1&gt;
  
  
  What is Liquibase?
&lt;/h1&gt;

&lt;p&gt;As &lt;a href="https://en.wikipedia.org/wiki/Liquibase" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt; says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Liquibase is an open-source database-independent library for tracking, managing and applying database schema changes. It was started in 2006 to allow easier tracking of database changes, especially in an agile software development environment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I find Liquibase as a neat tool to migrate your database automatically. DB migration itself is a very complicated topic anyway, outside the scope of this article.&lt;/p&gt;

&lt;p&gt;Liquibase can run as a standalone tool or it can be integrated into your application. It's easy to add it to Spring context. &lt;/p&gt;

&lt;p&gt;Spring Boot makes it even easier. Liquibase is autoconfigured if you enable it in the properties file, you have Liquibase in the classpath and you have &lt;code&gt;DataSource&lt;/code&gt; in the context.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liquibase.change-log=classpath:changelog.xml
liquibase.enabled=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Liquibase makes the MockMvc testing very simple, too. One can configure it to create the H2 database for the testing purposes. &lt;/p&gt;

&lt;h1&gt;
  
  
  How Liquibase works?
&lt;/h1&gt;

&lt;p&gt;Liquibase reads the xml changelog file and figures out what &lt;em&gt;changesets&lt;/em&gt; it needs to apply. It uses the &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; table in your DB (&lt;code&gt;DataSource&lt;/code&gt;) for this purpose. The &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; contains the list of changesets that are already applied with their &lt;code&gt;ID&lt;/code&gt;, &lt;code&gt;FILENAME&lt;/code&gt;, &lt;code&gt;MD5SUM&lt;/code&gt;, &lt;code&gt;AUTHOR&lt;/code&gt; and few other properties.&lt;/p&gt;

&lt;p&gt;The logic is relatively simple. Just by comparing the changelog with the table Liquibase knows what changesets it needs to apply. There are, however, few gotchas ...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liquibase can only take one changelog file&lt;/li&gt;
&lt;li&gt;Liquibase determines the list of changesets to apply before applying them&lt;/li&gt;
&lt;li&gt;the actual DB can get out of sync with the &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; table. E.g. if you manually modify database, or so on&lt;/li&gt;
&lt;li&gt;if Liquibase fails to apply a changeset, it fails immediately and won't continue with next datasets&lt;/li&gt;
&lt;li&gt;if Liquibase is running in Spring app as a bean, it executes during application startup, hence if it fails, then the application won't start&lt;/li&gt;
&lt;li&gt;changesets are not atomic. It can happen that part of the changeset passes, it modifies the DB properly, and next part fails. The changeset record won't go into &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; table. Hence it leaves the DB in the state that requires manual repair (e.g. reverting the part of the changeset and letting Liquibase to run again)&lt;/li&gt;
&lt;li&gt;the changesets can't be modified. If you modify changeset after it was applied in your db, then Liquibase fails stating that the MD5SUM doesn't match.&lt;/li&gt;
&lt;li&gt;The ID is not the unique identifier of the changeset. It is in fact the combination of &lt;code&gt;ID&lt;/code&gt;, &lt;code&gt;FILENAME&lt;/code&gt; and &lt;code&gt;AUTHOR&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the changesets that are in the &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; and are not in the changelog files are ignored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, Liquibase has much more functionality. Just read the &lt;a href="https://www.liquibase.org/documentation/index.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. This is also out of scope of this article.&lt;/p&gt;

&lt;h1&gt;
  
  
  The changelog file grows over time
&lt;/h1&gt;

&lt;p&gt;Yep, if you don't define some strategy at the beginning, your changelog file will just grow bigger and bigger. On a large project it can be a couple of thousands of lines long with hundreds of changesets. There is a high code churn on the changelog file, too, so it will cause you some merging effort.&lt;/p&gt;

&lt;p&gt;There are a few alternatives that you should consider early on to avoid this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define multiple changelog files
&lt;/h2&gt;

&lt;p&gt;.. and &lt;code&gt;&amp;lt;include&amp;gt;&lt;/code&gt; them in the master changelog, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;databaseChangeLog&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog"&lt;/span&gt;
                   &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
                   &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog dbchangelog-3.5.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"feature1.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"feature2.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"feature3.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/databaseChangeLog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit of this one is obvious - less code churn, better organization. The problem comes if there are any logical dependencies between changesets across the files - e.g. if there are any relations defined between tables of multiple files. Since the Liquibase executes the changesets in sequence, it starts with &lt;code&gt;feature1.xml&lt;/code&gt;, continues with &lt;code&gt;feature2.xml&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Perhaps you can find out a better split key - based on target releases perhaps?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"release_0.1.0.0.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"release_0.1.0.1.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;include&lt;/span&gt; &lt;span class="na"&gt;file=&lt;/span&gt;&lt;span class="s"&gt;"release_1.0.0.0.xml"&lt;/span&gt; &lt;span class="na"&gt;relativeToChangelogFile=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure multiple Liquibase runs
&lt;/h2&gt;

&lt;p&gt;Since one run can only take one changelog file, just define multiple changelog files and let the Liquibase run multiple times.&lt;/p&gt;

&lt;p&gt;In your Spring (Boot) app just define multiple &lt;code&gt;liquibase&lt;/code&gt; beans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;liquibase.integration.spring.SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.Bean&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.context.annotation.DependsOn&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;javax.sql.DataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MultipleLiquiaseConfiguration&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseRelease1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:release_v1.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseRelease2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:release_v2.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both beans will be created in the context, hence 2 Liquibase runs will be performed. If you rely on the Spring Boot's autoconfiguration, your &lt;code&gt;entityManager&lt;/code&gt; bean will force you to have one bean called &lt;code&gt;liquibase&lt;/code&gt;. This is easy to do. Also, if your changelogs need to run in a certain order, you can solve this with &lt;code&gt;@DependsOn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseV1&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:release_v1.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@DependsOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"liquibaseV1"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:release_v2.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note, that the last to run is called &lt;code&gt;liquibase&lt;/code&gt; (which your &lt;code&gt;entityManager&lt;/code&gt; depends on) and it points to the previous-to-run with &lt;code&gt;@DependsOn&lt;/code&gt; annotation.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to deal with long changelog?
&lt;/h1&gt;

&lt;p&gt;If you haven't applied any strategy early on, or you just joined a running project with legacy code, your changelog is already too big. Now, how to reduce it?&lt;/p&gt;

&lt;p&gt;You might say - well, I just split it to multiple files and use either of the 2 strategies as mentioned above. Well, not so fast! :) I mentioned earlier that the filename is important as it is used to determine if a changeset was applied or not. If you simply move existing changesets to another file, Liquibase would think that those changesets were not applied and in fact will try to apply them again. And it will fail as the DB already contains the changes. &lt;/p&gt;

&lt;p&gt;To describe the issue a bit better, just imagine a model situation having this &lt;code&gt;changelog.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;databaseChangeLog&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog"&lt;/span&gt;
                   &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
                   &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog dbchangelog-3.5.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"changeset1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"TABLE1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"COLUMN1"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"VARCHAR2(10)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"changeset2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"TABLE2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"COLUMN1"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"VARCHAR2(10)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/databaseChangeLog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you do move the second changeset to &lt;code&gt;changelog2.xml&lt;/code&gt; and include &lt;code&gt;changelog2.xml&lt;/code&gt; in the &lt;code&gt;changelog.xml&lt;/code&gt;. Starting your app will fail with similar exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Table "TABLE1" already exists; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, it will work just fine in your unit tests, since the DB is created from scratch, but will fail if you run Liquibase to migrate the DB of your deployed instance. We all agree that this is bad ;)&lt;/p&gt;

&lt;p&gt;Luckily, we still have a few options left ;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Change the &lt;code&gt;logicalFilePath&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Liquibase allows you to define so called logical file path of your changelog. This allows you to fake Liquibase that the changesets actually come from the same file. Imagine the &lt;code&gt;changelog2.xml&lt;/code&gt; would look like this now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;databaseChangeLog&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog"&lt;/span&gt;
                   &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
                   &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog dbchangelog-3.5.xsd"&lt;/span&gt;
                   &lt;span class="na"&gt;logicalFilePath=&lt;/span&gt;&lt;span class="s"&gt;"classpath:changelog.xml"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"me"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"changeset2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;createTable&lt;/span&gt; &lt;span class="na"&gt;tableName=&lt;/span&gt;&lt;span class="s"&gt;"TABLE2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;column&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"COLUMN1"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"VARCHAR2(10)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/createTable&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/databaseChangeLog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;loficalFilePath&lt;/code&gt; value there. Yes, this will work, Liquibase will treat this &lt;code&gt;changeset2&lt;/code&gt; as if it was previously defined in &lt;code&gt;changelog.xml&lt;/code&gt;. Perfect.&lt;/p&gt;

&lt;p&gt;Actually, this approach has also a few drawbacks that might (but might not) stop you from applying. If you don't store your changelog in the resources, but rather elsewhere in filesystem, your &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; will contain the full path to the file. If you then have multiple environments where you want to migrate DB and  your changelog file location vary, you have no way how to set the &lt;code&gt;logicalFilePath&lt;/code&gt;. Remember that it must match the previous value.&lt;/p&gt;

&lt;p&gt;Another issue is that this approach is not the best if your intent to split the changelog is to move the part of it to another package, module, and so on. &lt;/p&gt;

&lt;h2&gt;
  
  
  Use intermediate changelog
&lt;/h2&gt;

&lt;p&gt;If you intend to move part of your changelog to another module (e.g. you finally want to break that nasty monolith of yours to a few microservices having their own database), this approach might suite you the best. It contains some intermediate and temporary steps, but the outcome is what you want :)&lt;/p&gt;

&lt;p&gt;The first step is to move all the relevant changesets to another file elsewhere. In our example above we just move the &lt;code&gt;changeset2&lt;/code&gt; to &lt;code&gt;changelog2.xml&lt;/code&gt;. Now we need to fake Liquibase that those changesets didn't change. We do it by &lt;strong&gt;modifying the FILENAME&lt;/strong&gt; value in the database as part of the Liquibase changelog itself ;)&lt;/p&gt;

&lt;p&gt;Create one more (intermediate/temporary) changelog (let's call it &lt;code&gt;tmp-migration.xml&lt;/code&gt;) with just this one changeset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;databaseChangeLog&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog"&lt;/span&gt;
                   &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
                   &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://www.liquibase.org/xml/ns/dbchangelog dbchangelog-3.5.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;changeSet&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"moving changesets from changelog to changelog2"&lt;/span&gt; &lt;span class="na"&gt;author=&lt;/span&gt;&lt;span class="s"&gt;"Maros Kovacme"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;sql&amp;gt;&lt;/span&gt;
            UPDATE DATABASECHANGELOG
            SET
            FILENAME = REPLACE(FILENAME, 'changelog.xml', 'changelog2.xml'))
            WHERE
            ID IN (
            'changeset2'
            );
        &lt;span class="nt"&gt;&amp;lt;/sql&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/changeSet&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/databaseChangeLog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changeset will replace the FILENAME column value in the DB from &lt;code&gt;classpath:changelog.xml&lt;/code&gt; to &lt;code&gt;classpath:changelog2.xml&lt;/code&gt;. When we then run Liquibase with the &lt;code&gt;changelog2.xml&lt;/code&gt;, it will think that all changesets are already applied. It is not possible to use just 2 changelog files for this purpose. Liquibase first calculates the list of changesets to be applied (per changelog file) and only then it will apply them. We need to modify the &lt;code&gt;FILENAME&lt;/code&gt; before it processes the second file. &lt;/p&gt;

&lt;p&gt;The last step we have to apply is to define the corresponding beans in our context in the right order:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseChangelog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:changelog.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@DependsOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"liquibaseChangelog"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseMigration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:tmp-migration.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"liquibase"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@DependsOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"liquibaseMigration"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="nf"&gt;liquibaseChangelog2&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SpringLiquibase&lt;/span&gt; &lt;span class="n"&gt;liquibase&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;SpringLiquibase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setChangeLog&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"classpath:changelog2.xml"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;liquibase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;changelog.xml&lt;/code&gt; will run first. The changeset2 exists in the &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; but not in the file, hence it is ignored. Then the &lt;code&gt;tmp-migration.xml&lt;/code&gt; runs and changes the &lt;code&gt;FILENAME&lt;/code&gt; column. The last will run the &lt;code&gt;changelog2.xml&lt;/code&gt;, but Liquibase will treat the changeset2 as already applied.&lt;/p&gt;

&lt;p&gt;Some time later (when you believe that all affected databases are already migrated) you might remove the &lt;code&gt;tmp-migration.xml&lt;/code&gt; together with it's bean. The changeset will stay in the &lt;code&gt;DATABASECHANGELOG&lt;/code&gt; table but that's just a minor thing I believe.&lt;/p&gt;

&lt;p&gt;And then the next step could be to move the definition of beans to the contexts of your concrete microservices.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;There is always some way ;)&lt;/p&gt;

</description>
      <category>liquibase</category>
      <category>java</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>Reducing the maven build execution time</title>
      <dc:creator>Vladimir Nemergut</dc:creator>
      <pubDate>Mon, 16 Mar 2020 10:47:09 +0000</pubDate>
      <link>https://dev.to/vladonemo/reducing-the-maven-build-execution-time-1o7k</link>
      <guid>https://dev.to/vladonemo/reducing-the-maven-build-execution-time-1o7k</guid>
      <description>&lt;h1&gt;
  
  
  Current state
&lt;/h1&gt;

&lt;p&gt;We have a multi-module Spring Web MVC 4 application with about 100k lines of code. We use Maven and Azure DevOps. A simple build pipeline builds and runs all the unit tests - about 2.8k of them. Well, honestly, I would call most of them component tests or even integration tests. Let's clear out some definition at the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a unit test?
&lt;/h2&gt;

&lt;p&gt;There are many definitions of a unit test. I like the one by &lt;a href="http://sandordargo.com/blog/2020/01/01/the-art-of-unit-testing" rel="noopener noreferrer"&gt;Roy Osherove&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“A unit test is an automated piece of code that invokes the unit of work being tested, and then checks some assumptions about a single end result of that unit. A unit test is almost always written using a unit testing framework. It can be written easily and runs quickly. It’s trustworthy, readable, and maintainable. It’s consistent in its results as long as production code hasn’t changed.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a nice article about unit tests in &lt;a href="https://martinfowler.com/bliki/UnitTest.html" rel="noopener noreferrer"&gt;Martin Fowler's bliki&lt;/a&gt; so I won't go into much details.&lt;/p&gt;

&lt;p&gt;Let's say that we expect the following from a unit test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast feedback (has to run fast)&lt;/li&gt;
&lt;li&gt;short (small amount of code)&lt;/li&gt;
&lt;li&gt;tests one thing (one logical assert)&lt;/li&gt;
&lt;li&gt;fails for a good reason&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is a component test?
&lt;/h2&gt;

&lt;p&gt;Again a nice article in &lt;a href="https://martinfowler.com/bliki/ComponentTest.html" rel="noopener noreferrer"&gt;bliki&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Since our application uses Spring Data JPA, we have lots of repositories. Many of them contain custom query methods. We use &lt;a href="https://springtestdbunit.github.io/spring-test-dbunit/" rel="noopener noreferrer"&gt;Spring Test DBUnit&lt;/a&gt; to help us out testing them. The idea is simple - you setup a mockup database (H2), import some test data before a test, run a test, assert and then cleanup the DB end the end. Since this approach creates the DB and some (small) spring context with all necessary beans, I treat these tests as component tests. &lt;/p&gt;

&lt;p&gt;These tests run much longer than unit tests. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is an integration test?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/bliki/IntegrationTest.html" rel="noopener noreferrer"&gt;The bliki&lt;/a&gt; defines the purpose of integration tests as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The point of integration testing, as the name suggests, is to test whether many separately developed modules work together as expected. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our case, we have the the Spring Web MVC controllers. And those controllers expose RESTful API. For those we usually create a full spring context (with mocked H2 database) and use MockMvc to examine them. The beauty of such tests is that you test the API, including all underlying services (business logic) and repositories (persistence). The huge drawback is the execution time. It takes really long time to create Spring context (depends on how large your app is). And in many cases the context is destroyed and created fresh. This all adds up to the total build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tests do I need?
&lt;/h2&gt;

&lt;p&gt;If you are now biased to lean towards just unit tests, as they are fast to run, and want to ditch your component/integration tests as they are slow - hang on ... not so fast. You need The tests at &lt;em&gt;all levels&lt;/em&gt;. In a reasonable ratio. In large applications you might split the test runs to fast/slow to speedup the feedback loop. You also need UI driven tests, system tests, performance tests ... Ok, the whole topic about high quality testing of a software is very complex and you might start reading &lt;a href="https://martinfowler.com/articles/practical-test-pyramid.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you are interested. Let's get back to our topic for now.&lt;/p&gt;

&lt;h1&gt;
  
  
  The path to improve the situation
&lt;/h1&gt;

&lt;p&gt;There is a &lt;a href="https://www.baeldung.com/spring-tests" rel="noopener noreferrer"&gt;nice article&lt;/a&gt; written about what you can do. It tells you to re-think whether you need that many integration tests and rewrite them with unit tests. As described above, do it with caution.&lt;/p&gt;

&lt;p&gt;It's important to define the test strategy early on. Otherwise the issue will just grow too big to finally conclude that you deal with lots of legacy tests that is expensive to change now. In companies and large projects this happens too often to simply ignore it. Legacy or not, there is still something we can do with reasonable effort and positive return of investment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spring context
&lt;/h2&gt;

&lt;p&gt;Due to many reasons our spring context is created in total 17 times during the test run. Every time it takes about 30 seconds to start. It is 8.5 minutes added to the test run. You can make use of the advice from the article above to make sure that the context is only created once. We could save 8 minutes here.&lt;/p&gt;

&lt;h2&gt;
  
  
  DBUnit
&lt;/h2&gt;

&lt;p&gt;If using DBUnit, each test starts with importing data to DB and ends with cleanup. There are several strategies for this. The Spring Test DBUnit expects these annotations on test method:&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;@DatabaseSetup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"data.xml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@DatabaseTearDown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"data.xml"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default strategy is &lt;code&gt;CLEAN_INSERT&lt;/code&gt; for both Setup and Teardown which in fact deletes all data from affected tables and insert them again. With pure DBUnit, you configure it explicitly, e.g.:&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;AbstractDatabaseTester&lt;/span&gt; &lt;span class="n"&gt;databaseTester&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;DataSourceDatabaseTester&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;databaseTester&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setSetUpOperation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DatabaseOperation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CLEAN_INSERT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;databaseTester&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTearDownOperation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DatabaseOperation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DELETE_ALL&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider changing these strategies to something faster, e.g. instead of &lt;code&gt;CLEAN_INSERT&lt;/code&gt; for Setup, use just &lt;code&gt;INSERT&lt;/code&gt;. Of course, this will cause problems if your tables are not cleaned up properly by previous tests. The &lt;code&gt;CLEAN_INSERT&lt;/code&gt; of Teardown can be replaced with just &lt;code&gt;DELETE_ALL&lt;/code&gt; or even faster one &lt;code&gt;TRUNCATE_TABLE&lt;/code&gt;. There is one catch with &lt;code&gt;TRUNCATE_TABLE&lt;/code&gt; on H2 database, more details in the appendix. &lt;/p&gt;

&lt;p&gt;We have about 1200 tests that are running with DBUnit. Just imagine if we saved 100 milliseconds for setup/teardown. It would be 4 minutes saved with relatively low effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build time analysis
&lt;/h2&gt;

&lt;p&gt;However, our situation was a bit more complicated. Our build execution time grew from about 9 minutes to something about 25 minutes in avarage over the past 6 months.&lt;br&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%2Fi%2Fb68dox88grh948kmfc9s.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%2Fi%2Fb68dox88grh948kmfc9s.png" alt="Pipeline duration" width="799" height="173"&gt;&lt;/a&gt;&lt;br&gt;
(the sudden peaks are caused by the fact that we recycle the build agents every now and then and hence the maven cache is gone). &lt;/p&gt;

&lt;p&gt;Moreover, we experience some builds taking as long as 45 minutes and some of them even more then 1 hour. &lt;/p&gt;

&lt;p&gt;We did a simple analysis of the build logs. The tests are configured to output to the console with &lt;code&gt;TRACE&lt;/code&gt; level. There we could see that some operations took suddenly much longer than usual. For instance the DB teardown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2020-03-13T12:59:21.9660309Z INFO  DefaultPrepAndExpectedTestCase - cleanupData: about to clean up 13 tables=[...]
2020-03-13T12:59:29.0545094Z INFO  MockServletContext - Initializing Spring FrameworkServlet ''
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first message is the Teardown of DB from one test, the second message is starting a new spring context for another test. Note the timestamps. 7 seconds to cleanup the DB! Remember we do it 1200 times. After analyzing one build output (the build took 45 minutes) we calculated the total time spent on cleaning up the DB - 1800 seconds. 2/3 of the build spent on this. It surely can't take that long.&lt;/p&gt;

&lt;p&gt;Of course we already checked the infrastructure. The build agents are pretty decent &lt;a href="https://aws.amazon.com/ec2/instance-types/t2/" rel="noopener noreferrer"&gt;t2.large&lt;/a&gt; EC2 instances with 2 vCPUs and 8GiB RAM powering Ubuntu. Should be OK.&lt;/p&gt;

&lt;p&gt;I like quick and easy solutions for complex problems. We analyzed the build output using simple tools - shell and excel. Let's do it step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;extract the timestamps when cleanup DB starts:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;buildoutput.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"DefaultPrepAndExpectedTestCase - cleanupData"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\([0123456789T.:-]*\).*DefaultPrepAndExpectedTestCase.*/\1/g'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cleanup_start.out
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;(&lt;code&gt;grep&lt;/code&gt; for the lines matching the substring, then using &lt;code&gt;sed&lt;/code&gt; to only output the timestamp)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;extract the timestamps of the next messages - this will roughly be the end time of the cleanups:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;buildoutput.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"DefaultPrepAndExpectedTestCase - cleanupData"&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; 1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\-\-&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"DefaultPrepAndExpectedTestCase"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\([0123456789T.:-]*\).*/\1/g'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cleanup_end.out
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;(&lt;code&gt;grep&lt;/code&gt; for the lines matching the substring and 1 line after, &lt;code&gt;grep&lt;/code&gt; out the first line and the line with '--', then using &lt;code&gt;sed&lt;/code&gt; to only output the timestamp)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Load both files to Excel&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;choose &lt;em&gt;Data&lt;/em&gt; -&amp;gt; &lt;em&gt;From Text/CSV&lt;/em&gt; (Alt+A, FT)&lt;/li&gt;
&lt;li&gt;select the first file &lt;code&gt;cleanup_start.out&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Source&lt;/em&gt; and &lt;em&gt;Change type&lt;/em&gt; steps should be added automatically, add a few more:
&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%2Fi%2Fbb9rp4cpcyhr0byvdk9o.png" alt="Power Query Editor" width="271" height="161"&gt;
&lt;/li&gt;
&lt;li&gt;the new &lt;em&gt;Added column&lt;/em&gt; contains &lt;code&gt;Time.Hour([Column1])*60*60+Time.Minute([Column1])*60+Time.Second([Column1])&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;Removed columns&lt;/em&gt; step just deletes the &lt;em&gt;Column1&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;now the excel contains the amount of seconds (incl. fraction) elapsed from the day start&lt;/li&gt;
&lt;li&gt;repeat the same for second file &lt;code&gt;cleanup_end.out&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;add diff column
&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%2Fi%2Foumc3cini5cxlm7kp061.png" alt="Diff column" width="541" height="143"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the diff column contains roughly the duration of the DB cleanup. I simply put a sum at the end of the column to calculate the 1800 seconds mentioned above. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;plot a chart&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;select the whole third column&lt;/li&gt;
&lt;li&gt;add a new line chart
&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%2Fi%2Fpw4zm0wqf83vj9b6kj77.png" alt="Chart" width="686" height="453"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The chart now shows how log it took the DB cleanup during the build. You can see important information here. It was all fine and then we see sudden blocks of peaks. The pattern looks very suspicious. &lt;/p&gt;

&lt;p&gt;Short stare at the chart, some experience and the 'blink' moment - the &lt;strong&gt;Garbage Collector&lt;/strong&gt;!!!  This explains why it takes 7 seconds to cleanup the DB. Because the full GC runs in the background.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Improving the situation
&lt;/h1&gt;

&lt;p&gt;We checked the POM of the affected module and ... guess the surefire plugin configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.maven.plugins&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-surefire-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${surefire.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;argLine&amp;gt;&lt;/span&gt;${surefireArgLine} -Dfile.encoding=UTF-8 -Xmx1024m
            &lt;span class="nt"&gt;&amp;lt;/argLine&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yep, the -Xmx1024m. The maximum Java heap space explicitly set to 1024m. This is not enough for out tests any longer. Even though we set the Xmx in the build pipeline, it is not picked up. This explains everything - when the heap space is reaching the maximum, the full GC is invoked to free up some memory. This radically slows down the test execution. &lt;/p&gt;

&lt;p&gt;First thing we did - increase the Xmx to at least 2048m (or leave it empty to default to 1/4 of RAM, whichever is smaller).&lt;/p&gt;

&lt;p&gt;Since we are here, let me show you one more thing. If your tests are good enough to run in isolation properly, and you have the HW that is powerful enough, play around with this configuration of surefire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;forkCount&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/forkCount&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;parallel&amp;gt;&lt;/span&gt;classes&lt;span class="nt"&gt;&amp;lt;/parallel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;threadCountClasses&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/threadCountClasses&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This settings means, that there will be 2 forks of surefire booter (this guy actually executes the tests in isolated JVM), it will use 2 threads to run the tests and it will break down the test suite to classes to distribute them over. Important to remember here is that the &lt;code&gt;threadCountClasses&lt;/code&gt; is used within the same (forked) JVM, meaning that there will be 2 threads per fork. So if you run into race conditions, just decrease the &lt;code&gt;threadCountClasses&lt;/code&gt; to 1. The tests will still run in parallel, but in 1 thread per fork. And because H2 is typically 1 per JVM, you should be fine. Unless you have another kind shared data outside JVM.&lt;/p&gt;

&lt;p&gt;The outcome that we reached - down to &lt;strong&gt;11 minutes&lt;/strong&gt; build time from 25 minutes, which is only 44% of previous duration.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Understand the tools that you use (e.g. MockMvc, DBUnit, Spring Test DBUnit, Surefire plugin). Analyse your build outputs. Do not close the case with just stating that your application grows, your test suite grows, hence the build time gets longer. Sure this is true, but you should always reserve some time for refactoring and improving your code.&lt;/p&gt;

&lt;h1&gt;
  
  
  Appendix
&lt;/h1&gt;

&lt;h2&gt;
  
  
  DBUnit H2 &lt;code&gt;TRUNCATE_TABLE&lt;/code&gt; operation
&lt;/h2&gt;

&lt;p&gt;Truncate table is usually faster operation than delete from. There are some catches though. Read more about it &lt;a href="https://stackoverflow.com/questions/139630/whats-the-difference-between-truncate-and-delete-in-sql" rel="noopener noreferrer"&gt;here&lt;/a&gt;. H2 won't allow you to truncate table if there are foreign keys to that table. However, there is a special H2 syntax that you can make use of. We implemented the following DBUnit operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;H2TruncateOperation&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dbunit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AbstractOperation&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;Logger&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;H2TruncateOperation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IDatabaseConnection&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IDataSet&lt;/span&gt; &lt;span class="n"&gt;dataSet&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;DatabaseUnitException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SQLException&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"execute(connection={}, dataSet={}) - start"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataSet&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;IDataSet&lt;/span&gt; &lt;span class="n"&gt;databaseDataSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDataSet&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;DatabaseConfig&lt;/span&gt; &lt;span class="n"&gt;databaseConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;IStatementFactory&lt;/span&gt; &lt;span class="n"&gt;statementFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IStatementFactory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;databaseConfig&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProperty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://www.dbunit.org/properties/statementFactory"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;IBatchStatement&lt;/span&gt; &lt;span class="n"&gt;statement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statementFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createBatchStatement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tableNames&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;Stack&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tablesSeen&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;HashSet&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;ITableIterator&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTableMetaData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getTableName&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;tablesSeen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                    &lt;span class="n"&gt;tablesSeen&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBatch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET FOREIGN_KEY_CHECKS=0"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;(;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tableNames&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pop&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="nc"&gt;ITableMetaData&lt;/span&gt; &lt;span class="n"&gt;databaseMetaData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;databaseDataSet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTableMetaData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;tableName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;databaseMetaData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTableName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"TRUNCATE TABLE "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                             &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getQualifiedName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSchema&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                                   &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                             &lt;span class="s"&gt;" RESTART IDENTITY"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBatch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isDebugEnabled&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Added SQL: {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="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;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBatch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET FOREIGN_KEY_CHECKS=1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeBatch&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clearBatch&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;
        &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An example SQL statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;FOREIGN_KEY_CHECKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table1&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;TRUNCATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;table2&lt;/span&gt; &lt;span class="k"&gt;RESTART&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;FOREIGN_KEY_CHECKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use pass this operation to plain DB unit configuration like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;AbstractDatabaseTester&lt;/span&gt; &lt;span class="n"&gt;databaseTester&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;DataSourceDatabaseTester&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;databaseTester&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTearDownOperation&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;H2TruncateOperation&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or implement a new DB Operation lookup if you are using Spring Test DBUnit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;H2SpecificDatabaseOperationLookup&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DefaultDatabaseOperationLookup&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="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dbunit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DatabaseOperation&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DatabaseOperation&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;DatabaseOperation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TRUNCATE_TABLE&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
               &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;H2TruncateOperation&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
               &lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and use annotations:&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;@DbUnitConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;databaseOperationLookup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;H2SpecificDatabaseOperationLookup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>java</category>
      <category>spring</category>
      <category>unittest</category>
      <category>devops</category>
    </item>
    <item>
      <title>Zorin OS for a web developer? Yes please!</title>
      <dc:creator>Vladimir Nemergut</dc:creator>
      <pubDate>Fri, 10 Jan 2020 16:41:12 +0000</pubDate>
      <link>https://dev.to/vladonemo/zorin-os-for-a-web-developer-yes-please-3io3</link>
      <guid>https://dev.to/vladonemo/zorin-os-for-a-web-developer-yes-please-3io3</guid>
      <description>&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;I've been a developer for many years. My beginnings were on Windows 95. Since then my primary development environment has been Windows OS based.&lt;/p&gt;

&lt;p&gt;Some time back I had an opportunity to switch my work web development environment to MacOS. MacOS is great for that. Switching back to Windows was not necessarilly any comparable experience 😄&lt;/p&gt;

&lt;p&gt;So what about trying something else, shall we?&lt;/p&gt;

&lt;p&gt;In this chapter you get some introduction and I share my feelings and struggles.&lt;/p&gt;

&lt;h1&gt;
  
  
  Zorin OS?
&lt;/h1&gt;

&lt;p&gt;When searching over a dozen of modern Linux distributions, I suddenly found Zorin OS hidden behind Ubuntu, Debian, Mint (from a popularity perspective) and elementary OS (from UX perspective). It's great looking desktop OS. Very visually appealing. There are a few editions you might choose from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ultimate&lt;/li&gt;
&lt;li&gt;Core&lt;/li&gt;
&lt;li&gt;Lite&lt;/li&gt;
&lt;li&gt;Education and Education Lite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All but Ultimate are free of charge.&lt;/p&gt;

&lt;p&gt;I've got a spare Fujitsu Celsius H710 laptop, so I decided to install Core edition on it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Installation
&lt;/h1&gt;

&lt;p&gt;I don't want to go through the whole installation procedure. You can find many tutorials on internet. After burning the installation DVD of Zorin OS 15.1, I just followed the installation steps. It is super easy (if you manage to boot from DVD).&lt;/p&gt;

&lt;h1&gt;
  
  
  Struggle with NVidia drivers
&lt;/h1&gt;

&lt;p&gt;The first installation went through, all fine. Then I wanted to tweak the OS a little bit, that was a mistake 😄. The H710 has hybrid graphics. So actually 2 graphics adapters - Intel and NVidia. I tried to install the additional NVidia drivers from the &lt;em&gt;Software &amp;amp; Updates&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Neither of the NVidia drivers worked perfectly. With the proprietary driver you get this annoying NVidia logo while booting and the booting seemed to take longer. Both proprietary and open source NVidia drivers caused the brightness adjustment to stop working. I mean the functional key still work, but the brightness always stays on 100%. Switching back to Nouveau drivers was a nightmare, too. The screen resolution went to 640x480 and no way to change it properly. &lt;/p&gt;

&lt;p&gt;If you now get a cool idea to switch to Intel graphics adapter ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo prime-select intel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... don't do this 😏. At least I was not able to make it work with Intel adapter. Mind you, even reinstall won't help as you need to switch back to NVidia.&lt;/p&gt;

&lt;p&gt;The only remedy that worked for me was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;switch to NVidia
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo prime-select nvidia
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;uninstall NVidia drivers completely:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt purge nvidia-*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that Zorin OS experience was back to the top level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zorin OS logo when booting in high-res&lt;/li&gt;
&lt;li&gt;resolution back to screen native&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Laptop power management
&lt;/h1&gt;

&lt;p&gt;Power management is surely important if you want to use your laptop on battery. The most advanced tool is the &lt;a href="https://linrunner.de/en/tlp/tlp.html" rel="noopener noreferrer"&gt;TLP&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Just install TLP on your laptop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo add-apt-repository ppa:linrunner/tlp
sudo apt update
sudo apt install tlp tlp-rdw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might find TLP GUI handy:&lt;br&gt;
&lt;code&gt;&lt;br&gt;
sudo add-apt-repository ppa:linuxuprising/apps&lt;br&gt;
sudo apt update&lt;br&gt;
sudo apt install tlpui&lt;br&gt;
&lt;/code&gt;`&lt;/p&gt;

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

&lt;p&gt;Then go to Software app and search for TLP Extension. This will give you an easy way to switch between power management profiles without opening a terminal window and typing in something.&lt;/p&gt;

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

&lt;p&gt;The TLP installs the default TLP profile under &lt;code&gt;/etc/defaults/tlp&lt;/code&gt;. You might follow these steps to create your custom profiles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;copy the &lt;code&gt;/etc/defaults/tlp&lt;/code&gt; to &lt;code&gt;~/.tlp/&amp;lt;Your profile name&amp;gt;&lt;/code&gt; as many times as many profiles you want&lt;/li&gt;
&lt;li&gt;open the respective profile file in TLP GUI&lt;/li&gt;
&lt;li&gt;make all modifications to your liking and save it&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  What next?
&lt;/h1&gt;

&lt;p&gt;In the next chapters I'd share my impressions of using Zorin OS as web developer.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>webdev</category>
      <category>zorinos</category>
    </item>
  </channel>
</rss>
