<?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: Austin</title>
    <description>The latest articles on DEV Community by Austin (@therobustroast).</description>
    <link>https://dev.to/therobustroast</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3769537%2Fe12ceb40-8773-4831-a07a-1f0a35c359e5.png</url>
      <title>DEV Community: Austin</title>
      <link>https://dev.to/therobustroast</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/therobustroast"/>
    <language>en</language>
    <item>
      <title>How I've Been Using Nix Flakes (And What You Can Do With Them)</title>
      <dc:creator>Austin</dc:creator>
      <pubDate>Thu, 19 Feb 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/therobustroast/how-ive-been-using-nix-flakes-and-what-you-can-do-with-them-3g8h</link>
      <guid>https://dev.to/therobustroast/how-ive-been-using-nix-flakes-and-what-you-can-do-with-them-3g8h</guid>
      <description>&lt;p&gt;I resisted Nix flakes for a while. The classic Nix channel-based workflow was familiar, and flakes felt like unnecessary complexity. Then I tried them on one project, and within a week I'd converted everything else. Here's what changed my mind and how you can get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What flakes actually solve
&lt;/h2&gt;

&lt;p&gt;Before flakes, my Nix setups had a recurring problem: reproducibility was aspirational rather than guaranteed. Running &lt;code&gt;nix-build&lt;/code&gt; on two different machines could produce different results depending on which channel revision each machine had pinned. Flakes fix this with a lockfile (&lt;code&gt;flake.lock&lt;/code&gt;) that pins every input to an exact revision. Same inputs, same outputs, every time.&lt;/p&gt;

&lt;p&gt;The other thing flakes fix is discoverability. A &lt;code&gt;flake.nix&lt;/code&gt; file has a defined structure. You can run &lt;code&gt;nix flake show&lt;/code&gt; on any flake and immediately see what it provides: packages, dev shells, NixOS modules, whatever. No more guessing what a random &lt;code&gt;default.nix&lt;/code&gt; is supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling flakes
&lt;/h2&gt;

&lt;p&gt;Flakes are still technically "experimental," but they've been stable in practice for years. Add this to your Nix configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/nix/nix.conf (or ~/.config/nix/nix.conf)&lt;/span&gt;
&lt;span class="nv"&gt;experimental-features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nix-command&lt;/span&gt; &lt;span class="nv"&gt;flakes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on NixOS, add it to your system configuration instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;nix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;experimental-features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"nix-command"&lt;/span&gt; &lt;span class="s2"&gt;"flakes"&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The anatomy of a flake
&lt;/h2&gt;

&lt;p&gt;Every &lt;code&gt;flake.nix&lt;/code&gt; has the same basic shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My project"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# stuff goes here&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;Inputs are your dependencies, pinned in &lt;code&gt;flake.lock&lt;/code&gt;. Outputs are what your flake provides to the world. That's it. Everything else is just filling in the outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 1: Dev shells that actually work
&lt;/h2&gt;

&lt;p&gt;This is where flakes won me over. I got tired of maintaining project-specific Docker containers just to get consistent tooling across machines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Web project"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt;
      &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;legacyPackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nv"&gt;nodejs_20&lt;/span&gt;
          &lt;span class="nv"&gt;nodePackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;typescript&lt;/span&gt;
          &lt;span class="nv"&gt;nodePackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;prettier&lt;/span&gt;
          &lt;span class="nv"&gt;postgresql_16&lt;/span&gt;
          &lt;span class="nv"&gt;redis&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="nv"&gt;shellHook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          echo "Dev environment loaded."&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;          export DATABASE_URL="postgresql://localhost:5432/myapp_dev"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;        ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;nix develop&lt;/code&gt; and you're in the shell. Every teammate gets the exact same versions of Node, PostgreSQL, and everything else, regardless of what's installed on their system.&lt;/p&gt;

&lt;p&gt;For projects that need to support multiple architectures, use &lt;code&gt;flake-utils&lt;/code&gt; to avoid repeating yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Cross-platform project"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;flake-utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:numtide/flake-utils"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;flake-utils&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
    &lt;span class="nv"&gt;flake-utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;eachDefaultSystem&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt;
        &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;legacyPackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;devShells&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;mkShell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nv"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kn"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nv"&gt;go_1_22&lt;/span&gt;
            &lt;span class="nv"&gt;gopls&lt;/span&gt;
            &lt;span class="nv"&gt;golangci-lint&lt;/span&gt;
            &lt;span class="nv"&gt;delve&lt;/span&gt;
          &lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it works on &lt;code&gt;x86_64-linux&lt;/code&gt;, &lt;code&gt;aarch64-linux&lt;/code&gt;, &lt;code&gt;x86_64-darwin&lt;/code&gt;, and &lt;code&gt;aarch64-darwin&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-loading with direnv
&lt;/h3&gt;

&lt;p&gt;Typing &lt;code&gt;nix develop&lt;/code&gt; every time you &lt;code&gt;cd&lt;/code&gt; into a project gets old. Pair flakes with &lt;code&gt;nix-direnv&lt;/code&gt; and the shell loads automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .envrc&lt;/span&gt;
use flake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole file. Walk into the directory, your tools appear. Walk out, they're gone. The shell is cached so it doesn't rebuild every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 2: Packaging your own software
&lt;/h2&gt;

&lt;p&gt;I maintain a few internal tools that need to be installable across machines. Here's what that looks like with flakes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Internal CLI tool"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt;
      &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;legacyPackages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kn"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;buildGoModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;pname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"mytool"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.3.1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;./.&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;vendorHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone can install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix profile &lt;span class="nb"&gt;install &lt;/span&gt;github:youruser/mytool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it without installing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix run github:youruser/mytool &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;nix run&lt;/code&gt; command is something I use constantly. Need a one-off tool without cluttering your system? Just &lt;code&gt;nix run&lt;/code&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 3: Running random software without installing it
&lt;/h2&gt;

&lt;p&gt;This is the &lt;code&gt;nix run&lt;/code&gt; trick I probably use more than anything else. Need to format some JSON but don't have &lt;code&gt;jq&lt;/code&gt; installed?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix run nixpkgs#jq &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'.users[0].name'&lt;/span&gt; data.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to spin up a quick HTTP server?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix run nixpkgs#python3 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to test something in a specific version of Node?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix run nixpkgs#nodejs_18 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;
nix run nixpkgs#nodejs_20 &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing gets installed to your profile. It's downloaded, cached, run, and that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 4: NixOS system configuration
&lt;/h2&gt;

&lt;p&gt;This is the big one if you run NixOS. I manage my entire system config as a flake. The structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nixos-config/
├── flake.nix
├── flake.lock
├── hosts/
│   ├── desktop/
│   │   ├── configuration.nix
│   │   └── hardware-configuration.nix
│   └── laptop/
│       ├── configuration.nix
│       └── hardware-configuration.nix
└── modules/
    ├── common.nix
    ├── dev-tools.nix
    └── desktop-environment.nix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;flake.nix&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My machines"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;home-manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nix-community/home-manager"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nv"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;follows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nixpkgs"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;home-manager&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;nixosConfigurations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;desktop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixosSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="sx"&gt;./hosts/desktop/configuration.nix&lt;/span&gt;
          &lt;span class="sx"&gt;./modules/common.nix&lt;/span&gt;
          &lt;span class="sx"&gt;./modules/dev-tools.nix&lt;/span&gt;
          &lt;span class="sx"&gt;./modules/desktop-environment.nix&lt;/span&gt;
          &lt;span class="nv"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixosModules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;home-manager&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;useGlobalPkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;useUserPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;home-manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;me&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="sx"&gt;./home.nix&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="nv"&gt;laptop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixosSystem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"x86_64-linux"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;modules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="sx"&gt;./hosts/laptop/configuration.nix&lt;/span&gt;
          &lt;span class="sx"&gt;./modules/common.nix&lt;/span&gt;
          &lt;span class="sx"&gt;./modules/dev-tools.nix&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rebuild with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nixos-rebuild switch &lt;span class="nt"&gt;--flake&lt;/span&gt; .#desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;inputs.nixpkgs.follows&lt;/code&gt; line on home-manager is worth calling out. It tells home-manager to use the same nixpkgs revision as the rest of your config instead of pulling its own. This avoids downloading two copies of nixpkgs and prevents version mismatches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 5: Composing flakes as inputs
&lt;/h2&gt;

&lt;p&gt;Flakes can depend on other flakes. This is how I pull in third-party NixOS modules, overlays, and tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;hyprland&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:hyprwm/Hyprland"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;nixvim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:nix-community/nixvim"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;agenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:ryantm/agenix"&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;Each input gets locked to a specific commit. Update one with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake update agenix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or update everything at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can review what changed before rebuilding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Thing 6: Templates
&lt;/h2&gt;

&lt;p&gt;Flakes can export project templates. I keep one with starter configs for languages I use often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Project templates"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;self&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;templates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;go&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;./templates/go&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Go project with dev shell"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nv"&gt;rust&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;./templates/rust&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Rust project with dev shell"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nv"&gt;python&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;./templates/python&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Python project with venv and dev shell"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a new project from a template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake init &lt;span class="nt"&gt;-t&lt;/span&gt; github:youruser/templates#go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directory gets populated with a working &lt;code&gt;flake.nix&lt;/code&gt; and whatever other boilerplate the template includes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thing 7: Checks and CI
&lt;/h2&gt;

&lt;p&gt;Flakes have a &lt;code&gt;checks&lt;/code&gt; output meant for CI. I use it to run tests, linting, and formatting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;checks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;system&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;runCommand&lt;/span&gt; &lt;span class="s2"&gt;"check-fmt"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixfmt-rfc-style&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    nixfmt --check &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="sx"&gt;./.&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    touch $out&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nv"&gt;lint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;runCommand&lt;/span&gt; &lt;span class="s2"&gt;"check-lint"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;buildInputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;pkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;statix&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    statix check &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="sx"&gt;./.&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;    touch $out&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;  ''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run all checks with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nix flake check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, that one command covers everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical tips I wish I'd known earlier
&lt;/h2&gt;

&lt;p&gt;Pin nixpkgs to a specific branch. &lt;code&gt;github:NixOS/nixpkgs/nixos-unstable&lt;/code&gt; is fine for dev machines. For production or CI, pin to a release branch like &lt;code&gt;nixos-24.11&lt;/code&gt; so you get security patches without surprise breakage.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;follows&lt;/code&gt; aggressively. If three of your inputs all pull in their own nixpkgs, you're evaluating three copies. I wasted an embarrassing amount of time on slow rebuilds before I figured this out. Make them all follow your top-level nixpkgs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nix"&gt;&lt;code&gt;&lt;span class="nv"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:NixOS/nixpkgs/nixos-unstable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:someone/foo"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;follows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nixpkgs"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nv"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github:someone/bar"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;nixpkgs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;follows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nixpkgs"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit your &lt;code&gt;flake.lock&lt;/code&gt;. The whole point of the lockfile is reproducibility. It belongs in version control. I've seen people &lt;code&gt;.gitignore&lt;/code&gt; it and then wonder why their CI builds are nondeterministic.&lt;/p&gt;

&lt;p&gt;Don't fight the eval cache. If &lt;code&gt;nix flake check&lt;/code&gt; or &lt;code&gt;nix build&lt;/code&gt; seems to skip evaluation, it's because the inputs haven't changed. Run &lt;code&gt;nix flake update&lt;/code&gt; if you actually want fresh inputs, or pass &lt;code&gt;--recreate-lock-file&lt;/code&gt; in rare cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I've landed
&lt;/h2&gt;

&lt;p&gt;Flakes are how I manage everything now. Dev environments, system configs, CI, personal tools. I still have complaints -- the Nix language itself remains confusing in spots, the error messages can be awful, and the documentation assumes you already know things you're reading the documentation to learn. But the lockfile means my builds actually reproduce, the standard structure means I can open any &lt;code&gt;flake.nix&lt;/code&gt; and orient myself quickly, and &lt;code&gt;nix run&lt;/code&gt; / &lt;code&gt;nix develop&lt;/code&gt; removed enough daily friction that I stopped noticing it.&lt;/p&gt;

&lt;p&gt;If you want to try it, start with a dev shell for one project. That's the lowest commitment and you'll know within a day whether it clicks for you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why your company's JSP app isn't dead - it just needs Spring Boot</title>
      <dc:creator>Austin</dc:creator>
      <pubDate>Thu, 12 Feb 2026 19:23:21 +0000</pubDate>
      <link>https://dev.to/therobustroast/why-your-companys-jsp-app-isnt-dead-it-just-needs-spring-boot-2fje</link>
      <guid>https://dev.to/therobustroast/why-your-companys-jsp-app-isnt-dead-it-just-needs-spring-boot-2fje</guid>
      <description>&lt;p&gt;Somewhere in your organization, there's a Java web application that everybody depends on and nobody wants to touch. It was written when Eclipse was the IDE, Tomcat 6 was cutting-edge, and &lt;code&gt;web.xml&lt;/code&gt; was the center of the universe. It works. Students (or customers, or employees) use it every day. But every time someone files a ticket asking for a new feature, the team exchanges a look that says: &lt;em&gt;who's going to open that codebase?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You don't need a rewrite. You don't need a microservices initiative or a "move to the cloud." You need Spring Boot, a week with the existing source code, and a strategy that lets you migrate one servlet at a time without taking the old system offline.&lt;/p&gt;

&lt;p&gt;This article is that strategy. It's drawn from a real applications at Bradley University: MyBradley, a pure-servlet JSP application with 43 servlets and 60+ JSPs that has served students for over a decade, with a update in progress; and Employee Meal Plan, a finished Spring Boot app that shows what the end state looks like when you get there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Understanding what you actually have
&lt;/h2&gt;

&lt;p&gt;Before you can migrate anything, you have to be honest about what "legacy" means in your specific case. Let's look at MyBradley. Not to shame it, but to understand the patterns that Spring Boot was designed to replace.&lt;/p&gt;

&lt;h3&gt;
  
  
  The anatomy of a servlet-era application
&lt;/h3&gt;

&lt;p&gt;MyBradley is a textbook early-2000s J2EE application. No Spring. No Struts. No framework at all. Just raw &lt;code&gt;HttpServlet&lt;/code&gt; classes dispatching to JSP views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyBradley (Old): Home.java -- a 200+ line servlet&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;Home&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HttpServlet&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;doGet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt; &lt;span class="n"&gt;response&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;ServletException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SessionManager&lt;/span&gt; &lt;span class="n"&gt;sessionManager&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;SessionManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnectionManager&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;OpenConnection&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="nc"&gt;UserLookupObject&lt;/span&gt; &lt;span class="n"&gt;ulo&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;UserLookupObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnectionManager&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNetworkId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;studnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ulo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getStudnum&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="nc"&gt;ProgramObject&lt;/span&gt; &lt;span class="n"&gt;po&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;ProgramObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnectionManager&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;studnum&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

            &lt;span class="nc"&gt;IdentityObject&lt;/span&gt; &lt;span class="n"&gt;ido&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;IdentityObject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnectionManager&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;studnum&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;firstname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ido&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getFirstName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"studnum"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;studnum&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"firstname"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;firstname&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUserName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="c1"&gt;// ... 20 more setAttribute calls ...&lt;/span&gt;

            &lt;span class="nc"&gt;RequestDispatcher&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestDispatcher&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Home.jsp"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HandleException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sessionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnectionManager&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;CloseConnection&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;Every servlet in the application follows this exact pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;SessionManager&lt;/code&gt; from the raw request/response&lt;/li&gt;
&lt;li&gt;Manually open a database connection&lt;/li&gt;
&lt;li&gt;Instantiate business objects, passing the connection around&lt;/li&gt;
&lt;li&gt;Set 10-30 request attributes as loose &lt;code&gt;String&lt;/code&gt; key-value pairs&lt;/li&gt;
&lt;li&gt;Dispatch to a JSP&lt;/li&gt;
&lt;li&gt;Manually close the connection in a &lt;code&gt;finally&lt;/code&gt; block (hopefully)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And behind those business objects? Raw JDBC with &lt;code&gt;CallableStatement&lt;/code&gt;, manually binding every parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyBradley (Old): StudentRecordDataAccess.java (class names changed)&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;StudentRecordDataAccess&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;INSERT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"{callStudentRecordInsert({an amount of ?s})}"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"{callStudentRecordSelectByID({an amount of ?s})}"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ConnectionManager&lt;/span&gt; &lt;span class="n"&gt;connectionManager&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CallableStatement&lt;/span&gt; &lt;span class="n"&gt;callableStatement&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;Insert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StudentRecordProperties&lt;/span&gt; &lt;span class="n"&gt;properties&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;ClassNotFoundException&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;callableStatement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectionManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Connection&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;prepareCall&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;INSERT&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;SetParameters&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&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;retval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callableStatement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeUpdate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;callableStatement&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;retval&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every domain concept in the system is implemented as this same three-class stack: a &lt;code&gt;Properties&lt;/code&gt; bean with getters and setters, a &lt;code&gt;DataAccess&lt;/code&gt; class wrapping raw JDBC, and an &lt;code&gt;Object&lt;/code&gt; class as the public facade. It's 234 Java files across 55 packages, all wired together by a 200-line &lt;code&gt;web.xml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cataloging the pain points
&lt;/h3&gt;

&lt;p&gt;Before migrating, make an inventory. In MyBradley:&lt;/p&gt;

&lt;p&gt;There is no connection pooling. Every request opens a new database connection and closes it when done. Under load, like during the first couple minutes of class registration, the database server is managing hundreds of short-lived connections instead of reusing a small pool. HikariCP alone would be a meaningful upgrade.&lt;/p&gt;

&lt;p&gt;Resource management is manual everywhere. If an exception is thrown between &lt;code&gt;OpenConnection()&lt;/code&gt; and &lt;code&gt;CloseConnection()&lt;/code&gt;, you leak a connection. The codebase is full of try/catch blocks that may or may not clean up properly. There are no try-with-resources statements.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SessionManager&lt;/code&gt; is a god object. It stores connection managers, user identity, flags, and business state in &lt;code&gt;HttpSession&lt;/code&gt;. Three different constructor overloads extract slightly different subsets of session attributes. It's the kind of class where you're afraid to add a field because you don't know what will break.&lt;/p&gt;

&lt;p&gt;There is no dependency injection. Every servlet manually creates its collaborators. Testing means deploying to Tomcat and clicking through a browser. The code wasn't designed to be unit-testable.&lt;/p&gt;

&lt;p&gt;Configuration is scattered across XML, encrypted XML, and hardcoded strings. The database credentials live in an XML file encrypted with 3DES using a key that's hardcoded in the Java source. CAS URLs are in &lt;code&gt;web.xml&lt;/code&gt;. Email addresses are hardcoded in servlet classes.&lt;/p&gt;

&lt;p&gt;Security runs through a legacy CAS client. The application uses Yale's original CAS filter library (pre-Apereo), configured through &lt;code&gt;web.xml&lt;/code&gt; init-params. Server names are commented-out and swapped manually for different environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- web.xml: spot the deployment process --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;param-value&amp;gt;&lt;/span&gt;localhost:8080&lt;span class="nt"&gt;&amp;lt;/param-value&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--&amp;lt;param-value&amp;gt;app-server-1.example.edu&amp;lt;/param-value&amp;gt;--&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--&amp;lt;param-value&amp;gt;app-server-2.example.edu&amp;lt;/param-value&amp;gt;--&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--&amp;lt;param-value&amp;gt;app-server-3.example.edu&amp;lt;/param-value&amp;gt;--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application works. It has worked for years. But every one of these pain points is a reason new features take longer than they should, bugs are harder to find, and onboarding a new developer takes weeks instead of days.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The migration strategy, one layer at a time
&lt;/h2&gt;

&lt;p&gt;The one rule of incremental migration: never be in a state where nothing works. You want to swap planks on the ship while it's still sailing. Here's the layer-by-layer strategy that MyBradley follows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0: Set up the Spring Boot shell
&lt;/h3&gt;

&lt;p&gt;Before touching any business logic, create the Spring Boot project that will become the new home. This is pure scaffolding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a Spring Boot project (start.spring.io or your IDE)&lt;/li&gt;
&lt;li&gt;Set packaging to WAR (you're still deploying to your existing Tomcat)&lt;/li&gt;
&lt;li&gt;Add your database JDBC driver&lt;/li&gt;
&lt;li&gt;Add Spring Security with your CAS dependencies&lt;/li&gt;
&lt;li&gt;Add Thymeleaf (your JSP replacement)&lt;/li&gt;
&lt;li&gt;Configure &lt;code&gt;application.properties&lt;/code&gt; with your database and CAS URLs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Package as a WAR, not a JAR. Your ops team deploys to Tomcat. Your load balancer points at Tomcat. Your CAS service URLs reference paths on Tomcat. Don't fight that battle yet. Spring Boot's &lt;code&gt;SpringBootServletInitializer&lt;/code&gt; lets you run as a WAR in an existing servlet container while still getting all of Spring Boot's auto-configuration:&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;ServletInitializer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SpringBootServletInitializer&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;protected&lt;/span&gt; &lt;span class="nc"&gt;SpringApplicationBuilder&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpringApplicationBuilder&lt;/span&gt; &lt;span class="n"&gt;application&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;application&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MybradleyApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end of Phase 0, you have a Spring Boot app that starts up, authenticates via CAS, and shows a blank home page. Nothing else. Deploy it to a test server and verify the CAS round-trip works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Migrate security first
&lt;/h3&gt;

&lt;p&gt;Security is the outermost layer. In MyBradley, it's a CAS filter in &lt;code&gt;web.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="nt"&gt;&amp;lt;filter&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;filter-name&amp;gt;&lt;/span&gt;CAS Filter&lt;span class="nt"&gt;&amp;lt;/filter-name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;filter-class&amp;gt;&lt;/span&gt;edu.yale.its.tp.cas.client.filter.CASFilter&lt;span class="nt"&gt;&amp;lt;/filter-class&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;init-param&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;param-name&amp;gt;&lt;/span&gt;edu.yale.its.tp.cas.client.filter.loginUrl&lt;span class="nt"&gt;&amp;lt;/param-name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;param-value&amp;gt;&lt;/span&gt;https://sso.example.edu/cas/login&lt;span class="nt"&gt;&amp;lt;/param-value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/init-param&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;init-param&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;param-name&amp;gt;&lt;/span&gt;edu.yale.its.tp.cas.client.filter.validateUrl&lt;span class="nt"&gt;&amp;lt;/param-name&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;param-value&amp;gt;&lt;/span&gt;https://sso.example.edu/cas/serviceValidate&lt;span class="nt"&gt;&amp;lt;/param-value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/init-param&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/filter&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;filter-mapping&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;filter-name&amp;gt;&lt;/span&gt;CAS Filter&lt;span class="nt"&gt;&amp;lt;/filter-name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;url-pattern&amp;gt;&lt;/span&gt;/*&lt;span class="nt"&gt;&amp;lt;/url-pattern&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/filter-mapping&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the new MyBradley, this becomes a Spring Security configuration with the modern Apereo CAS client:&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="nd"&gt;@EnableWebSecurity&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;SecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${cas.server-login-url}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;casServerLoginUrl&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${cas.server-url-prefix}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;casServerUrlPrefix&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;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;filterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                           &lt;span class="nc"&gt;CasAuthenticationFilter&lt;/span&gt; &lt;span class="n"&gt;casFilter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                           &lt;span class="nc"&gt;CasAuthenticationEntryPoint&lt;/span&gt; &lt;span class="n"&gt;entryPoint&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                           &lt;span class="nc"&gt;LogoutFilter&lt;/span&gt; &lt;span class="n"&gt;logoutFilter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorize&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/css/**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/js/**"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/assets/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/actuator/health"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestMatchers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin/**"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;hasAnyAuthority&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adminAuthorities&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionManagement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionFixation&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;newSession&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maximumSessions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;httpStrictTransportSecurity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hsts&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;hsts&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;includeSubDomains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxAgeInSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;31536000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exceptionHandling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticationEntryPoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entryPoint&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;casFilter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addFilterBefore&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;SingleSignOutFilter&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;CasAuthenticationFilter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="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;What you gain: environment-specific config via properties instead of commented-out XML. HSTS, session fixation protection, and frame options that the old app doesn't have at all. Proper single sign-out through Spring Security's filter chain. Role-based authorization (&lt;code&gt;/admin/**&lt;/code&gt; restricted to a whitelist) instead of ad-hoc checks in each servlet. And you can write integration tests against the &lt;code&gt;SecurityFilterChain&lt;/code&gt; without deploying to Tomcat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Migrate controllers, keep the data layer
&lt;/h3&gt;

&lt;p&gt;This is the insight that makes incremental migration practical: you don't have to migrate the data layer when you migrate the controllers. MyBradley proves it. Every controller in the new application is clean Spring MVC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyBradley (New): HomeController.java -- 18 lines total&lt;/span&gt;
&lt;span class="nd"&gt;@Controller&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@AuthenticationPrincipal&lt;/span&gt; &lt;span class="nc"&gt;UserDetails&lt;/span&gt; &lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userDetails&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUsername&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAttribute&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hasHolds"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"home"&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;Compare this to the 200+ line &lt;code&gt;Home&lt;/code&gt; servlet it replaces. The &lt;code&gt;SessionManager&lt;/code&gt; is gone. The manual connection handling is gone. The &lt;code&gt;RequestDispatcher&lt;/code&gt; is gone. Spring gives you &lt;code&gt;@AuthenticationPrincipal&lt;/code&gt; instead of digging through session attributes, &lt;code&gt;Model&lt;/code&gt; instead of &lt;code&gt;request.setAttribute()&lt;/code&gt;, and a view name return instead of &lt;code&gt;dispatcher.include()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But look at the repository layer underneath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MyBradley (New): PersonalInfoRepository.java -- still calling stored procedures&lt;/span&gt;
&lt;span class="nd"&gt;@Repository&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;PersonalInfoRepository&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;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PersonalInfoRepository&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jdbcTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PersonalInfoDTO&lt;/span&gt; &lt;span class="nf"&gt;getPersonalInfo&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;studentId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PersonalInfoDTO&lt;/span&gt; &lt;span class="n"&gt;dto&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;PersonalInfoDTO&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;populateBasicIdentity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// stored proc&lt;/span&gt;
        &lt;span class="n"&gt;populateAcademicRecord&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// stored proc&lt;/span&gt;
        &lt;span class="n"&gt;populateAddresses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// stored proc&lt;/span&gt;
        &lt;span class="n"&gt;populatePreferences&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;studentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// stored proc&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;populateBasicIdentity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PersonalInfoDTO&lt;/span&gt; &lt;span class="n"&gt;dto&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;studentId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SimpleJdbcCall&lt;/span&gt; &lt;span class="n"&gt;call&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;SimpleJdbcCall&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withProcedureName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"StudentSelectById"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;declareParameters&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;SqlParameter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"student_id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INTEGER&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="c1"&gt;// ... map results to DTO&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the same stored procedure that MyBradley calls through raw &lt;code&gt;CallableStatement&lt;/code&gt;. The database hasn't changed. The stored procedures haven't changed. But &lt;code&gt;JdbcTemplate&lt;/code&gt; handles resource cleanup, &lt;code&gt;SimpleJdbcCall&lt;/code&gt; handles parameter binding, and Spring's &lt;code&gt;@Repository&lt;/code&gt; annotation makes the whole thing injectable and testable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JdbcTemplate&lt;/code&gt; is the bridge. You can wrap every existing stored procedure call in a &lt;code&gt;JdbcTemplate&lt;/code&gt;-backed repository and immediately get connection pooling via HikariCP (configured once in &lt;code&gt;application.properties&lt;/code&gt;), automatic resource cleanup, constructor injection so you can test with mocks, and Spring's &lt;code&gt;DataAccessException&lt;/code&gt; hierarchy instead of raw &lt;code&gt;SQLException&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Replace JSPs with Thymeleaf templates
&lt;/h3&gt;

&lt;p&gt;JSP replacement is the most visible part of the migration, but it's also the most mechanical. For each JSP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a corresponding &lt;code&gt;.html&lt;/code&gt; file in &lt;code&gt;src/main/resources/templates/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;&amp;lt;%= request.getAttribute("foo") %&amp;gt;&lt;/code&gt; with &lt;code&gt;th:text="${foo}"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace JSTL &lt;code&gt;&amp;lt;c:forEach&amp;gt;&lt;/code&gt; with &lt;code&gt;th:each&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;&amp;lt;jsp:include&amp;gt;&lt;/code&gt; with Thymeleaf fragments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The real win is layout composition. MyBradley (Old) includes the header and sidebar by having each JSP call &lt;code&gt;&amp;lt;jsp:include page="Header.jsp" /&amp;gt;&lt;/code&gt;. MyBradley (New) uses Thymeleaf fragment layouts, where a single &lt;code&gt;layout.html&lt;/code&gt; defines the page shell and each view fills in content blocks. Change the sidebar once, it changes everywhere. No more copy-paste across 60 JSPs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Upgrade the data layer (when you're ready)
&lt;/h3&gt;

&lt;p&gt;This is where Employee Meal Plan shows the end state. Once your controllers and services are stable, you can migrate from &lt;code&gt;JdbcTemplate&lt;/code&gt; to Spring Data JPA for domains where it makes sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// EmployeeMealPlan: The JPA entity&lt;/span&gt;
&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meal_plans"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Data&lt;/span&gt;
&lt;span class="nd"&gt;@NoArgsConstructor&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;MealPlan&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IDENTITY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"base_price"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;basePrice&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tax_rate"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;taxRate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meals_count"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;mealsCount&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// EmployeeMealPlan: The repository -- the entire data access layer&lt;/span&gt;
&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;MealPlanRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MealPlan&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MealPlan&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByMealsCountGreaterThan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MealPlan&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByTitleContainingIgnoreCase&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;title&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 13-line interface replaces what would be a &lt;code&gt;Properties&lt;/code&gt; class, a &lt;code&gt;DataAccess&lt;/code&gt; class, a &lt;code&gt;Factory&lt;/code&gt; class, and an &lt;code&gt;Object&lt;/code&gt; class in MyBradley. Probably 200+ lines of manual JDBC boilerplate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: What you get for free
&lt;/h2&gt;

&lt;p&gt;The case for Spring Boot isn't any single feature. It's everything you stop doing by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection pooling
&lt;/h3&gt;

&lt;p&gt;MyBradley opens and closes a fresh database connection on every HTTP request. MyBradley adds two lines to &lt;code&gt;application.properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;spring.datasource.hikari.maximum-pool-size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;spring.datasource.hikari.minimum-idle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now 10 connections are shared across all requests. The database server went from managing hundreds of ephemeral connections to 10 stable ones. Response times drop. Connection timeouts disappear. Zero lines of Java.&lt;/p&gt;

&lt;h3&gt;
  
  
  Externalized configuration
&lt;/h3&gt;

&lt;p&gt;MyBradley encrypts database credentials with 3DES using a key that's hardcoded in the Java source and stores them in an XML file. Switching environments means manually editing XML and redeploying.&lt;/p&gt;

&lt;p&gt;MyBradley uses Spring profiles:&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="c"&gt;# application-dev.properties
&lt;/span&gt;&lt;span class="py"&gt;spring.datasource.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jdbc:yourdriver://dbhost.example.edu:5000/devdb&lt;/span&gt;

&lt;span class="c"&gt;# application-production.properties
&lt;/span&gt;&lt;span class="py"&gt;spring.datasource.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;jdbc:yourdriver://dbhost.example.edu:5000/proddb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch environments with &lt;code&gt;--spring.profiles.active=production&lt;/code&gt;. No code changes. No recompilation. No commented-out server names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;p&gt;Employee Meal Plan includes Spring Boot Actuator with a Prometheus metrics endpoint:&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;management.endpoints.web.exposure.include&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;health,info,metrics,prometheus&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you request latency histograms, JVM memory metrics, HikariCP pool utilization, and custom health checks, all exportable to Grafana or whatever your ops team uses. MyBradley has &lt;code&gt;e.printStackTrace()&lt;/code&gt; and silent catches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured logging
&lt;/h3&gt;

&lt;p&gt;Employee Meal Plan uses Logback with a Logstash encoder that outputs JSON with correlation IDs:&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;encoder&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"net.logstash.logback.encoder.LogstashEncoder"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;includeMdcKeyName&amp;gt;&lt;/span&gt;correlationId&lt;span class="nt"&gt;&amp;lt;/includeMdcKeyName&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/encoder&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every log line from a single request shares a correlation ID. When a user reports a problem, you search for one ID and see the entire request lifecycle. MyBradley calls &lt;code&gt;sessionManager.HandleException(e.getMessage())&lt;/code&gt;, which may or may not print something to stdout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security headers
&lt;/h3&gt;

&lt;p&gt;The old application sends none. The new one sends HSTS, X-Frame-Options, Referrer-Policy, and session fixation protection out of the box. These are features you were always supposed to have but couldn't justify hand-coding in every servlet's response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;

&lt;p&gt;Employee Meal Plan adds Bucket4j rate limiting on purchase endpoints:&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;bucket4j.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;bucket4j.filters[0]&lt;/span&gt;&lt;span class="py"&gt;.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;^/purchase/.*&lt;/span&gt;
&lt;span class="err"&gt;bucket4j.filters[0].rate-limits[0].bandwidths[0]&lt;/span&gt;&lt;span class="py"&gt;.capacity&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="err"&gt;bucket4j.filters[0].rate-limits[0].bandwidths[0]&lt;/span&gt;&lt;span class="py"&gt;.unit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try adding rate limiting to 43 individual servlets in MyBradley. Now stop trying.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The migration checklist
&lt;/h2&gt;

&lt;p&gt;Here's the concrete sequence that MyBradley follows:&lt;/p&gt;

&lt;h3&gt;
  
  
  What to migrate first
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Why first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Build system (Maven/Gradle)&lt;/td&gt;
&lt;td&gt;Everything else depends on reproducible builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Security (CAS/LDAP/OAuth)&lt;/td&gt;
&lt;td&gt;It's the outermost layer; get it right before touching pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Database connectivity&lt;/td&gt;
&lt;td&gt;HikariCP + &lt;code&gt;JdbcTemplate&lt;/code&gt; replaces manual connection management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Highest-traffic pages&lt;/td&gt;
&lt;td&gt;Migrate the home page first so users see the new UI immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;CRUD pages&lt;/td&gt;
&lt;td&gt;Forms, lists, detail views. These are mechanical.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Edge cases and admin tools&lt;/td&gt;
&lt;td&gt;Lowest traffic, least urgency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What to carry forward
&lt;/h3&gt;

&lt;p&gt;Not everything from the old system is bad. MyBradley explicitly preserves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored procedures. They encode business rules that have been refined for years. Wrap them in &lt;code&gt;SimpleJdbcCall&lt;/code&gt; and move on. You can migrate to JPA queries later, or not.&lt;/li&gt;
&lt;li&gt;The database schema. Don't combine a Spring Boot migration with a database redesign. That's two high-risk projects disguised as one.&lt;/li&gt;
&lt;li&gt;CAS authentication. The protocol is the same. You're upgrading the client library (Yale to Apereo) and the configuration mechanism (XML to Spring Security), not the identity infrastructure.&lt;/li&gt;
&lt;li&gt;WAR deployment. Your Tomcat servers work. Your deployment scripts work. Keep packaging as WAR. Embedded Tomcat is nice, but it's not worth a simultaneous ops migration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to leave behind
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old pattern&lt;/th&gt;
&lt;th&gt;New pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;web.xml&lt;/code&gt; servlet mappings&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Controller&lt;/code&gt; + &lt;code&gt;@GetMapping&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SessionManager&lt;/code&gt; god object&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@AuthenticationPrincipal&lt;/code&gt; + service injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ConnectionManager.OpenConnection()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HikariCP auto-configured by Spring Boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;request.setAttribute("key", value)&lt;/code&gt; x 30&lt;/td&gt;
&lt;td&gt;DTOs populated by service layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RequestDispatcher.include(JSP)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return a Thymeleaf view name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Properties&lt;/code&gt;/&lt;code&gt;DataAccess&lt;/code&gt;/&lt;code&gt;Object&lt;/code&gt; classes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Repository&lt;/code&gt; + &lt;code&gt;JdbcTemplate&lt;/code&gt; (or JPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3DES encrypted XML config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;application-{profile}.properties&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HandleException(e.getMessage())&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@ControllerAdvice&lt;/code&gt; + structured logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commented-out server names in XML&lt;/td&gt;
&lt;td&gt;Spring profiles&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 5: The honest tradeoffs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What's hard
&lt;/h3&gt;

&lt;p&gt;Stored procedure mapping is tedious. MyBradley's &lt;code&gt;PersonalInfoRepository&lt;/code&gt; calls 9+ stored procedures to build a single DTO. Each one requires a &lt;code&gt;SimpleJdbcCall&lt;/code&gt; with manually declared parameters and a custom &lt;code&gt;RowMapper&lt;/code&gt;. It's better than raw JDBC, but it's still boilerplate. Budget time for it.&lt;/p&gt;

&lt;p&gt;Thymeleaf and JSP think differently. JSPs are essentially Java code that emits HTML. Thymeleaf templates are HTML documents with special attributes. If your JSPs contain significant scriptlet logic (&lt;code&gt;&amp;lt;% ... %&amp;gt;&lt;/code&gt;), you'll need to move that logic into the controller or a service before the template migration makes sense.&lt;/p&gt;

&lt;p&gt;The old and new apps can't share sessions. If you're running both simultaneously during migration (old app handles pages you haven't migrated yet, new app handles migrated pages), users may need to authenticate separately to each. Plan your CAS service URLs accordingly.&lt;/p&gt;

&lt;p&gt;Database-specific quirks don't disappear. If your legacy database doesn't fully implement the JDBC spec, you'll still need workarounds. Spring Boot doesn't magically fix them, but it does give you a single place to put the workaround instead of scattering it across 43 servlets.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's worth it
&lt;/h3&gt;

&lt;p&gt;MyBradley is 234 Java files in 55 packages. MyBradley, which already handles the same core pages (home, schedule, grades, transcript, personal info, registration, holds, advisors), is 8 controllers, 7 services, 8 repositories, and 20 DTOs. Roughly 2,700 lines of Java. The functionality nearly is the same. The code is a fraction of the size.&lt;/p&gt;

&lt;p&gt;Employee Meal Plan goes further: JPA entities, dual data sources, payment gateway integration, field-level encryption, rate limiting, structured logging, Prometheus metrics, custom health checks. A new developer could understand the whole thing in a day. One developer built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Your servlet application isn't a failure. It's a success that outlived its architecture. Raw servlets, manual JDBC, session-based state management, XML configuration: these were the standard approach when it was written. They solved real problems. They just solve them in ways that create new problems as the application grows and the team changes.&lt;/p&gt;

&lt;p&gt;Spring Boot doesn't ask you to throw that investment away. Start with the build system. Then security. Then connection pooling. Then one controller. The application works the entire time. The codebase gets smaller with each phase. And you pick up things like observability and security hardening that would have been impractical to bolt onto the old architecture.&lt;/p&gt;

&lt;p&gt;MyBradley (Old) has 43 servlets and 234 Java files. MyBradley (New) has 8 controllers and clean separation of concerns. Employee Meal Plan has JPA, dual data sources, payment processing, and production observability in under 3,000 lines of Java. The servlet app isn't dead. It just needs Spring Boot, a plan, and one controller at a time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All code examples in this article are drawn from applications at Bradley University. The migration from MyBradley (Old) to MyBradley (New) is ongoing. Employee Meal Plan runs on Spring Boot 3.5.6.&lt;/em&gt;&lt;/p&gt;

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