<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ software development - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ software development - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 11 Jul 2026 10:45:17 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/software-development/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ The Heroku Nostalgia Trap: Why Easy Deploys Aren't the Only Answer ]]>
                </title>
                <description>
                    <![CDATA[ There's a sentiment I've seen in Slack threads, Hacker News comments, and late-night Discord vents more times than I can count: "I just want something like Heroku." I've said it myself. And I was wr ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-heroku-nostalgia-trap-why-easy-deploys-arent-the-only-answer/</link>
                <guid isPermaLink="false">6a4c4a3acf22af9f521e9536</guid>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Heroku ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Iroro Chadere ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2026 00:37:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/6bc939c6-c2b8-40f7-9238-101ebd57d39c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>There's a sentiment I've seen in Slack threads, Hacker News comments, and late-night Discord vents more times than I can count:</p>
<blockquote>
<p><em>"I just want something like Heroku."</em></p>
</blockquote>
<p>I've said it myself. And I was wrong about what I meant.</p>
<p>Not wrong that Heroku was good. It was genuinely great, and the people who built it understood something most infrastructure companies still haven't internalized.</p>
<p>But when Salesforce killed the free tier in 2022 (and recently when it announced the <a href="https://www.heroku.com/blog/an-update-on-heroku/">end of sale for new enterprise customers</a>) and the migration scramble started, something interesting happened: almost everyone reached for the wrong lesson.</p>
<p>They thought the thing people loved about Heroku was easy deploys. So they built easier deploys.</p>
<p>But that wasn't it.</p>
<h2 id="heading-what-heroku-actually-did">What Heroku Actually Did</h2>
<p>When I was running a side project on Heroku sometime around 2019 or so, I didn't think about infrastructure at all. I pushed to Git, it deployed. My Postgres was there. If I needed a queue, I added a plugin. Everything lived in the same mental model, and that mental model took up approximately zero space in my head on a normal workday.</p>
<p>The thing Heroku sold wasn't a deployment pipeline. It was cognitive zero. The absence of infrastructure as a thing you had to think about.</p>
<p>That distinction matters more than it sounds. Because when you have to think about infrastructure, even a little, it bleeds into everything.</p>
<p>You make product decisions based on what's easy to deploy, not what's right. You delay shipping because you're not sure how the pieces connect. You spend a Friday afternoon debugging why your app can't reach its own database across two vendor networks instead of building the feature that would've landed three new customers.</p>
<p>Heroku removed all of that. One platform, everything pre-wired, one bill, one place to look when something breaks.</p>
<h2 id="heading-the-alternatives-got-one-thing-right-and-missed-the-rest-of-the-point">The Alternatives Got One Thing Right and Missed the Rest of the Point</h2>
<p>After Heroku's free tier died, Render, Railway, and Fly.io stepped up. All three are genuinely better than wrangling EC2 yourself. I've used all of them. I have no interest in being unfair to any of them.</p>
<p>But here's what actually happens when you use them.</p>
<p>You deploy your app to Render. Then you provision a Postgres database, also on Render, and paste the connection string into your environment variables.</p>
<p>Then you realize you need background jobs, so you sign up for Upstash or add a Redis instance somewhere. Then you want object storage, so you create an S3 bucket on AWS because Render doesn't have that.</p>
<p>Now you have three dashboards, three billing relationships, three sets of network rules to get right, and the mental overhead of three vendors stitched together with environment variables and prayer.</p>
<p>Railway is closer to the old vision, and it feels more integrated. But it runs on AWS underneath. You're paying Railway's margin on top of Amazon's margin, which means you're paying the hyperscaler tax twice: once to Amazon for the actual compute, and once to Railway for the privilege of not talking to Amazon directly.</p>
<p>Fly.io made the most interesting bet: they went bare metal. Real hardware, no AWS underneath, which structurally breaks the double-margin problem. But the integration story never quite closed. Your Postgres on Fly is still a thing you wire up separately. The "everything connected" feeling that made Heroku feel magical isn't there. You're still the one holding the wires.</p>
<h2 id="heading-the-thing-nobody-talks-about-egress-charges-between-your-own-services">The Thing Nobody Talks About: Egress Charges Between Your Own Services</h2>
<p>Here's something that took me longer to fully internalize than it should have.</p>
<p>When your app, your database, and your storage bucket live on different vendors, or even different services inside a cloud provider, data moving between them costs money. Not a lot, usually, until it is a lot.</p>
<p>An app that reads from a database 10,000 times a day, processes some results, and writes to S3 is moving data in three directions constantly. On a hyperscaler-backed platform, some of that movement crosses billing boundaries.</p>
<p>On a platform where compute, Postgres, storage, and queues all live on the same bare metal network? That movement is free, because it never leaves the building.</p>
<p>This isn't a hypothetical. It's a structural difference in how the platforms are architected, and it compounds over time in ways that don't show up clearly on any individual invoice.</p>
<h2 id="heading-opinionated-is-doing-real-work-here-not-marketing-work">"Opinionated" is Doing Real Work Here, Not Marketing Work</h2>
<p>I've worked with teams that spent actual engineering hours debating which queue system to use. Not implementing it. Debating it. SQS vs. BullMQ vs. RabbitMQ vs. something someone read about on a blog three years ago.</p>
<p>The argument for an opinionated platform isn't that you're incapable of making that decision. It's that the decision doesn't matter as much as you think, and the time you spend making it is time you're not spending on the thing that actually differentiates your product.</p>
<p>Postgres is the right database for almost every startup that exists. S3-compatible storage handles almost every file storage use case. A reliable queue is a reliable queue. These aren't interesting decisions. They stopped being interesting about a decade ago. The interesting decisions are in your product.</p>
<p>An opinionated platform forces you to stop relitigating settled questions. That's not a limitation. That's the point.</p>
<h2 id="heading-the-lock-in-question-answered-honestly">The Lock-in Question, Answered Honestly</h2>
<p>The most common pushback I hear when someone looks at a vertically integrated platform is: "What if I want to leave?"</p>
<p>It's a fair question and I used to ask it, too. Here's what I've realized: the lock-in concern is almost always theoretical, and it's usually raised by people who've never actually migrated off a platform.</p>
<p>Real lock-in requires something proprietary that your code depends on. A custom SDK that only works with that vendor. A query language that doesn't exist anywhere else. A deployment model that requires rewriting your app to leave.</p>
<p>If your app runs in a container, uses standard Postgres connection strings, speaks to S3 with an AWS SDK, and publishes jobs to a queue over a standard protocol, you're not locked in. You're just deployed somewhere. The migration path is a <code>pg_dump</code>, a bucket copy, and a new <code>docker push</code>. I've done migrations like that in a weekend.</p>
<p>The platforms that actually create lock-in are the ones that abstract everything into their own proprietary layer. Serverless functions with custom runtimes. Vendor-specific databases with proprietary query features. Edge compute that only exists on one network. Those are the things worth being suspicious of.</p>
<h2 id="heading-where-each-alternative-actually-breaks-down"><strong>Where Each Alternative Actually Breaks Down</strong></h2>
<p>Render is the easiest to recommend and the easiest to outgrow. Deploy a Next.js app, get a managed Postgres, done.</p>
<p>The problem shows up around month three when you need background jobs and object storage. Render doesn't have either natively.</p>
<p>So you reach for Upstash for queues and AWS S3 for storage. Now you have three dashboards, three billing relationships, and three networks that have to trust each other. The deployment step takes minutes. Everything around it takes the afternoon.</p>
<p>Railway feels more integrated than Render and the DX is genuinely good. But it runs on AWS. That's not a criticism of the team. It's a structural fact that has downstream consequences. As I mentioned before, you're paying Railway's margin on top of Amazon's margin, and data moving between your app and your database may cross billing boundaries depending on how Railway has provisioned things. The cost doesn't look alarming on any single invoice. But it compounds.</p>
<p>Fly.io made the most interesting architectural bet. Real hardware, no hyperscaler underneath, which structurally breaks the double-margin problem. \</p>
<p>I've deployed on Fly and the performance at the edge is real. But compute and Postgres are still separate things you wire together. Storage is still an external conversation. The "everything connected" feeling isn't there because the connections are still yours to make.</p>
<h2 id="heading-what-the-i-just-want-something-like-heroku-crowd-actually-needs">What the "I Just Want Something Like Heroku" Crowd Actually Needs</h2>
<p>I've thought about this a lot, and I think the nostalgia is real but misdirected.</p>
<p>People don't want Heroku specifically. They want the feeling that infrastructure is someone else's problem. Not because they can't handle it, but because handling it isn't why they got into building software. They want to push code and have things work. They want one place to look when things break. They want a bill they can understand.</p>
<p>The platforms that came after Heroku optimized for the wrong thing. They made the deployment step easier while leaving the integration work on you. They gave you a better on-ramp to the same fragmented landscape.</p>
<p>The more honest path is a platform that's made the architectural decisions Heroku never fully made: own the hardware, connect the services at the network level, charge one bill, and let the developer focus on code.</p>
<p>That's not nostalgia. That's just what the problem actually requires.\</p>
<h3 id="heading-another-alternative-to-heroku">Another Alternative to Heroku</h3>
<p>Here is what I've been watching: <a href="https://atlasflow.com">Atlasflow</a>. Bare metal, with compute, Postgres, S3-compatible storage, and queues on the same network before you ever touch them. And one bill.</p>
<p>I haven't run a production workload there yet, so I can't speak to reliability under pressure. That matters and I won't pretend it doesn't.</p>
<p>But the architectural argument is the most honest attempt I've seen at solving the actual problem rather than the symptom. Every other platform made deploy easier. Atlasflow is asking whether the integration should have been your problem in the first place.</p>
<p>That's not a small distinction. It's the whole thing.</p>
<p><em>I've been building production apps for close to a decade and belive me, you, the infrastructure landscape has gotten better in almost every measurable way. The integration problem is still mostly unsolved.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From Flutter to Backend: How to Build and Ship Production REST APIs with Dart and Shelf ]]>
                </title>
                <description>
                    <![CDATA[ As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications. The gap betwee ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-and-ship-production-rest-apis-with-dart-and-shelf/</link>
                <guid isPermaLink="false">6a1d92fa080b80f11f574194</guid>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend developments ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jun 2026 14:11:06 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/8ba5ec9d-22ba-4313-9b34-ce1e0e7dce23.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a Flutter engineer, you already know Dart. You understand async/await, you work with models and repositories, you think in clean architecture, and you have shipped real applications.</p>
<p>The gap between where you are and being able to build and deploy a production backend is smaller than you think.</p>
<p>The missing piece is not a new language. It's not a new paradigm. It's understanding how Dart behaves when there's no widget tree, no BuildContext, no Flutter framework – just a running process handling HTTP requests, talking to a database, and sending responses back to clients.</p>
<p>That's exactly what this article covers.</p>
<p>We're going to build a full User and Profile Management REST API from scratch using Dart and Shelf, connect it to a PostgreSQL database running in Docker, secure it with JWT authentication, and deploy it to Fly.io.</p>
<p>By the end, you'll have a working production-grade backend written entirely in Dart, the same language you already know.</p>
<p>This article is part of a series (of standalone articles) where we'll build the same project using three different frameworks. We'll use Shelf here, Serverpod in the next article, and Dart Frog in the one after that. This will let you directly compare how each framework approaches the same problem.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-dart-works-on-the-server">How Dart Works on the Server</a></p>
</li>
<li><p><a href="#heading-what-is-shelf">What is Shelf?</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
<ul>
<li><p><a href="#heading-creating-the-project">Creating the Project</a></p>
</li>
<li><p><a href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a href="#heading-database-setup-with-docker">Database Setup with Docker</a></p>
</li>
<li><p><a href="#heading-environment-configuration">Environment Configuration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-shelf-core-concepts">Shelf Core Concepts</a></p>
<ul>
<li><p><a href="#heading-handlers">Handlers</a></p>
</li>
<li><p><a href="#heading-request-and-response">Request and Response</a></p>
</li>
<li><p><a href="#heading-router">Router</a></p>
</li>
<li><p><a href="#heading-pipeline-and-middleware">Pipeline and Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-connecting-to-postgresql">Connecting to PostgreSQL</a></p>
<ul>
<li><p><a href="#heading-the-database-connection-manager">The Database Connection Manager</a></p>
</li>
<li><p><a href="#heading-running-migrations">Running Migrations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-building-the-api">Building the API</a></p>
<ul>
<li><p><a href="#heading-the-user-model">The User Model</a></p>
</li>
<li><p><a href="#heading-the-user-repository">The User Repository</a></p>
</li>
<li><p><a href="#heading-user-handlers">User Handlers</a></p>
</li>
<li><p><a href="#heading-the-profile-model">The Profile Model</a></p>
</li>
<li><p><a href="#heading-the-profile-repository">The Profile Repository</a></p>
</li>
<li><p><a href="#heading-profile-handlers">Profile Handlers</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-authentication">Authentication</a></p>
<ul>
<li><p><a href="#heading-password-hashing">Password Hashing</a></p>
</li>
<li><p><a href="#heading-jwt-token-generation-and-validation">JWT Token Generation and Validation</a></p>
</li>
<li><p><a href="#heading-auth-handlers">Auth Handlers</a></p>
</li>
<li><p><a href="#heading-auth-middleware">Auth Middleware</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-error-handling">Error Handling</a></p>
</li>
<li><p><a href="#heading-wiring-everything-together">Wiring Everything Together</a></p>
</li>
<li><p><a href="#heading-deployment">Deployment</a></p>
<ul>
<li><p><a href="#heading-dockerfile">Dockerfile</a></p>
</li>
<li><p><a href="#heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</a></p>
</li>
<li><p><a href="#heading-deploying-to-flyio">Deploying to Fly.io</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>Comfortable familiarity with Dart and Flutter development</p>
</li>
<li><p>Understanding of REST API concepts, endpoints, HTTP methods, status codes</p>
</li>
<li><p>Docker Desktop installed and running</p>
</li>
<li><p>A Fly.io account (free tier is sufficient, fly.io)</p>
</li>
<li><p>The Fly CLI installed (brew install flyctl on macOS, or the official installer on Windows/Linux)</p>
</li>
<li><p>A PostgreSQL client for inspecting the database, like TablePlus or DBeaver – both work well</p>
</li>
</ul>
<h2 id="heading-how-dart-works-on-the-server">How Dart Works on the Server</h2>
<p>When you run a Flutter app, the Flutter framework is doing an enormous amount of work, managing the widget tree, handling the render pipeline, coordinating state, and responding to platform events. Your Dart code sits on top of all of that.</p>
<p>On the server, none of that exists. There's no widget tree. There's no framework managing a UI lifecycle. There's just a Dart process running, listening on a port, receiving HTTP requests, doing work, and sending responses.</p>
<p>Dart's standard library, dart:io, has everything needed to do this at the lowest level:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  final server = await HttpServer.bind('0.0.0.0', 8080);
  print('Server running on port 8080');

  await for (final request in server) {
    request.response
      ..statusCode = 200
      ..write('Hello from Dart')
      ..close();
  }
}
</code></pre>
<p>This is a working HTTP server in raw Dart. No packages, no framework. Every request comes in through the HttpServer stream, and you write directly to the response.</p>
<p>This works, but it scales poorly. As soon as you need routing, middleware, authentication, and structured error handling, raw dart:io becomes difficult to manage. That is the problem Shelf solves.</p>
<h2 id="heading-what-is-shelf">What is Shelf?</h2>
<p>Shelf is a composable web server middleware library for Dart, maintained by the Dart team. It doesn't try to be a full framework – instead, it gives you the primitives to build one, or to assemble exactly what you need.</p>
<p>The Shelf mental model is built on four concepts:</p>
<ul>
<li><p><strong>Handler:</strong> a function that takes a Request and returns a Response. Everything in Shelf is ultimately a handler.</p>
</li>
<li><p><strong>Middleware:</strong> a function that wraps a handler, adding behaviour before or after it runs. Logging, authentication, and error handling are all middleware.</p>
</li>
<li><p><strong>Pipeline:</strong> a chain of middleware with a handler at the end. Requests flow through the middleware chain before reaching the handler.</p>
</li>
<li><p><strong>Router:</strong> maps URL patterns and HTTP methods to specific handlers.</p>
</li>
</ul>
<p>If you've used Flutter's Navigator or provider middleware concepts, the composition model will feel familiar. Small, single-responsibility pieces assembled into a working whole.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<h3 id="heading-creating-the-project">Creating the Project</h3>
<p>Dart includes a server-side project template that gives us a clean starting point:</p>
<pre><code class="language-bash">dart create -t server-shelf user_profile_api
cd user_profile_api
</code></pre>
<p>Add the dependencies we need to pubspec.yaml:</p>
<pre><code class="language-yaml">name: user_profile_api
description: User and Profile Management REST API built with Dart and Shelf
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

dependencies:
  shelf: ^1.4.1
  shelf_router: ^1.1.4
  postgres: ^3.3.0
  dart_jsonwebtoken: ^2.12.0
  bcrypt: ^1.1.3
  dotenv: ^4.1.0
  crypto: ^3.0.3

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">dart pub get
</code></pre>
<h3 id="heading-project-structure">Project Structure</h3>
<p>Now we'll build a backend project structure that Flutter engineers will find intuitive, that's familiar enough to navigate immediately, and that's correct enough for backend conventions:</p>
<pre><code class="language-plaintext">user_profile_api/
  bin/
    server.dart              ← entry point
  lib/
    config/
      database.dart          ← connection manager
      env.dart               ← environment config
    handlers/
      auth_handler.dart      ← auth endpoints
      user_handler.dart      ← user endpoints
      profile_handler.dart   ← profile endpoints
    middleware/
      auth_middleware.dart   ← JWT validation
      error_middleware.dart  ← global error handling
      logger_middleware.dart ← request logging
    models/
      user.dart
      profile.dart
    repositories/
      user_repository.dart
      profile_repository.dart
    services/
      auth_service.dart      ← JWT + password logic
    router.dart              ← route definitions
  migrations/
    001_create_users.sql
    002_create_profiles.sql
  docker-compose.yml
  Dockerfile
  .env
  .env.example
</code></pre>
<p>This separation of concerns maps directly to what you'll already know if you're a Flutter engineer: models, repositories, and services are the same concepts. Handlers replace ViewModels or Controllers. Middleware replaces interceptors.</p>
<h3 id="heading-database-setup-with-docker">Database Setup with Docker</h3>
<p>Create docker-compose.yml in the project root:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: user_profile_db
    environment:
      POSTGRES_DB: user_profile_api
      POSTGRES_USER: dart_user
      POSTGRES_PASSWORD: dart_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
</code></pre>
<p>Start the database:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>Verify that it's running:</p>
<pre><code class="language-bash">docker compose ps
# user_profile_db   running   0.0.0.0:5432-&gt;5432/tcp
</code></pre>
<h3 id="heading-environment-configuration">Environment Configuration</h3>
<p>Create .env in the project root:</p>
<pre><code class="language-plaintext">DB_HOST=localhost
DB_PORT=5432
DB_NAME=user_profile_api
DB_USER=dart_user
DB_PASSWORD=dart_password
JWT_SECRET=your_super_secret_key_change_this_in_production
JWT_EXPIRY_HOURS=24
PORT=8080
</code></pre>
<p>Create .env.example with the same keys but no values. This is what you commit to Git:</p>
<pre><code class="language-plaintext">DB_HOST=
DB_PORT=
DB_NAME=
DB_USER=
DB_PASSWORD=
JWT_SECRET=
JWT_EXPIRY_HOURS=
PORT=
</code></pre>
<p>Add .env to .gitignore:</p>
<pre><code class="language-plaintext">.env
</code></pre>
<p>Create lib/config/env.dart:</p>
<pre><code class="language-dart">import 'package:dotenv/dotenv.dart';

class Env {
  static late final DotEnv _env;

  static void load() {
    _env = DotEnv(includePlatformEnvironment: true)..load();
  }

  static String get dbHost =&gt; _env['DB_HOST'] ?? 'localhost';
  static int get dbPort =&gt; int.parse(_env['DB_PORT'] ?? '5432');
  static String get dbName =&gt; _env['DB_NAME'] ?? 'user_profile_api';
  static String get dbUser =&gt; _env['DB_USER'] ?? 'dart_user';
  static String get dbPassword =&gt; _env['DB_PASSWORD'] ?? '';
  static String get jwtSecret =&gt; _env['JWT_SECRET'] ?? '';
  static int get jwtExpiryHours =&gt; int.parse(_env['JWT_EXPIRY_HOURS'] ?? '24');
  static int get port =&gt; int.parse(_env['PORT'] ?? '8080');
}
</code></pre>
<p>includePlatformEnvironment: true means the Env class reads from both the .env file and real system environment variables, so the same code works locally with a .env file and in production with injected environment variables.</p>
<h2 id="heading-shelf-core-concepts">Shelf Core Concepts</h2>
<p>Before building the API, it's worth understanding each Shelf concept properly – not just what it does, but why it's designed the way it is.</p>
<h3 id="heading-handlers">Handlers</h3>
<p>A handler is the most fundamental unit in Shelf. It's simply a function:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Response helloHandler(Request request) {
  return Response.ok('Hello, Dart backend!');
}
</code></pre>
<p>Request in, Response out. That's the entire contract. Every endpoint you write is a handler. Every piece of middleware is a function that takes a handler and returns a handler.</p>
<p>Handlers can be async:</p>
<pre><code class="language-dart">Future&lt;Response&gt; getUserHandler(Request request) async {
  final users = await userRepository.findAll();
  return Response.ok(jsonEncode(users));
}
</code></pre>
<h3 id="heading-request-and-response">Request and Response</h3>
<p>Request gives you everything about the incoming HTTP call:</p>
<pre><code class="language-dart">Future&lt;Response&gt; handler(Request request) async {
  // URL and path
  print(request.url);           // the full URL
  print(request.url.path);      // just the path

  // Path parameters (when using shelf_router)
  final id = request.params['id'];

  // Query parameters
  final page = request.url.queryParameters['page'];

  // Headers
  final auth = request.headers['authorization'];

  // Body
  final body = await request.readAsString();
  final json = jsonDecode(body) as Map&lt;String, dynamic&gt;;

  return Response.ok('handled');
}
</code></pre>
<p>Response has named constructors for common status codes:</p>
<pre><code class="language-dart">Response.ok(body)           // 200
Response.notFound(body)     // 404
Response(201, body: body)   // any status code
Response(400, body: body)   // bad request
Response(401, body: body)   // unauthorized
Response(500, body: body)   // server error
</code></pre>
<p>Always set the Content-Type header when returning JSON:</p>
<pre><code class="language-dart">Response.ok(
  jsonEncode({'message': 'success'}),
  headers: {'Content-Type': 'application/json'},
)
</code></pre>
<h3 id="heading-router">Router</h3>
<p>shelf_router maps URL patterns and HTTP methods to handlers:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';

final router = Router();

router.get('/users', getAllUsersHandler);
router.get('/users/&lt;id&gt;', getUserHandler);
router.post('/users', createUserHandler);
router.put('/users/&lt;id&gt;', updateUserHandler);
router.delete('/users/&lt;id&gt;', deleteUserHandler);
</code></pre>
<p>The syntax defines a path parameter. Access it inside the handler via request.params['id'].</p>
<h3 id="heading-pipeline-and-middleware">Pipeline and Middleware</h3>
<p>A Pipeline chains middleware together with a handler at the end:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

final handler = Pipeline()
    .addMiddleware(loggerMiddleware())
    .addMiddleware(errorMiddleware())
    .addMiddleware(authMiddleware())
    .addHandler(router.call);
</code></pre>
<p>Middleware is a function with this signature:</p>
<pre><code class="language-dart">Middleware myMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      // Before the handler runs
      print('Request received: \({request.method} \){request.url}');

      final response = await innerHandler(request);

      // After the handler runs
      print('Response sent: ${response.statusCode}');

      return response;
    };
  };
}
</code></pre>
<p>The outer function returns a Middleware. That Middleware is a function that takes the next Handler in the chain and returns a new Handler. This nesting is what allows middleware to run code both before and after the inner handler.</p>
<h2 id="heading-connecting-to-postgresql">Connecting to PostgreSQL</h2>
<h3 id="heading-the-database-connection-manager">The Database Connection Manager</h3>
<p>Create lib/config/database.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import 'env.dart';

class Database {
  static Connection? _connection;

  static Future&lt;Connection&gt; get connection async {
    if (_connection != null) return _connection!;
    _connection = await _connect();
    return _connection!;
  }

  static Future&lt;Connection&gt; _connect() async {
    final conn = await Connection.open(
      Endpoint(
        host: Env.dbHost,
        port: Env.dbPort,
        database: Env.dbName,
        username: Env.dbUser,
        password: Env.dbPassword,
      ),
      settings: const ConnectionSettings(
        sslMode: SslMode.disable,
      ),
    );

    print('✅ Database connected: \({Env.dbHost}:\){Env.dbPort}/${Env.dbName}');
    return conn;
  }

  static Future&lt;void&gt; close() async {
    await _connection?.close();
    _connection = null;
  }
}
</code></pre>
<p>This is a singleton connection manager – the same pattern Flutter engineers use for shared services. The connection is created once on first access and reused for every subsequent database call.</p>
<h3 id="heading-running-migrations">Running Migrations</h3>
<p>Create the migrations folder and SQL files:</p>
<p>migrations/001_create_users.sql:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  first_name VARCHAR(100) NOT NULL,
  last_name VARCHAR(100) NOT NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
</code></pre>
<p>migrations/002_create_profiles.sql:</p>
<pre><code class="language-sql">CREATE TABLE IF NOT EXISTS profiles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  bio TEXT,
  avatar_url VARCHAR(500),
  phone VARCHAR(20),
  location VARCHAR(255),
  website VARCHAR(500),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(user_id)
);

CREATE INDEX IF NOT EXISTS idx_profiles_user_id ON profiles(user_id);
</code></pre>
<p>Create a migration runner in lib/config/database.dart:</p>
<pre><code class="language-dart">static Future&lt;void&gt; runMigrations() async {
  final conn = await connection;
  final migrationsDir = Directory('migrations');

  final files = migrationsDir
      .listSync()
      .whereType&lt;File&gt;()
      .where((f) =&gt; f.path.endsWith('.sql'))
      .toList()
    ..sort((a, b) =&gt; a.path.compareTo(b.path));

  for (final file in files) {
    final sql = await file.readAsString();
    await conn.execute(sql);
    print('✅ Migration applied: ${file.path}');
  }
}
</code></pre>
<h2 id="heading-building-the-api">Building the API</h2>
<p>With the database connected and migrations in place, we can now build the actual API layer.</p>
<p>This section covers the models, repositories, and handlers for both users and profiles. Models define the shape of the data, repositories handle all database interactions, and handlers translate HTTP requests into repository calls and send responses back to the client. We'll build the user layer first, then the profile layer on top of it.</p>
<h3 id="heading-the-user-model">The User Model</h3>
<p>The User model represents a single user record in the database. It maps directly to the users table created in the migration and handles two-way conversion between database rows and Dart objects.</p>
<p>Create lib/models/user.dart:</p>
<pre><code class="language-dart">class User {
  final String id;
  final String email;
  final String passwordHash;
  final String firstName;
  final String lastName;
  final bool isActive;
  final DateTime createdAt;
  final DateTime updatedAt;

  const User({
    required this.id,
    required this.email,
    required this.passwordHash,
    required this.firstName,
    required this.lastName,
    required this.isActive,
    required this.createdAt,
    required this.updatedAt,
  });

  factory User.fromRow(Map&lt;String, dynamic&gt; row) =&gt; User(
        id: row['id'] as String,
        email: row['email'] as String,
        passwordHash: row['password_hash'] as String,
        firstName: row['first_name'] as String,
        lastName: row['last_name'] as String,
        isActive: row['is_active'] as bool,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  // Never include passwordHash in JSON responses
  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'email': email,
        'firstName': firstName,
        'lastName': lastName,
        'isActive': isActive,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<p>fromRow maps a PostgreSQL result row to a User. toJson deliberately excludes passwordHash – you should never return password data in API responses.</p>
<h3 id="heading-the-user-repository">The User Repository</h3>
<p>The UserRepository is the single point of contact between the application and the users table. Every database operation for users goes through here, keeping the SQL contained and the handlers clean.</p>
<p>Create lib/repositories/user_repository.dart:</p>
<pre><code class="language-dart">import 'dart:async';
import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/user.dart';

class UserRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;List&lt;User&gt;&gt; findAll() async {
    final conn = await _conn;
    final results = await conn.execute(
      'SELECT * FROM users WHERE is_active = TRUE ORDER BY created_at DESC',
    );

    return results.map((row) =&gt; User.fromRow(row.toColumnMap())).toList();
  }

  Future&lt;User?&gt; findById(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE id = @id AND is_active = TRUE'),
      parameters: {'id': id},
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; findByEmail(String email) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM users WHERE email = @email'),
      parameters: {'email': email},
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User&gt; create({
    required String email,
    required String passwordHash,
    required String firstName,
    required String lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO users (email, password_hash, first_name, last_name)
        VALUES (@email, @passwordHash, @firstName, @lastName)
        RETURNING *
      '''),
      parameters: {
        'email': email,
        'passwordHash': passwordHash,
        'firstName': firstName,
        'lastName': lastName,
      },
    );

    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;User?&gt; update({
    required String id,
    String? firstName,
    String? lastName,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users
        SET
          first_name = COALESCE(@firstName, first_name),
          last_name  = COALESCE(@lastName, last_name),
          updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING *
      '''),
      parameters: {
        'id': id,
        'firstName': firstName,
        'lastName': lastName,
      },
    );

    if (results.isEmpty) return null;
    return User.fromRow(results.first.toColumnMap());
  }

  Future&lt;bool&gt; delete(String id) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE users SET is_active = FALSE, updated_at = NOW()
        WHERE id = @id AND is_active = TRUE
        RETURNING id
      '''),
      parameters: {'id': id},
    );

    return results.isNotEmpty;
  }
}
</code></pre>
<p>A few things worth noting here. Sql.named uses named parameters (@paramName) instead of positional parameters. This prevents SQL injection and makes queries readable.</p>
<p>Also, the delete operation is a soft delete. It sets is_active = FALSE rather than removing the row. This is the standard production approach: data is never truly deleted, it's deactivated.</p>
<p>COALESCE(@firstName, first_name) on the update means: use the new value if provided, otherwise keep the existing value. This handles partial updates cleanly without requiring all fields every time.</p>
<h3 id="heading-user-handlers">User Handlers</h3>
<p>The UserHandler class exposes the repository operations as HTTP endpoints. It owns a Router instance internally and maps each route to a private method, keeping the routing logic and the handler logic together in one place.</p>
<p>Create lib/handlers/user_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';

class UserHandler {
  final UserRepository _repository;

  UserHandler(this._repository);

  Router get router {
    final router = Router();
    router.get('/', _getAll);
    router.get('/&lt;id&gt;', _getOne);
    router.put('/&lt;id&gt;', _update);
    router.delete('/&lt;id&gt;', _delete);
    return router;
  }

  Future&lt;Response&gt; _getAll(Request request) async {
    final users = await _repository.findAll();
    return Response.ok(
      jsonEncode(users.map((u) =&gt; u.toJson()).toList()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _getOne(Request request, String id) async {
    final user = await _repository.findById(id);

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _update(Request request, String id) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final user = await _repository.update(
      id: id,
      firstName: body['firstName'] as String?,
      lastName: body['lastName'] as String?,
    );

    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(user.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _delete(Request request, String id) async {
    final deleted = await _repository.delete(id);

    if (!deleted) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response(
      204,
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h3 id="heading-the-profile-model">The Profile Model</h3>
<p>The Profile model represents a user's extended information, stored separately from the core user record. The one-to-one relationship is enforced by the unique index on user_id in the profiles table. All fields except userId are nullable since a profile can be created with partial information and filled in over time.</p>
<p>Create lib/models/profile.dart:</p>
<pre><code class="language-dart">class Profile {
  final String id;
  final String userId;
  final String? bio;
  final String? avatarUrl;
  final String? phone;
  final String? location;
  final String? website;
  final DateTime createdAt;
  final DateTime updatedAt;

  const Profile({
    required this.id,
    required this.userId,
    this.bio,
    this.avatarUrl,
    this.phone,
    this.location,
    this.website,
    required this.createdAt,
    required this.updatedAt,
  });

  factory Profile.fromRow(Map&lt;String, dynamic&gt; row) =&gt; Profile(
        id: row['id'] as String,
        userId: row['user_id'] as String,
        bio: row['bio'] as String?,
        avatarUrl: row['avatar_url'] as String?,
        phone: row['phone'] as String?,
        location: row['location'] as String?,
        website: row['website'] as String?,
        createdAt: row['created_at'] as DateTime,
        updatedAt: row['updated_at'] as DateTime,
      );

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
        'createdAt': createdAt.toIso8601String(),
        'updatedAt': updatedAt.toIso8601String(),
      };
}
</code></pre>
<h3 id="heading-the-profile-repository">The Profile Repository</h3>
<p>The ProfileRepository handles all database operations for the profiles table. Unlike the user repository which looks up by id, most profile operations use userId as the lookup key since that is how the client references a profile — by whose it belongs to, not by its own internal ID.</p>
<p>Create lib/repositories/profile_repository.dart:</p>
<pre><code class="language-dart">import 'package:postgres/postgres.dart';
import '../config/database.dart';
import '../models/profile.dart';

class ProfileRepository {
  Future&lt;Connection&gt; get _conn =&gt; Database.connection;

  Future&lt;Profile?&gt; findByUserId(String userId) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('SELECT * FROM profiles WHERE user_id = @userId'),
      parameters: {'userId': userId},
    );

    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile&gt; create({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        INSERT INTO profiles (user_id, bio, avatar_url, phone, location, website)
        VALUES (@userId, @bio, @avatarUrl, @phone, @location, @website)
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );

    return Profile.fromRow(results.first.toColumnMap());
  }

  Future&lt;Profile?&gt; update({
    required String userId,
    String? bio,
    String? avatarUrl,
    String? phone,
    String? location,
    String? website,
  }) async {
    final conn = await _conn;
    final results = await conn.execute(
      Sql.named('''
        UPDATE profiles
        SET
          bio        = COALESCE(@bio, bio),
          avatar_url = COALESCE(@avatarUrl, avatar_url),
          phone      = COALESCE(@phone, phone),
          location   = COALESCE(@location, location),
          website    = COALESCE(@website, website),
          updated_at = NOW()
        WHERE user_id = @userId
        RETURNING *
      '''),
      parameters: {
        'userId': userId,
        'bio': bio,
        'avatarUrl': avatarUrl,
        'phone': phone,
        'location': location,
        'website': website,
      },
    );

    if (results.isEmpty) return null;
    return Profile.fromRow(results.first.toColumnMap());
  }
}
</code></pre>
<h3 id="heading-profile-handlers">Profile Handlers</h3>
<p>The ProfileHandler manages the profile endpoints nested under a user's ID. Before every operation, it verifies the parent user exists — a profile can't be created, fetched, or updated for a user that doesn't exist. It also prevents duplicate profiles by checking for an existing record before allowing a create.</p>
<p>Create lib/handlers/profile_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/profile_repository.dart';
import '../repositories/user_repository.dart';

class ProfileHandler {
  final ProfileRepository _profileRepository;
  final UserRepository _userRepository;

  ProfileHandler(this._profileRepository, this._userRepository);

  Router get router {
    final router = Router();
    router.get('/&lt;userId&gt;/profile', _getProfile);
    router.post('/&lt;userId&gt;/profile', _createProfile);
    router.put('/&lt;userId&gt;/profile', _updateProfile);
    return router;
  }

  Future&lt;Response&gt; _getProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final profile = await _profileRepository.findByUserId(userId);
    if (profile == null) {
      return Response.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _createProfile(Request request, String userId) async {
    final user = await _userRepository.findById(userId);
    if (user == null) {
      return Response.notFound(
        jsonEncode({'error': 'User not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _profileRepository.findByUserId(userId);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'Profile already exists for this user'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.create(
      userId: userId,
      bio: body['bio'] as String?,
      avatarUrl: body['avatarUrl'] as String?,
      phone: body['phone'] as String?,
      location: body['location'] as String?,
      website: body['website'] as String?,
    );

    return Response(
      201,
      body: jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _updateProfile(Request request, String userId) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final profile = await _profileRepository.update(
      userId: userId,
      bio: body['bio'] as String?,
      avatarUrl: body['avatarUrl'] as String?,
      phone: body['phone'] as String?,
      location: body['location'] as String?,
      website: body['website'] as String?,
    );

    if (profile == null) {
      return Response.notFound(
        jsonEncode({'error': 'Profile not found'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode(profile.toJson()),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<h2 id="heading-authentication">Authentication</h2>
<p>With the core user and profile CRUD in place, the next step is securing the API.</p>
<p>Authentication in this project works in two parts: an AuthService handles the cryptographic operations — password hashing and JWT generation and verification — and an AuthHandler exposes the register and login endpoints that clients call to get a token. Once a token is issued, the AuthMiddleware validates it on every protected request before it reaches a handler.</p>
<h3 id="heading-password-hashing">Password Hashing</h3>
<p>Create lib/services/auth_service.dart:</p>
<pre><code class="language-dart">import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../config/env.dart';
import '../models/user.dart';

class AuthService {
  String hashPassword(String password) {
    return BCrypt.hashpw(password, BCrypt.gensalt());
  }

  bool verifyPassword(String password, String hash) {
    return BCrypt.checkpw(password, hash);
  }

  String generateToken(User user) {
    final jwt = JWT(
      {
        'sub': user.id,
        'email': user.email,
        'iat': DateTime.now().millisecondsSinceEpoch ~/ 1000,
      },
    );

    return jwt.sign(
      SecretKey(Env.jwtSecret),
      expiresIn: Duration(hours: Env.jwtExpiryHours),
    );
  }

  JWT? verifyToken(String token) {
    try {
      return JWT.verify(token, SecretKey(Env.jwtSecret));
    } catch (_) {
      return null;
    }
  }
}
</code></pre>
<p>BCrypt.hashpw generates a salted hash. BCrypt.checkpw verifies a plain password against a stored hash. The salt is embedded in the hash itself – you don't store it separately.</p>
<p>verifyToken returns null on any failure, expired token, invalid signature, or malformed token rather than throwing. This keeps the auth middleware clean.</p>
<h3 id="heading-auth-handlers">Auth Handlers</h3>
<p>Create lib/handlers/auth_handler.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import '../repositories/user_repository.dart';
import '../services/auth_service.dart';

class AuthHandler {
  final UserRepository _userRepository;
  final AuthService _authService;

  AuthHandler(this._userRepository, this._authService);

  Router get router {
    final router = Router();
    router.post('/register', _register);
    router.post('/login', _login);
    return router;
  }

  Future&lt;Response&gt; _register(Request request) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final email = body['email'] as String?;
    final password = body['password'] as String?;
    final firstName = body['firstName'] as String?;
    final lastName = body['lastName'] as String?;

    if (email == null || password == null || firstName == null || lastName == null) {
      return Response(
        400,
        body: jsonEncode({'error': 'email, password, firstName, and lastName are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    if (password.length &lt; 8) {
      return Response(
        400,
        body: jsonEncode({'error': 'Password must be at least 8 characters'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final existing = await _userRepository.findByEmail(email);
    if (existing != null) {
      return Response(
        409,
        body: jsonEncode({'error': 'An account with this email already exists'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final passwordHash = _authService.hashPassword(password);

    final user = await _userRepository.create(
      email: email,
      passwordHash: passwordHash,
      firstName: firstName,
      lastName: lastName,
    );

    final token = _authService.generateToken(user);

    return Response(
      201,
      body: jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }

  Future&lt;Response&gt; _login(Request request) async {
    final body = jsonDecode(await request.readAsString()) as Map&lt;String, dynamic&gt;;

    final email = body['email'] as String?;
    final password = body['password'] as String?;

    if (email == null || password == null) {
      return Response(
        400,
        body: jsonEncode({'error': 'email and password are required'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final user = await _userRepository.findByEmail(email);

    // Deliberately vague error, never confirm whether an email exists
    if (user == null || !_authService.verifyPassword(password, user.passwordHash)) {
      return Response(
        401,
        body: jsonEncode({'error': 'Invalid email or password'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    final token = _authService.generateToken(user);

    return Response.ok(
      jsonEncode({
        'user': user.toJson(),
        'token': token,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  }
}
</code></pre>
<p>The login error message is deliberately vague: "Invalid email or password" rather than "Email not found" or "Wrong password." Confirming which part is wrong helps attackers enumerate valid accounts.</p>
<h3 id="heading-auth-middleware">Auth Middleware</h3>
<p>Create lib/middleware/auth_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';
import '../services/auth_service.dart';

Middleware authMiddleware(AuthService authService) {
  return (Handler innerHandler) {
    return (Request request) async {
      final authHeader = request.headers['authorization'];

      if (authHeader == null || !authHeader.startsWith('Bearer ')) {
        return Response(
          401,
          body: jsonEncode({'error': 'Authorization header missing or malformed'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      final token = authHeader.substring(7); // Remove 'Bearer '
      final jwt = authService.verifyToken(token);

      if (jwt == null) {
        return Response(
          401,
          body: jsonEncode({'error': 'Invalid or expired token'}),
          headers: {'Content-Type': 'application/json'},
        );
      }

      // Attach the user ID to the request context for downstream handlers
      final updatedRequest = request.change(
        context: {
          ...request.context,
          'userId': jwt.payload['sub'] as String,
          'userEmail': jwt.payload['email'] as String,
        },
      );

      return innerHandler(updatedRequest);
    };
  };
}
</code></pre>
<p>request.change(context: {...}) is how Shelf passes data from middleware to handlers, the equivalent of attaching data to a request in Express or ASP.NET middleware. Any handler downstream can read request.context['userId'] to know which user is authenticated.</p>
<h2 id="heading-error-handling">Error Handling</h2>
<p>No matter how carefully you write your handlers, unexpected failures will happen in production — malformed request bodies, database timeouts, unhandled edge cases.</p>
<p>Rather than letting each handler manage its own error responses individually, we'll centralise error handling in a single middleware that wraps the entire pipeline. This guarantees a consistent error response shape across every endpoint and prevents internal error details from leaking to the client.</p>
<p>Create lib/middleware/error_middleware.dart:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'package:shelf/shelf.dart';

Middleware errorMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      try {
        return await innerHandler(request);
      } on FormatException catch (e) {
        return Response(
          400,
          body: jsonEncode({'error': 'Invalid request body: ${e.message}'}),
          headers: {'Content-Type': 'application/json'},
        );
      } catch (e, stackTrace) {
        // Log the full error and stack trace server-side
        print('Unhandled error: $e');
        print(stackTrace);

        // Never expose internal error details to the client
        return Response(
          500,
          body: jsonEncode({'error': 'An internal server error occurred'}),
          headers: {'Content-Type': 'application/json'},
        );
      }
    };
  };
}
</code></pre>
<p>Create lib/middleware/logger_middleware.dart:</p>
<pre><code class="language-dart">import 'package:shelf/shelf.dart';

Middleware loggerMiddleware() {
  return (Handler innerHandler) {
    return (Request request) async {
      final start = DateTime.now();

      final response = await innerHandler(request);

      final duration = DateTime.now().difference(start).inMilliseconds;
      print(
        '[${DateTime.now().toIso8601String()}] '
        '\({request.method} \){request.url.path} '
        '→ \({response.statusCode} (\){duration}ms)',
      );

      return response;
    };
  };
}
</code></pre>
<h2 id="heading-wiring-everything-together">Wiring Everything Together</h2>
<p>With the handlers, repositories, and middleware all in place, the final step is connecting them into a single running server. The router maps URL prefixes to their handler, the pipeline stacks the middleware in the correct order, and the entry point boots everything up in sequence — loading environment variables, running migrations, and starting the server.</p>
<p>Create lib/router.dart:</p>
<pre><code class="language-dart">import 'package:shelf_router/shelf_router.dart';
import 'handlers/auth_handler.dart';
import 'handlers/user_handler.dart';
import 'handlers/profile_handler.dart';
import 'middleware/auth_middleware.dart';
import 'repositories/user_repository.dart';
import 'repositories/profile_repository.dart';
import 'services/auth_service.dart';

Router createRouter() {
  final userRepository = UserRepository();
  final profileRepository = ProfileRepository();
  final authService = AuthService();

  final authHandler = AuthHandler(userRepository, authService);
  final userHandler = UserHandler(userRepository);
  final profileHandler = ProfileHandler(profileRepository, userRepository);

  final router = Router();

  // Public routes, no auth required
  router.mount('/auth', authHandler.router.call);

  // Protected routes, auth middleware applied
  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(userHandler.router.call),
  );

  router.mount(
    '/users',
    Pipeline()
        .addMiddleware(authMiddleware(authService))
        .addHandler(profileHandler.router.call),
  );

  return router;
}
</code></pre>
<p>Create the entry point bin/server.dart:</p>
<pre><code class="language-dart">import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import '../lib/config/database.dart';
import '../lib/config/env.dart';
import '../lib/middleware/error_middleware.dart';
import '../lib/middleware/logger_middleware.dart';
import '../lib/router.dart';

void main() async {
  // Load environment variables
  Env.load();

  // Run database migrations
  await Database.runMigrations();

  // Build the handler pipeline
  final router = createRouter();

  final handler = Pipeline()
      .addMiddleware(errorMiddleware())
      .addMiddleware(loggerMiddleware())
      .addHandler(router.call);

  // Start the server
  final server = await shelf_io.serve(
    handler,
    InternetAddress.anyIPv4,
    Env.port,
  );

  print('🚀 Server running on port ${server.port}');
}
</code></pre>
<p>Run the server:</p>
<pre><code class="language-bash">dart run bin/server.dart
# ✅ Database connected: localhost:5432/user_profile_api
# ✅ Migration applied: migrations/001_create_users.sql
# ✅ Migration applied: migrations/002_create_profiles.sql
# 🚀 Server running on port 8080
</code></pre>
<h2 id="heading-deployment">Deployment</h2>
<p>The server is running locally and all endpoints are working. Now it's time to ship it.</p>
<p>We'll cover two deployment paths: first packaging the app and database together with Docker Compose for local production testing, then deploying to Fly.io where your API will be accessible over the internet with a managed PostgreSQL database and automatic TLS.</p>
<h3 id="heading-dockerfile">Dockerfile</h3>
<p>Create Dockerfile in the project root:</p>
<pre><code class="language-dockerfile">FROM dart:stable AS build

WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart compile exe bin/server.dart -o bin/server

FROM debian:stable-slim

RUN apt-get update &amp;&amp; apt-get install -y ca-certificates &amp;&amp; rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=build /app/bin/server bin/server
COPY --from=build /app/migrations migrations/

EXPOSE 8080

CMD ["bin/server"]
</code></pre>
<p>This is a multi-stage build. The first stage uses the full Dart SDK image to compile the server to a native binary. The second stage copies only the compiled binary and migrations into a minimal Debian image – no Dart SDK, no source code, no build tools. The final image is lean and production-ready.</p>
<h3 id="heading-docker-compose-for-local-production-testing">Docker Compose for Local Production Testing</h3>
<p>Update docker-compose.yml to include the app alongside the database:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:16-alpine
    container_name: user_profile_db
    environment:
      POSTGRES_DB: user_profile_api
      POSTGRES_USER: dart_user
      POSTGRES_PASSWORD: dart_password
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dart_user -d user_profile_api"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    build: .
    container_name: user_profile_api
    ports:
      - "8080:8080"
    environment:
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: user_profile_api
      DB_USER: dart_user
      DB_PASSWORD: dart_password
      JWT_SECRET: local_test_secret_replace_in_production
      JWT_EXPIRY_HOURS: 24
      PORT: 8080
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
</code></pre>
<p>The healthcheck on the Postgres service ensures that the API container only starts once the database is ready to accept connections (a common production problem when services start simultaneously).</p>
<p>Build and run everything:</p>
<pre><code class="language-bash">docker compose up --build
</code></pre>
<h3 id="heading-deploying-to-flyio">Deploying to Fly.io</h3>
<p>Fly.io is one of the cleanest deployment targets for containerized backend services. It handles global distribution, automatic TLS, and managed PostgreSQL databases.</p>
<p><strong>Step 1 – Install and authenticate:</strong></p>
<pre><code class="language-bash"># macOS
brew install flyctl

# Authenticate
fly auth login
</code></pre>
<p><strong>Step 2 – Launch the app:</strong></p>
<pre><code class="language-bash">fly launch
</code></pre>
<p>Fly detects the Dockerfile automatically and asks a few questions: app name, region, and whether to create a PostgreSQL database. Answer yes to the PostgreSQL prompt, and Fly will provision a managed database and inject the connection string automatically.</p>
<p><strong>Step 3 – Set environment variables:</strong></p>
<pre><code class="language-bash">fly secrets set JWT_SECRET="your_production_secret_here"
fly secrets set JWT_EXPIRY_HOURS="24"
</code></pre>
<p>Database connection variables are set automatically by Fly when it provisions the PostgreSQL cluster.</p>
<p><strong>Step 4 – Deploy:</strong></p>
<pre><code class="language-bash">fly deploy
</code></pre>
<p>Fly builds the Docker image, pushes it to their registry, and deploys it to your chosen region. Once complete:</p>
<pre><code class="language-bash">fly status
# Your app is running at https://your-app-name.fly.dev
</code></pre>
<p><strong>Step 5 – Verify the deployment:</strong></p>
<pre><code class="language-bash">curl https://your-app-name.fly.dev/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"password123","firstName":"Seyi","lastName":"Dev"}'
</code></pre>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>With the server running locally on port 8080, here's the full flow to verify that everything works end to end.</p>
<p>Register a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "email": "seyi@example.com",
    "password": "securepassword",
    "firstName": "Seyi",
    "lastName": "Dev"
  }'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "user": {
    "id": "uuid-here",
    "email": "seyi@example.com",
    "firstName": "Seyi",
    "lastName": "Dev",
    "isActive": true,
    "createdAt": "2025-01-01T00:00:00.000Z",
    "updatedAt": "2025-01-01T00:00:00.000Z"
  },
  "token": "eyJhbGci..."
}
</code></pre>
<p>Login:</p>
<pre><code class="language-bash">curl http://localhost:8080/auth/login \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "seyi@example.com", "password": "securepassword"}'
</code></pre>
<p>Get all users (authenticated):</p>
<pre><code class="language-bash">curl http://localhost:8080/users \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<p>Create a profile:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId}/profile \
  -X POST \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{
    "bio": "Flutter engineer turned backend developer",
    "location": "Lagos, Nigeria",
    "website": "https://example.com"
  }'
</code></pre>
<p>Update a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X PUT \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"firstName": "Oluwaseyi"}'
</code></pre>
<p>Delete a user:</p>
<pre><code class="language-bash">curl http://localhost:8080/users/{userId} \
  -X DELETE \
  -H "Authorization: Bearer eyJhbGci..."
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You just built and deployed a production-grade REST API in Dart – the same language you already know from Flutter. No new language, no new paradigm. Just Dart running in a different context.</p>
<p>The Shelf mental model (Handlers, Middleware, Pipelines, Routers) is deliberately minimal. It doesn't make decisions for you. It gives you composable primitives and lets you assemble them into exactly the architecture your project needs. That philosophy will feel familiar to Flutter engineers who build their own clean architecture rather than relying on a prescriptive framework.</p>
<p>What you built here – models, repositories, services, handlers, and middleware – is the same separation of concerns you apply in Flutter, applied to the backend. The concepts transfer. The Dart skills transfer. The architecture discipline transfers.</p>
<p>With this, you'll understand that Dart is a powerful language that cuts across both frontend and backend ecosystems. Aside from Shelf, we have Dartfrog and Serverpod which still functions well on the backend side of things. More on those in upcoming articles.</p>
<p>So yeah, try this out and thank me later!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Tradeoff That Slows Production Teams Down: Flexibility vs Actually Shipping ]]>
                </title>
                <description>
                    <![CDATA[ Every company says it wants speed. Roadmaps talk about velocity. Leadership meetings talk about reducing cycle time. Quarterly goals talk about faster execution and quicker releases. Every business wa ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-tradeoff-that-slows-production-teams-down-flexibility-vs-actually-shipping/</link>
                <guid isPermaLink="false">6a19ccc19e433f18f384364b</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ production ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PaaS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Fri, 29 May 2026 17:28:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/495a017a-0f6f-4e3b-8d55-6c3854917c51.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every company says it wants speed.</p>
<p>Roadmaps talk about velocity. Leadership meetings talk about reducing cycle time. Quarterly goals talk about faster execution and quicker releases.</p>
<p>Every business wants teams moving faster.</p>
<p>Then many of those same companies make a decision that quietly slows everything down. They optimise for infrastructure flexibility instead of product delivery.</p>
<p>It sounds reasonable in the beginning. Teams want control. Engineers want options. Platform architects want systems that can support every future scenario.</p>
<p>So production teams start building infrastructure ecosystems around themselves.</p>
<p>Deployment pipelines get built from scratch. Cloud resources become heavily customised. Internal platforms gain endless knobs, switches, and configuration layers. New projects begin with architecture discussions instead of customer problems.</p>
<p>Months later, software delivery slows down.</p>
<p>Product teams miss timelines. Releases move out by quarters. Customer feedback arrives later. Competitors keep shipping.</p>
<p>The tradeoff hiding underneath all of this is simple. Teams choose flexibility over actually shipping.</p>
<p>And beyond a certain point, flexibility becomes one of the most expensive forms of organisational drag a company can create.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-the-myth-that-more-flexibility-creates-better-production-systems">The Myth That More Flexibility Creates Better Production Systems</a></p>
</li>
<li><p><a href="#heading-infrastructure-ownership-quietly-becomes-a-second-business">Infrastructure Ownership Quietly Becomes a Second Business</a></p>
</li>
<li><p><a href="#heading-the-real-cost-is-delayed-customer-learning">The Real Cost Is Delayed Customer Learning</a></p>
</li>
<li><p><a href="#heading-paas-changes-the-optimisation-function">PaaS Changes the Optimisation Function</a></p>
</li>
<li><p><a href="#heading-the-best-production-teams-remove-decisions">The Best Production Teams Remove Decisions</a></p>
</li>
<li><p><a href="#heading-custom-infrastructure-usually-solves-problems-nobody-has-yet">Custom Infrastructure Usually Solves Problems Nobody Has Yet</a></p>
</li>
<li><p><a href="#heading-the-real-competitive-advantage-is-shipping-faster">The Real Competitive Advantage Is Shipping Faster</a></p>
</li>
<li><p><a href="#heading-when-paas-might-not-be-the-right-choice">When PaaS Might Not Be the Right Choice</a></p>
</li>
<li><p><a href="#heading-stop-building-infrastructure-businesses-by-accident">Stop Building Infrastructure Businesses By Accident</a></p>
</li>
</ul>
<h2 id="heading-the-myth-that-more-flexibility-creates-better-production-systems">The Myth That More Flexibility Creates Better Production Systems</h2>
<p>Engineering teams love optionality. The logic sounds convincing.</p>
<p>If infrastructure is fully customizable, teams can adapt to future requirements. If deployment systems are built internally, every use case can be supported. If every layer is configurable, engineers can optimise for unique situations.</p>
<p>This feels like responsible engineering. But it often becomes expensive business behaviour.</p>
<p>Most production teams massively overestimate how often they need deep infrastructure flexibility.</p>
<p>What actually happens becomes predictable.</p>
<p>A product team starts a new initiative. Instead of shipping an early version and learning from customers, discussions begin.</p>
<ul>
<li><p>Should Kubernetes clusters be organised by team or service?</p>
</li>
<li><p>Should CI/CD use GitHub Actions or Jenkins?</p>
</li>
<li><p>Should secrets management use Vault or cloud-native tooling?</p>
</li>
<li><p>Should observability use Prometheus or Datadog?</p>
</li>
<li><p>Should deployment strategies use canary releases, <a href="https://www.redhat.com/en/topics/devops/what-is-blue-green-deployment">blue-green deployments</a>, or something custom?</p>
</li>
</ul>
<p>Weeks disappear. No customer sees anything. No assumptions get tested. No learning happens.</p>
<p>Meanwhile, product managers wait. Leadership waits. Customers wait.</p>
<p>Even with <a href="https://sevalla.com/blog/building-apps-with-sevalla-and-claude-code/">agentic coding tools</a> like Claude generating code, scaffolding systems and accelerating implementation, teams still lose speed when every output collides with infrastructure decisions and deployment debates.</p>
<p>The problem isn't technology. The problem is optimising around theoretical future flexibility instead of present business outcomes.</p>
<p>Software creates value when customers use it. Everything else is support work.</p>
<h2 id="heading-infrastructure-ownership-quietly-becomes-a-second-business">Infrastructure Ownership Quietly Becomes a Second Business</h2>
<p>Traditional deployment models accidentally create a dangerous pattern: companies think they are building products. Slowly, they start building infrastructure organisations.</p>
<p>Production teams provision servers. Then networking. Then IAM systems. Then deployment pipelines. Then, observability layers. Then secrets management. Then autoscaling. Then rollback systems.</p>
<p>Every decision feels reasonable in isolation. But collectively, teams create an operational machine they now own forever.</p>
<p>And ownership is where the hidden cost appears.</p>
<p>Because infrastructure work doesn't end after launch. It expands. Pipelines need maintenance. Security policies change. Monitoring systems require tuning. Platform dependencies break. Internal tooling needs upgrades.</p>
<p>Production teams gradually spend more time maintaining systems around software than improving software itself.</p>
<p>This creates a strange situation: highly paid engineers become caretakers for infrastructure instead of builders of customer value.</p>
<p>No customer purchases a product because deployment pipelines have become elegant. No customer upgrades because IAM policies are beautifully designed. No competitor loses market share because Kubernetes YAML looks sophisticated.</p>
<p>Customers care about products solving problems. Infrastructure only matters when it slows product delivery.</p>
<p>And infrastructure ownership creates endless opportunities for that to happen.</p>
<h2 id="heading-the-real-cost-is-delayed-customer-learning">The Real Cost Is Delayed Customer Learning</h2>
<p>The biggest cost of infrastructure complexity isn't engineering effort. It's delayed learning.</p>
<p>Software companies win through feedback loops. Teams ship something. Customers react. Teams learn. Products improve.</p>
<p>The faster this cycle operates, the stronger the company becomes.</p>
<p>Infrastructure work interrupts that loop. Every month spent building deployment systems is a month where customers aren't using new features. Every quarter spent designing internal platforms delays customer feedback. Every architecture discussion delays real market signals.</p>
<p>This is where many organisations misunderstand velocity.</p>
<p>They look at sprint metrics. They measure tickets completed. They count engineering output.</p>
<p>But business speed isn't measured through internal activity. Business speed measures how quickly ideas become customer reality.</p>
<p>Infrastructure ownership slows that process dramatically. And slower learning creates slower companies.</p>
<h2 id="heading-paas-changes-the-optimisation-function">PaaS Changes the Optimisation Function</h2>
<p>This is where <a href="https://www.freecodecamp.org/news/from-metrics-to-meaning-how-paas-helps-developers-understand-production/">Platform as a Service</a> changes the equation.</p>
<p>PaaS forces organisations to optimise around shipping rather than infrastructure ownership. That shift matters more than most teams realise.</p>
<p>Instead of spending weeks designing deployment architecture, production teams connect repositories and deploy.</p>
<p>Instead of building pipelines manually, pipelines already exist.</p>
<p>Instead of designing scaling systems, scaling becomes infrastructure behaviour rather than engineering work.</p>
<p>Instead of repeatedly building foundations, infrastructure becomes a utility.</p>
<p>That sounds simple. It should be simple. Deployment should feel boring. But the fact that deployment often becomes a major organisational project is usually evidence of unnecessary complexity rather than unavoidable complexity.</p>
<p>PaaS providers remove entire categories of decisions. And while many engineers see that as a compromise, it's often the opposite.</p>
<p>Constraints create speed. Speed creates learning. Learning creates better products.</p>
<h2 id="heading-the-best-production-teams-remove-decisions">The Best Production Teams Remove Decisions</h2>
<p>There's a common misconception that elite engineering organisations maximise options. The opposite is often true.</p>
<p>High-performing production teams aggressively eliminate decisions. They standardise. They create defaults. They remove unnecessary choices.</p>
<p>Because every decision carries a cost.</p>
<p>Cognitive load grows. Coordination increases. Meetings multiply. Dependencies expand. Eventually, the workaround software becomes larger than the software itself.</p>
<p>PaaS systems follow a different philosophy. They intentionally reduce optionality.</p>
<p>That reduction creates focus. And focus creates product velocity. Product velocity creates business outcomes.</p>
<p>The chain is straightforward. Too many organisations break it by introducing infrastructure ownership far too early.</p>
<h2 id="heading-custom-infrastructure-usually-solves-problems-nobody-has-yet">Custom Infrastructure Usually Solves Problems Nobody Has Yet</h2>
<p>One of the most expensive habits in software companies is solving future problems before current ones exist.</p>
<p>Teams build for scale before scale exists. They create multi-region architectures before international users arrive. They build deployment frameworks before deployment pain appears.</p>
<p>This usually comes from good intentions. Engineers want to avoid future rewrites. But the irony is that premature flexibility creates an immediate business slowdown.</p>
<p>A startup with twenty engineers shouldn't operate like a company with ten thousand engineers. Yet many production teams copy infrastructure patterns from giant technology firms.</p>
<p>What gets ignored is context. Large technology companies have entire platform teams maintaining internal systems. They have thousands of engineers supporting infrastructure investments.</p>
<p>Most companies do not.</p>
<p>Copying technical architecture without copying organisational scale creates enormous inefficiency.</p>
<p>PaaS acts as protection against this behaviour. It prevents teams from accidentally becoming infrastructure companies before they become successful product companies.</p>
<h2 id="heading-the-real-competitive-advantage-is-shipping-faster">The Real Competitive Advantage Is Shipping Faster</h2>
<p>Companies rarely lose because infrastructure flexibility was insufficient. They lost because competitors learned faster.</p>
<p>Speed matters. Not speed in sprint or <a href="https://linear.app/">linear dashboards</a>. Not speed in story points.</p>
<p>Actual speed. The ability to move ideas into production quickly. The ability to test assumptions rapidly. The ability to learn continuously.</p>
<p>Shipping creates learning. Learning creates improvement. Improvement creates advantage.</p>
<p>Infrastructure complexity interrupts this loop. PaaS strengthens it.</p>
<p>This is why deployment decisions should never be treated as purely technical discussions. They are business decisions.</p>
<p>Infrastructure ownership affects company velocity. Velocity affects market outcomes.</p>
<p>The argument isn't about servers. The argument is about competitive speed.</p>
<h2 id="heading-when-paas-might-not-be-the-right-choice">When PaaS Might Not Be the Right Choice</h2>
<p>There are situations where PaaS can become limiting.</p>
<p>Organisations with highly specialised infrastructure requirements may require direct control over networking, security layers, hardware optimisation, or deployment behaviour.</p>
<p>Some industries have regulatory requirements that create unusually specific infrastructure needs.</p>
<p>Large organisations with mature platform engineering teams may also justify custom infrastructure investments.</p>
<p>There are also cases where platform costs become meaningful at very large scale.</p>
<p>These scenarios exist. But many companies use edge cases as justification years before they become relevant. They prepare for infrastructure problems they may never have while struggling to ship ordinary product releases today.</p>
<p>That sequence creates unnecessary friction.</p>
<h2 id="heading-stop-building-infrastructure-businesses-by-accident">Stop Building Infrastructure Businesses By Accident</h2>
<p>Engineering culture often celebrates flexibility.</p>
<p>Flexibility sounds sophisticated. It sounds future-proof. It sounds like good systems thinking.</p>
<p>But flexibility carries a cost. Every additional option creates complexity. Every additional decision slows movement. Every additional layer creates maintenance work.</p>
<p>Production teams should ask a simpler question. Does this help us ship customer-facing software faster? If the answer is no, it deserves scrutiny.</p>
<p>Too many companies accidentally build infrastructure ecosystems that optimise for hypothetical future needs.</p>
<p>Meanwhile, competitors deploy products, learn from customers and improve faster.</p>
<p>Shipping beats flexibility. And for many production teams, choosing a PaaS is one of the clearest ways to prove it.</p>
<p>Hope you enjoyed this article. You can <a href="https://linkedin.com/in/manishmshiva">connect with me on LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Advanced Error Handling in Dart: Records, Result Types, Monads, and Freezed Exceptions ]]>
                </title>
                <description>
                    <![CDATA[ Every Dart developer has written this at some point: try {   final user = await repository.getUser(id);   // do something with user } catch (e) {   // what is e? who knows.   print(e.toString()); } I ]]>
                </description>
                <link>https://www.freecodecamp.org/news/advanced-error-handling-in-dart-records-result-types-monads-and-freezed-exceptions/</link>
                <guid isPermaLink="false">6a17657ebadcd8afcb2bcdb4</guid>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ error handling ]]>
                    </category>
                
                    <category>
                        <![CDATA[ exception ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Wed, 27 May 2026 21:43:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/21795781-af21-4c57-9457-6c58f22af656.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every Dart developer has written this at some point:</p>
<pre><code class="language-dart">try {
  final user = await repository.getUser(id);
  // do something with user
} catch (e) {
  // what is e? who knows.
  print(e.toString());
}
</code></pre>
<p>It works. It compiles. It ships. And then six months later, a bug report lands in your inbox from a user who got a blank screen instead of an error message, and you spend three hours tracing it back to a <code>catch (e)</code> block that swallowed the failure silently.</p>
<p>This is the fundamental problem with exception-based error handling in Dart. Exceptions are invisible in function signatures. They carry no type information at the call site. The compiler can't help you because it doesn't know a function can fail.</p>
<p>Every failure path is a social contract between the author and the caller — and social contracts break under pressure, in large teams, and at 2am during an incident.</p>
<p>Production applications deserve better than that.</p>
<p>In this article, we're going to walk through a complete, modern approach to error handling in Dart — the kind used in real production Flutter codebases. We'll start with Dart Records as lightweight result containers, build a proper sealed Result type, extend it into the Monad pattern, integrate the <code>dartz</code> package for functional Either types, and finally cap it off with typed, exhaustive exceptions using Freezed.</p>
<p>By the end, failures in your codebase will be typed, visible, compiler-enforced, and impossible to ignore.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-the-problem-with-exceptions-in-dart">The Problem with Exceptions in Dart</a></p>
</li>
<li><p><a href="#heading-part-1-record-types-as-lightweight-result-containers">Part 1: Record Types as Lightweight Result Containers</a></p>
<ul>
<li><p><a href="#heading-what-are-dart-records">What are Dart Records?</a></p>
</li>
<li><p><a href="#heading-records-as-result-types">Records as Result Types</a></p>
</li>
<li><p><a href="#heading-sealed-classes-as-namespaced-constructors">Sealed Classes as Namespaced Constructors</a></p>
</li>
<li><p><a href="#heading-domain-specific-record-types">Domain-Specific Record Types</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-2-building-a-proper-sealed-result-type">Part 2: Building a Proper Sealed Result Type</a></p>
<ul>
<li><p><a href="#heading-the-appresult-sealed-class">The AppResult Sealed Class</a></p>
</li>
<li><p><a href="#heading-consuming-results-with-when">Consuming Results with when()</a></p>
</li>
<li><p><a href="#heading-why-this-is-better">Why This is Better</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-3-extending-to-the-monad-pattern">Part 3: Extending to the Monad Pattern</a></p>
<ul>
<li><p><a href="#heading-what-makes-something-a-monad">What Makes Something a Monad?</a></p>
</li>
<li><p><a href="#heading-adding-map-and-flatmap">Adding map and flatMap</a></p>
</li>
<li><p><a href="#heading-chaining-operations">Chaining Operations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-4-either-with-dartz">Part 4: Either with dartz</a></p>
<ul>
<li><p><a href="#heading-what-is-either">What is Either?</a></p>
</li>
<li><p><a href="#heading-using-either-in-practice">Using Either in Practice</a></p>
</li>
<li><p><a href="#heading-bridging-records-and-either">Bridging Records and Either</a></p>
</li>
<li><p><a href="#heading-folding-an-either">Folding an Either</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-5-typed-exceptions-with-freezed">Part 5: Typed Exceptions with Freezed</a></p>
<ul>
<li><p><a href="#heading-why-freezed-for-exceptions">Why Freezed for Exceptions?</a></p>
</li>
<li><p><a href="#heading-building-iexception">Building iException</a></p>
</li>
<li><p><a href="#heading-pattern-matching-on-exception-types">Pattern Matching on Exception Types</a></p>
</li>
<li><p><a href="#heading-a-cleaner-base-getter-pattern">A Cleaner Base Getter Pattern</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-part-6-putting-it-all-together">Part 6: Putting It All Together</a></p>
<ul>
<li><p><a href="#heading-the-full-architecture">The Full Architecture</a></p>
</li>
<li><p><a href="#heading-repository-layer">Repository Layer</a></p>
</li>
<li><p><a href="#heading-domain-layer">Domain Layer</a></p>
</li>
<li><p><a href="#heading-presentation-layer">Presentation Layer</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>A working Flutter project with Dart 3.0 or later</p>
</li>
<li><p>Basic familiarity with Dart generics and async/await</p>
</li>
<li><p>Basic understanding of sealed classes in Dart</p>
</li>
<li><p>The <code>freezed</code>, <code>freezed_annotation</code>, and <code>build_runner</code> packages available</p>
</li>
<li><p>The <code>dartz</code> package available</p>
</li>
<li><p><code>flutter pub run build_runner build</code> working in your project</p>
</li>
</ul>
<h2 id="heading-the-problem-with-exceptions-in-dart">The Problem with Exceptions in Dart</h2>
<p>Let's look at what typical exception-based error handling actually looks like across a full stack:</p>
<pre><code class="language-dart">// Repository
Future&lt;User&gt; getUser(String id) async {
  final response = await dio.get('/users/$id');
  return User.fromJson(response.data);
}

// Use case
Future&lt;User&gt; execute(String id) async {
  return await repository.getUser(id);
}

// ViewModel
Future&lt;void&gt; loadUser(String id) async {
  try {
    final user = await useCase.execute(id);
    state = UserState.loaded(user);
  } catch (e) {
    state = UserState.error(e.toString());
  }
}
</code></pre>
<p>This looks reasonable. But there are serious hidden problems here.</p>
<p><strong>The failure is invisible in the signature:</strong> <code>Future&lt;User&gt;</code> tells the caller "you will get a User." It says nothing about what happens when the network fails, when the token expires, or when the JSON is malformed. The caller has to know — by reading the implementation — that this function can fail.</p>
<p><strong>The compiler can't help you:</strong> If you forget the <code>try/catch</code> in the ViewModel, the app compiles fine. The crash happens at runtime, in production, in front of a real user.</p>
<p><code>catch (e)</code> <strong>catches everything:</strong> A typo in a variable name, a null dereference, a real network failure — they all land in the same catch block. You can't distinguish between them without inspecting the error string, which is fragile.</p>
<p><strong>Errors lose their type across layers:</strong> By the time an <code>UnauthorizedException</code> from the API layer reaches the ViewModel, it's just an <code>Object</code>. All structural information is gone.</p>
<p>The solution is to make failures a first-class part of your function signatures, your type system, and your compiler checks. That is exactly what the patterns in this article do.</p>
<h2 id="heading-part-1-record-types-as-lightweight-result-containers">Part 1: Record Types as Lightweight Result Containers</h2>
<h3 id="heading-what-are-dart-records">What are Dart Records?</h3>
<p>Dart 3.0 introduced Records — anonymous, immutable value types that group multiple fields together without needing a full class definition.</p>
<pre><code class="language-dart">// A record with two named fields
({String name, int age}) person = (name: 'Seyi', age: 28);

print(person.name); // Seyi
print(person.age);  // 28
</code></pre>
<p>Records are structurally typed — two records with the same field names and types are the same type, regardless of where they were defined. They're also immutable and compare by value, not by reference.</p>
<h3 id="heading-records-as-result-types">Records as Result Types</h3>
<p>The simplest application of records in error handling is encoding success and failure as a single return type with nullable fields:</p>
<pre><code class="language-dart">typedef Result&lt;E, T&gt; = ({E? e, T? data});
</code></pre>
<p>This defines a record type with two nullable fields — <code>e</code> for the error and <code>data</code> for the success value. The contract is simple: exactly one of them will be non-null.</p>
<pre><code class="language-dart">// On success — data is present, e is null
Result&lt;String, User&gt; result = (e: null, data: user);

// On failure — e is present, data is null
Result&lt;String, User&gt; result = (e: 'User not found', data: null);
</code></pre>
<p>This is already a significant improvement over exceptions. The return type now tells the caller that this function can produce either data or an error. The failure is part of the signature.</p>
<p>You can define more specific typedefs for different layers of your application:</p>
<pre><code class="language-dart">typedef ApiResult&lt;T, E&gt;      = ({T? data, E? exception});
typedef SecurityResponse     = ({bool? isSecured, String? error});
typedef Repository&lt;T&gt;        = ApiResult&lt;T, iException&gt;;
</code></pre>
<p>Each typedef gives a meaningful name to a record shape, making the intent clear at every call site.</p>
<h3 id="heading-sealed-classes-as-namespaced-constructors">Sealed Classes as Namespaced Constructors</h3>
<p>Creating result records manually every time is repetitive and error-prone. The cleanest solution is to use a sealed class purely as a namespace for static factory methods:</p>
<pre><code class="language-dart">sealed class Res&lt;E, T&gt; {
  static Result&lt;E, T&gt; success&lt;E, T&gt;(T data) =&gt; (e: null, data: data);
  static Result&lt;E, T&gt; failure&lt;E, T&gt;(E e) =&gt; (e: e, data: null);
}
</code></pre>
<p>Notice what <code>sealed</code> is doing here: it's not being used for polymorphism. It can't be instantiated. It exists purely to group two related static methods under a meaningful, non-extendable name.</p>
<p>The call site becomes clean and intentional:</p>
<pre><code class="language-dart">// In a repository
Future&lt;Result&lt;iException, User&gt;&gt; getUser(String id) async {
  try {
    final user = await _api.fetchUser(id);
    return Res.success(user);
  } on NetworkException catch (e) {
    return Res.failure(iException.internet(message: e.message));
  }
}
</code></pre>
<p>The same pattern applies for Dio-specific responses:</p>
<pre><code class="language-dart">sealed class DioResult&lt;T, E&gt; {
  static ApiResult&lt;T, E&gt; success&lt;T, E&gt;(T data) =&gt; (data: data, exception: null);
  static ApiResult&lt;T, E&gt; failure&lt;T, E&gt;(E exception) =&gt; (data: null, exception: exception);
}
</code></pre>
<p>And for repository-level results with a simplified type alias:</p>
<pre><code class="language-dart">// GET&lt;E, T&gt; is just ({E? e, T? res})
typedef New&lt;T&gt; = GET&lt;iException, T&gt;;

sealed class R&lt;E, T&gt; {
  static New&lt;T&gt; success&lt;T&gt;(T data) =&gt; (e: null, res: data);
  static New&lt;T&gt; failed&lt;T&gt;(iException error) =&gt; (e: error, res: null);
}
</code></pre>
<p>Each sealed class namespace has a single responsibility and maps to a single layer of the application.</p>
<h3 id="heading-domain-specific-record-types">Domain-Specific Record Types</h3>
<p>Records also work beautifully for domain-specific result shapes that don't fit a generic success/failure pattern:</p>
<pre><code class="language-dart">typedef SecurityResponse = ({bool? isSecured, String? error});

sealed class Check {
  static SecurityResponse isSecured() =&gt; (isSecured: true, error: null);
  static SecurityResponse isInsecured(String error) =&gt; (isSecured: false, error: error);
}
</code></pre>
<p>Using it:</p>
<pre><code class="language-dart">final check = Check.isSecured();
if (check.isSecured == true) {
  // proceed
}

final check = Check.isInsecured('Certificate validation failed');
print(check.error); // Certificate validation failed
</code></pre>
<p>Clean, readable, and self-documenting. The record shape tells you exactly what the function can return.</p>
<p><strong>The limitation to keep in mind:</strong> Record-based result types require you to manually check which field is non-null. There is no compiler enforcement that you handle both cases, and no built-in way to transform the result without unwrapping it manually. That's where a proper sealed Result type becomes necessary.</p>
<h2 id="heading-part-2-building-a-proper-sealed-result-type">Part 2: Building a Proper Sealed Result Type</h2>
<h3 id="heading-the-appresult-sealed-class">The AppResult Sealed Class</h3>
<p>A sealed Result type goes further than a record — it uses Dart's type system to make the two possible states structurally distinct, and provides a <code>when()</code> method that forces the caller to handle both cases at compile time.</p>
<pre><code class="language-dart">import 'app_failure.dart';

sealed class AppResult&lt;T&gt; {
  const AppResult();

  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}

class AppSuccess&lt;T&gt; extends AppResult&lt;T&gt; {
  const AppSuccess(this.value);

  final T value;

  @override
  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return success(value);
  }
}

class AppFailureResult&lt;T&gt; extends AppResult&lt;T&gt; {
  const AppFailureResult(this.error);

  final AppFailure error;

  @override
  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  }) {
    return failure(error);
  }
}
</code></pre>
<p>Let's walk through the design decisions carefully.</p>
<p><code>sealed class AppResult&lt;T&gt;</code>: <code>sealed</code> means all subtypes must live in the same file and the compiler knows every possible subtype. This is what enables exhaustive pattern matching. <code>&lt;T&gt;</code> is the type of data you get on success.</p>
<p><code>AppSuccess&lt;T&gt;</code>: holds the actual data. When <code>when()</code> is called on an <code>AppSuccess</code>, it always calls the <code>success</code> callback and passes the value through.</p>
<p><code>AppFailureResult&lt;T&gt;</code>: holds an <code>AppFailure</code> (your error model). When <code>when()</code> is called on an <code>AppFailureResult</code>, it always calls the <code>failure</code> callback. Notice it still carries <code>&lt;T&gt;</code> even though there is no value — this makes both subtypes compatible with the same <code>AppResult&lt;T&gt;</code> type.</p>
<p><strong>The</strong> <code>when()</code> <strong>method</strong>: this is the key mechanism. Both callbacks are <code>required</code>. The compiler won't let you call <code>when()</code> without handling both cases. You can't forget the error path. You can't forget the success path. The object itself decides which branch runs — not an if/else in the calling code.</p>
<pre><code class="language-dart">// Repository returning AppResult
Future&lt;AppResult&lt;User&gt;&gt; login(String email, String password) async {
  try {
    final user = await _api.login(email, password);
    return AppSuccess(user);
  } on UnauthorizedException {
    return AppFailureResult(AppFailure.unauthorized());
  } on NetworkException {
    return AppFailureResult(AppFailure.network());
  } catch (e) {
    return AppFailureResult(AppFailure.unknown(e.toString()));
  }
}
</code></pre>
<h3 id="heading-consuming-results-with-when">Consuming Results with <code>when()</code></h3>
<pre><code class="language-dart">final result = await _repository.login(email, password);

result.when(
  success: (user) =&gt; emit(AuthState.authenticated(user)),
  failure: (error) =&gt; emit(AuthState.error(error.message)),
);
</code></pre>
<p>You can also use it to return values:</p>
<pre><code class="language-dart">// Returning a Widget
final widget = result.when(
  success: (user) =&gt; UserProfileCard(user: user),
  failure: (error) =&gt; ErrorView(message: error.message),
);

// Returning a String
final message = result.when(
  success: (data) =&gt; 'Welcome back, ${data.name}',
  failure: (error) =&gt; 'Something went wrong: ${error.message}',
);
</code></pre>
<p>The return type <code>R</code> is inferred — whatever both callbacks return, <code>when()</code> returns. If they return a <code>Widget</code>, you get a <code>Widget</code>. If they return a <code>String</code>, you get a <code>String</code>.</p>
<h3 id="heading-why-this-is-better">Why This is Better</h3>
<table>
<thead>
<tr>
<th></th>
<th>Exceptions</th>
<th>AppResult</th>
</tr>
</thead>
<tbody><tr>
<td>Failure visible in signature</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Compiler enforces handling</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Both paths required at call site</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Type safe across all layers</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Readable and self-documenting</td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody></table>
<h2 id="heading-part-3-extending-to-the-monad-pattern">Part 3: Extending to the Monad Pattern</h2>
<h3 id="heading-what-makes-something-a-monad">What Makes Something a Monad?</h3>
<p>A monad is a pattern from functional programming. In practical terms, a type is monadic when it satisfies three things:</p>
<p><strong>Wrap</strong> — you can put a value into the context.</p>
<pre><code class="language-dart">AppSuccess(user) // wrapping a User into AppResult
</code></pre>
<p><strong>Transform (map)</strong> — you can apply a function to the wrapped value without manually unwrapping it. If the result is a failure, the transformation is skipped and the failure propagates.</p>
<p><strong>Chain (flatMap)</strong> — you can sequence multiple operations that each return the same wrapper type, without nesting. The first failure short-circuits the entire chain.</p>
<p><code>AppResult</code> as defined above satisfies the first rule and the <em>spirit</em> of the second through <code>when()</code>. But without <code>map</code> and <code>flatMap</code>, it's not mechanically monadic. Let's fix that.</p>
<h3 id="heading-adding-map-and-flatmap">Adding <code>map</code> and <code>flatMap</code></h3>
<pre><code class="language-dart">sealed class AppResult&lt;T&gt; {
  const AppResult();

  /// Transform the success value, propagate failure untouched
  AppResult&lt;R&gt; map&lt;R&gt;(R Function(T value) transform) {
    return when(
      success: (value) =&gt; AppSuccess(transform(value)),
      failure: (error) =&gt; AppFailureResult(error),
    );
  }

  /// Chain an operation that itself returns an AppResult
  AppResult&lt;R&gt; flatMap&lt;R&gt;(AppResult&lt;R&gt; Function(T value) transform) {
    return when(
      success: (value) =&gt; transform(value),
      failure: (error) =&gt; AppFailureResult(error),
    );
  }

  R when&lt;R&gt;({
    required R Function(T value) success,
    required R Function(AppFailure failure) failure,
  });
}
</code></pre>
<p><code>map</code> transforms the success value using a regular function. If the result is already a failure, <code>map</code> skips the transformation entirely and passes the failure through unchanged. This is called "failure propagation" — errors flow through the chain automatically.</p>
<p><code>flatMap</code> chains an operation that itself returns an <code>AppResult</code>. This is what allows sequencing — when each step in a process can independently succeed or fail, <code>flatMap</code> connects them so the first failure stops the chain.</p>
<h3 id="heading-chaining-operations">Chaining Operations</h3>
<p>Without monadic chaining, sequential operations that can each fail look like this:</p>
<pre><code class="language-dart">final loginResult = await login(email, password);

loginResult.when(
  success: (user) async {
    final profileResult = await getProfile(user.id);
    profileResult.when(
      success: (profile) async {
        final settingsResult = await loadSettings(profile.settingsId);
        settingsResult.when(
          success: (settings) =&gt; emit(AppState.ready(settings)),
          failure: (error) =&gt; emit(AppState.error(error)),
        );
      },
      failure: (error) =&gt; emit(AppState.error(error)),
    );
  },
  failure: (error) =&gt; emit(AppState.error(error)),
);
</code></pre>
<p>Deeply nested, repetitive error handling on every single step. With <code>flatMap</code>:</p>
<pre><code class="language-dart">final result = (await login(email, password))
    .flatMap((user) =&gt; getProfile(user.id))
    .flatMap((profile) =&gt; loadSettings(profile.settingsId))
    .map((settings) =&gt; settings.theme);

result.when(
  success: (theme) =&gt; emit(AppState.ready(theme)),
  failure: (error) =&gt; emit(AppState.error(error)),
);
</code></pre>
<p>Each step only runs if the previous one succeeded. The first failure short-circuits the entire chain. Error handling happens once at the end, not at every step. This is the full power of the monad pattern applied to real application code.</p>
<h2 id="heading-part-4-either-with-dartz">Part 4: Either with dartz</h2>
<h3 id="heading-what-is-either">What is Either?</h3>
<p><code>Either&lt;L, R&gt;</code> is a type from functional programming that represents one of two possible values — a <code>Left</code> or a <code>Right</code>. By convention:</p>
<ul>
<li><p><code>Left</code> — the failure case</p>
</li>
<li><p><code>Right</code> — the success case</p>
</li>
</ul>
<p>The <code>dartz</code> package brings this and many other functional programming primitives to Dart. Add it to your project:</p>
<pre><code class="language-yaml">dependencies:
  dartz: ^0.10.1
</code></pre>
<p>In the codebase we are building from, <code>Either</code> is used with a type alias that makes the intent explicit:</p>
<pre><code class="language-dart">import 'package:dartz/dartz.dart';

typedef API&lt;T&gt; = Either&lt;T, iException&gt;;
</code></pre>
<p>Note the convention here: <code>Left</code> holds the success value <code>T</code>, and <code>Right</code> holds the failure <code>iException</code>. This is intentionally flipped from the functional programming norm. Both conventions exist in real codebases — what matters is that you're consistent.</p>
<h3 id="heading-using-either-in-practice">Using Either in Practice</h3>
<p>Creating Either values:</p>
<pre><code class="language-dart">// Success — Left holds the data
Either&lt;User, iException&gt; result = Left(user);

// Failure — Right holds the exception
Either&lt;User, iException&gt; result = Right(iException.internet(message: 'No connection'));
</code></pre>
<p>Checking which side you're on:</p>
<pre><code class="language-dart">if (result.isLeft()) {
  final user = result.fold((user) =&gt; user, (_) =&gt; null);
}
</code></pre>
<h3 id="heading-bridging-records-and-either">Bridging Records and Either</h3>
<p>The real power of the <code>API</code> typedef comes from <code>ApiRes</code> — a utility class that converts between the record-based world of your data layer and the Either-based world of your domain layer:</p>
<pre><code class="language-dart">class ApiRes {
  static Future&lt;API&lt;T&gt;&gt; deserialize&lt;T&gt;(ApiResult&lt;T, iException&gt; res) async {
    return (res.data != null)
        ? Left(res.data as T)
        : Right(res.exception!);
  }

  static Future&lt;API&gt; deserializeDynamic(
    ApiResult&lt;dynamic, iException&gt; res,
  ) async {
    return (res.data != null) ? Left(res.data) : Right(res.exception!);
  }
}
</code></pre>
<p><code>ApiResult&lt;T, iException&gt;</code> is your record type from the data layer — a Dio response wrapped with nullable fields. <code>ApiRes.deserialize</code> takes that record and converts it into a proper <code>Either</code>, ready to be used in the domain layer.</p>
<p>In practice, a repository method looks like this:</p>
<pre><code class="language-dart">Future&lt;API&lt;User&gt;&gt; getUser(String id) async {
  // Data layer returns a record
  final res = await _dataSource.fetchUser(id);

  // Convert to Either at the boundary
  return ApiRes.deserialize&lt;User&gt;(res);
}
</code></pre>
<p>The boundary between layers is the conversion point. Inside the data layer, you work with records. At the boundary, you convert. In the domain layer, you work with Either. Each layer has the type that suits it best.</p>
<h3 id="heading-folding-an-either">Folding an Either</h3>
<p><code>dartz</code> provides a <code>fold</code> method on Either that works similarly to <code>when()</code> on <code>AppResult</code>:</p>
<pre><code class="language-dart">final result = await repository.getUser(id);

result.fold(
  (user) =&gt; emit(UserState.loaded(user)),       // Left — success
  (exception) =&gt; emit(UserState.error(exception.message)), // Right — failure
);
</code></pre>
<p><code>dartz</code> also gives you monadic operations out of the box:</p>
<pre><code class="language-dart">// map — transform the Left value
final nameResult = result.map((user) =&gt; user.name);

// flatMap / bind — chain Either-returning operations
final profileResult = result.flatMap(
  (user) =&gt; getProfile(user.id),
);
</code></pre>
<p>The full functional toolkit, ready to use without building it yourself.</p>
<h2 id="heading-part-5-typed-exceptions-with-freezed">Part 5: Typed Exceptions with Freezed</h2>
<h3 id="heading-why-freezed-for-exceptions">Why Freezed for Exceptions?</h3>
<p>Standard Dart exceptions carry almost no useful information:</p>
<pre><code class="language-dart">throw Exception('Something went wrong');
// At the catch site: what went wrong? what type? what code? who knows.
</code></pre>
<p>Even custom exception classes require significant boilerplate to implement properly — <code>==</code>, <code>hashCode</code>, <code>toString</code>, immutability, copyWith. Freezed generates all of that automatically, and adds exhaustive pattern matching on top.</p>
<p>Add the required packages:</p>
<pre><code class="language-yaml">dependencies:
  freezed_annotation: ^2.4.1

dev_dependencies:
  freezed: ^2.4.5
  build_runner: ^2.4.6
</code></pre>
<h3 id="heading-building-iexception">Building iException</h3>
<pre><code class="language-dart">import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'exception.freezed.dart';

@freezed
class iException with _$iException {
  const factory iException.internet({
    required String message,
    int? code,
  }) = InternetException;

  const factory iException.mapper({
    required String message,
    int? code,
  }) = MapperException;

  const factory iException.validation({
    required String message,
    int? code,
  }) = ValidationException;

  const factory iException.unauthorized({
    required String message,
    int? code,
  }) = UnauthorizedException;

  const factory iException.unknown({
    required String message,
    int? code,
  }) = UnknownException;

  const iException._();
}
</code></pre>
<p>Run code generation:</p>
<pre><code class="language-bash">flutter pub run build_runner build --delete-conflicting-outputs
</code></pre>
<p>What Freezed generates from this:</p>
<pre><code class="language-plaintext">iException (sealed base)
├── InternetException    — network failures, no connectivity
├── MapperException      — JSON parsing and deserialization failures
├── ValidationException  — input validation failures
├── UnauthorizedException — auth failures, expired tokens
└── UnknownException     — catch-all for unexpected errors
</code></pre>
<p>Each subclass is fully immutable, has <code>==</code> and <code>hashCode</code> based on its fields, and a proper <code>toString</code>. Creating exceptions is clean and explicit:</p>
<pre><code class="language-dart">iException.internet(message: 'No internet connection')
iException.unauthorized(message: 'Session expired', code: 401)
iException.validation(message: 'Email format is invalid')
iException.mapper(message: 'Failed to parse UserResponse', code: 500)
iException.unknown(message: e.toString())
</code></pre>
<p>The private constructor <code>const iException._()</code> is a Freezed requirement when you add any instance method or getter to the base class — it allows Freezed's generated subclasses to call <code>super._()</code> without exposing a public constructor on the base.</p>
<h3 id="heading-pattern-matching-on-exception-types">Pattern Matching on Exception Types</h3>
<p>Because <code>iException</code> is a Freezed sealed class, you get <code>when</code>, <code>maybeWhen</code>, <code>map</code>, and <code>maybeMap</code> for free from code generation:</p>
<pre><code class="language-dart">exception.when(
  internet: (message, code) =&gt; 'No internet: $message',
  mapper: (message, code) =&gt; 'Parse error: $message',
  validation: (message, code) =&gt; 'Invalid input: $message',
  unauthorized: (message, code) =&gt; 'Unauthorised — please log in again',
  unknown: (message, code) =&gt; 'Unexpected error: $message',
);
</code></pre>
<p>Every case is required. The compiler rejects incomplete matches. You can't accidentally handle only some exception types and silently miss others.</p>
<p>For cases where you only care about specific types:</p>
<pre><code class="language-dart">exception.maybeWhen(
  unauthorized: (message, code) =&gt; _redirectToLogin(),
  orElse: () =&gt; _showGenericError(exception),
);
</code></pre>
<h3 id="heading-a-cleaner-base-getter-pattern">A Cleaner Base Getter Pattern</h3>
<p>One thing worth improving in the base <code>iException</code> is providing a safe <code>message</code> getter that works across all subtypes without throwing <code>UnimplementedError</code>:</p>
<pre><code class="language-dart">const iException._();

String get displayMessage =&gt; when(
  internet: (message, _) =&gt; message,
  mapper: (message, _) =&gt; message,
  validation: (message, _) =&gt; message,
  unauthorized: (message, _) =&gt; message,
  unknown: (message, _) =&gt; message,
);
</code></pre>
<p>Now any code holding an <code>iException</code> — regardless of which subtype — can call <code>.displayMessage</code> safely:</p>
<pre><code class="language-dart">// In a ViewModel or BLoC — no need to pattern match just for the message
emit(ErrorState(message: exception.displayMessage));
</code></pre>
<p>This is significantly cleaner than a base getter that throws <code>UnimplementedError</code> at runtime.</p>
<h2 id="heading-part-6-putting-it-all-together">Part 6: Putting It All Together</h2>
<h3 id="heading-the-full-architecture">The Full Architecture</h3>
<p>Here's how all four patterns connect across a real clean architecture Flutter application:</p>
<pre><code class="language-plaintext">Data Layer
  Dio/HTTP call returns raw response
    └── Wrapped in ApiResult&lt;T, iException&gt; (record type)
          │
          ▼
Repository Layer
  ApiRes.deserialize() converts record → Either&lt;T, iException&gt;
    └── Returns API&lt;T&gt; = Either&lt;T, iException&gt;
          │
          ▼
Domain / Use Case Layer
  AppResult&lt;T&gt; is the standard return type
    └── Sealed class with AppSuccess and AppFailureResult
          │
          ▼
Presentation Layer
  result.when() handles both paths
    └── exception.when() handles all failure types
</code></pre>
<p>Each layer has the result type that suits its responsibility. Conversion happens at the boundaries. The presentation layer always deals with <code>AppResult&lt;T&gt;</code> — it doesn't need to know about Either or records.</p>
<h3 id="heading-repository-layer">Repository Layer</h3>
<pre><code class="language-dart">class AuthRepository {
  final AuthDataSource _dataSource;

  AuthRepository(this._dataSource);

  Future&lt;AppResult&lt;User&gt;&gt; login(String email, String password) async {
    // Data source returns a record
    final res = await _dataSource.login(email, password);

    // Convert to Either at the data/domain boundary
    final either = await ApiRes.deserialize&lt;User&gt;(res);

    // Convert Either to AppResult for the domain layer
    return either.fold(
      (user) =&gt; AppSuccess(user),
      (exception) =&gt; AppFailureResult(exception),
    );
  }

  Future&lt;AppResult&lt;List&lt;User&gt;&gt;&gt; getUsers() async {
    final res = await _dataSource.fetchUsers();
    final either = await ApiRes.deserialize&lt;List&lt;User&gt;&gt;(res);

    return either.fold(
      (users) =&gt; AppSuccess(users),
      (exception) =&gt; AppFailureResult(exception),
    );
  }
}
</code></pre>
<h3 id="heading-domain-layer">Domain Layer</h3>
<pre><code class="language-dart">class LoginUseCase {
  final AuthRepository _repository;

  LoginUseCase(this._repository);

  Future&lt;AppResult&lt;User&gt;&gt; execute(String email, String password) async {
    if (email.isEmpty || password.isEmpty) {
      return AppFailureResult(
        iException.validation(message: 'Email and password are required'),
      );
    }

    return _repository.login(email, password);
  }
}
</code></pre>
<p>The use case adds its own validation layer — returning a <code>ValidationException</code> before even hitting the repository. All failures flow through the same <code>AppResult&lt;T&gt;</code> type regardless of where they originated.</p>
<h3 id="heading-presentation-layer">Presentation Layer</h3>
<pre><code class="language-dart">class AuthViewModel extends ChangeNotifier {
  final LoginUseCase _loginUseCase;

  AuthViewModel(this._loginUseCase);

  AuthState _state = const AuthState.idle();
  AuthState get state =&gt; _state;

  Future&lt;void&gt; login(String email, String password) async {
    _state = const AuthState.loading();
    notifyListeners();

    final result = await _loginUseCase.execute(email, password);

    result.when(
      success: (user) {
        _state = AuthState.authenticated(user);
      },
      failure: (exception) {
        // Pattern match on the exception type for specific handling
        final message = exception.when(
          internet: (msg, _) =&gt; 'No internet connection. Please check your network.',
          unauthorized: (msg, _) =&gt; 'Your session has expired. Please log in again.',
          validation: (msg, _) =&gt; msg,
          mapper: (msg, _) =&gt; 'Something went wrong. Please try again.',
          unknown: (msg, _) =&gt; 'An unexpected error occurred.',
        );

        _state = AuthState.error(message);
      },
    );

    notifyListeners();
  }
}
</code></pre>
<p>Two levels of exhaustive pattern matching — one for the result, one for the exception type. Every possible failure has a specific, user-friendly message. The compiler guarantees nothing is missed.</p>
<p>And using the monadic chain from Part 3 for a multi-step flow:</p>
<pre><code class="language-java">Future&lt;void&gt; loadDashboard(String userId) async {
  _state = const DashboardState.loading();
  notifyListeners();

  final result = (await _userRepo.getUser(userId))
      .flatMap((user) =&gt; _profileRepo.getProfile(user.profileId))
      .flatMap((profile) =&gt; _settingsRepo.loadSettings(profile.settingsId))
      .map((settings) =&gt; DashboardData(settings: settings));

  result.when(
    success: (data) =&gt; _state = DashboardState.loaded(data),
    failure: (exception) =&gt; _state = DashboardState.error(
      exception.displayMessage,
    ),
  );

  notifyListeners();
}
</code></pre>
<p>Three sequential async operations, each of which can independently fail, handled in a clean chain with a single error handler at the end. This is what production-grade error handling looks like.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Error handling is one of those things that every codebase has, but few codebases have done well. The default in Dart , throwing and catching exceptions, is convenient for small projects and becomes a liability at scale. Failures become invisible, type information is lost across layers, and the compiler can't help you when something goes wrong.</p>
<p>The patterns in this article change that entirely.</p>
<p>Records give you lightweight result containers with zero boilerplate — perfect for layer-specific result types and domain-specific responses. Sealed Result types bring compiler enforcement — both paths are required, no failure can be silently ignored. The Monad pattern adds the ability to chain sequential operations cleanly, with automatic failure propagation through the chain. Either with <code>dartz</code> brings the full functional toolkit and a clean boundary type between your data and domain layers. And Freezed exceptions give your failure states structure, immutability, and exhaustive pattern matching, so every error type is handled explicitly and nothing slips through.</p>
<p>None of these patterns are complicated once you understand the problem they solve. And the problem they solve – invisible, unenforceable, type-unsafe error handling – is one of the most common sources of production bugs in Flutter applications.</p>
<p>The next step is taking one of these patterns into a real project. Using these will totally transform the error handling story and processes of your entire code base.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Command Line Interface (CLI) Development with Dart: From Zero to a Fully Published Developer Tool ]]>
                </title>
                <description>
                    <![CDATA[ Most developers spend a significant portion of their day in the terminal. They run flutter build, push with git, manage packages with dart pub, and orchestrate pipelines from the command line. Every o ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-command-line-interface-cli-development-with-dart-from-zero-to-a-fully-published-developer-tool/</link>
                <guid isPermaLink="false">69fe3149f239332df4fdfd46</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cli ]]>
                    </category>
                
                    <category>
                        <![CDATA[ command line ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Fri, 08 May 2026 18:54:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a4c564c2-f5f3-4824-b4e7-d103b5fc488e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most developers spend a significant portion of their day in the terminal. They run <code>flutter build</code>, push with <code>git</code>, manage packages with <code>dart pub</code>, and orchestrate pipelines from the command line. Every one of those tools is a CLI, or command line interface: a program that lives in the terminal and responds to text commands.</p>
<p>Yet most developers have never built one.</p>
<p>That's a missed opportunity. CLI tools are one of the most practical things a developer can ship. They automate repetitive workflows, standardise processes across teams, and, when published, become tangible artifacts that the developer community can discover, install, and use.</p>
<p>In this handbook, you'll go from zero to building a fully distributed Dart CLI tool. We'll start with the fundamentals – how CLIs work, how Dart receives and processes terminal input, and the core syntax you need to know. Then we'll build three progressively complex CLIs, starting with the basics and finishing with a real-world API request runner. Finally, we will cover every distribution path available, from <code>pub.dev</code> to compiled binaries, Homebrew taps, Docker, and local team activation.</p>
<p>By the end of the guide, you'll understand both how to build a CLI tool in Dart as well as how to ship it so other developers can actually use it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-is-a-cli-and-why-should-you-build-one">What is a CLI and Why Should You Build One?</a></p>
</li>
<li><p><a href="#heading-cli-syntax-anatomy">CLI Syntax Anatomy</a></p>
</li>
<li><p><a href="#heading-how-dart-receives-terminal-input">How Dart Receives Terminal Input</a></p>
</li>
<li><p><a href="#heading-core-cli-concepts-in-dart">Core CLI Concepts in Dart</a></p>
<ul>
<li><p><a href="#heading-stdout-stderr-and-stdin">stdout, stderr, and stdin</a></p>
</li>
<li><p><a href="#heading-exit-codes">Exit Codes</a></p>
</li>
<li><p><a href="#heading-environment-variables">Environment Variables</a></p>
</li>
<li><p><a href="#heading-file-and-directory-operations">File and Directory Operations</a></p>
</li>
<li><p><a href="#heading-running-external-processes">Running External Processes</a></p>
</li>
<li><p><a href="#heading-platform-detection">Platform Detection</a></p>
</li>
<li><p><a href="#heading-async-in-cli">Async in CLI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-setting-up-your-dart-cli-project">Setting Up Your Dart CLI Project</a></p>
</li>
<li><p><a href="#heading-cli-1-hello-cli-the-fundamentals">CLI 1 — Hello CLI: The Fundamentals</a></p>
</li>
<li><p><a href="#heading-cli-2-darttodo-a-terminal-task-manager">CLI 2 — dart_todo: A Terminal Task Manager</a></p>
<ul>
<li><p><a href="#heading-introducing-the-args-package">Introducing the args Package</a></p>
</li>
<li><p><a href="#heading-building-darttodo">Building dart_todo</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-cli-3-darthttp-a-lightweight-api-request-runner">CLI 3 — dart_http: A Lightweight API Request Runner</a></p>
<ul>
<li><a href="#heading-building-darthttp">Building dart_http</a></li>
</ul>
</li>
<li><p><a href="#heading-adding-color-and-polish-to-your-cli">Adding Color and Polish to Your CLI</a></p>
</li>
<li><p><a href="#heading-testing-your-cli-tool">Testing Your CLI Tool</a></p>
</li>
<li><p><a href="#heading-deploying-and-distributing-your-cli">Deploying and Distributing Your CLI</a></p>
<ul>
<li><p><a href="#heading-mode-1-pubdev-public-package-distribution">Mode 1: pub.dev — Public Package Distribution</a></p>
</li>
<li><p><a href="#heading-mode-2-local-path-activation">Mode 2: Local Path Activation</a></p>
</li>
<li><p><a href="#heading-mode-3-compiled-binary-via-github-releases">Mode 3: Compiled Binary via GitHub Releases</a></p>
</li>
<li><p><a href="#heading-mode-4-homebrew-tap">Mode 4: Homebrew Tap</a></p>
</li>
<li><p><a href="#heading-mode-5-docker">Mode 5: Docker</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-choosing-the-right-distribution-mode">Choosing the Right Distribution Mode</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before starting, you should have:</p>
<ul>
<li><p>Dart SDK installed (<code>dart --version</code> should work in your terminal)</p>
</li>
<li><p>Basic familiarity with Dart syntax</p>
</li>
<li><p>Comfort with the terminal and running commands</p>
</li>
<li><p>A pub.dev account (for the publishing section)</p>
</li>
<li><p>A GitHub account (for the binary distribution section)</p>
</li>
</ul>
<h2 id="heading-what-is-a-cli-and-why-should-you-build-one">What is a CLI and Why Should You Build One?</h2>
<p>A CLI (or <strong>Command Line Interface</strong>) is a program you interact with entirely through text commands in a terminal, rather than through buttons and screens in a graphical interface.</p>
<p>Many of the tools you likely already rely on as a developer are CLI tools:</p>
<pre><code class="language-yaml">flutter build apk
git commit -m "fix: auth flow"
dart pub get
npm install
</code></pre>
<p>Flutter, Git, Dart, npm – all CLIs. You are already a CLI user every single day. This article is about becoming a CLI builder.</p>
<p>There are three strong reasons to build CLI tools as a developer:</p>
<ol>
<li><p><strong>Automating repetitive work:</strong> Anything you type more than twice a week is a candidate for automation. Generating boilerplate folder structures, running sequences of commands, scaffolding files, checking environments before a build a CLI turns a seven-step manual process into a single command.</p>
</li>
<li><p><strong>Standardising team workflows:</strong> Instead of a README that says "run these commands in this order," you ship one command that does all of it – consistently, every time, with no room for human error or a missed step.</p>
</li>
<li><p><strong>Building and publishing tooling.</strong> A published Dart CLI package is a tangible artifact. It shows up on pub.dev, gets installed and used by other developers, and communicates real engineering depth in a way that a portfolio or resume cannot.</p>
</li>
</ol>
<h2 id="heading-cli-syntax-anatomy">CLI Syntax Anatomy</h2>
<p>Before writing a single line of code, it helps to understand the structure of a CLI command. Every command follows a consistent pattern:</p>
<pre><code class="language-bash">tool [subcommand] [arguments] [options/flags]
</code></pre>
<p>Breaking down a real example:</p>
<pre><code class="language-bash">flutter build apk --release --obfuscate
│       │     │   │
tool    sub   arg  flags
</code></pre>
<ul>
<li><p><strong>Tool</strong> — the program itself (<code>flutter</code>, <code>dart</code>, <code>git</code>)</p>
</li>
<li><p><strong>Subcommand</strong> — the action being performed (<code>build</code>, <code>run</code>, <code>pub</code>)</p>
</li>
<li><p><strong>Arguments</strong> — what the action operates on (<code>apk</code>, <code>main.dart</code>, a filename)</p>
</li>
<li><p><strong>Flags and Options</strong> — modifiers that change behaviour</p>
</li>
</ul>
<p>There are two types of options:</p>
<pre><code class="language-plaintext">--release              # Boolean flag — either present or absent

--output=build/app     # Key-value option — name and a value
-v                     # Short flag — single hyphen, single character
</code></pre>
<p>This is the anatomy your CLIs will follow. Understanding it before writing any code means you will design your commands intentionally rather than stumbling into structure by accident.</p>
<h2 id="heading-how-dart-receives-terminal-input">How Dart Receives Terminal Input</h2>
<p>In Dart, everything the user types after your tool name is passed into your program through the <code>main</code> function:</p>
<pre><code class="language-dart">void main(List&lt;String&gt; args) {
  print(args);
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/mytool.dart hello world --name=Seyi
# [hello, world, --name=Seyi]
</code></pre>
<p>That <code>List&lt;String&gt; args</code> is just a list of strings. Each word or flag the user typed becomes an element in that list. Everything else you build on top of a CLI subcommands, flags, validation — is ultimately just processing this list.</p>
<h2 id="heading-core-cli-concepts-in-dart">Core CLI Concepts in Dart</h2>
<p>Before building anything, there's a set of foundational concepts that every CLI developer needs to understand. These are the building blocks that everything else sits on top of.</p>
<h3 id="heading-stdout-stderr-and-stdin">stdout, stderr, and stdin</h3>
<p>Most developers use <code>print()</code> for all output when they start building CLIs. That works for learning but it's incorrect in production.</p>
<p>There are two separate output streams in a terminal program:</p>
<ul>
<li><p><code>stdout</code> — regular output, meant for the user</p>
</li>
<li><p><code>stderr</code> — error output, meant for diagnostic messages and failures</p>
</li>
</ul>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Error: no arguments provided');
    exit(1);
  }

  stdout.writeln('Processing: ${args[0]}');
}
</code></pre>
<p>Keeping these separate matters because users can redirect stdout to a file without errors polluting it:</p>
<pre><code class="language-bash">dart run bin/tool.dart &gt; output.txt
# Errors still appear in the terminal
# Normal output goes cleanly to the file
</code></pre>
<p>Tools like <code>git</code>, <code>flutter</code>, and <code>curl</code> all do this correctly. Your CLI should too.</p>
<p><code>stdin</code> is the third stream — reading input from the user interactively at runtime:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  stdout.write('Enter your name: ');
  final name = stdin.readLineSync();

  if (name == null || name.trim().isEmpty) {
    stderr.writeln('Error: no name provided');
    exit(1);
  }

  stdout.writeln('Hello, $name!');
}
</code></pre>
<p><code>stdout.write</code> (without <code>ln</code>) keeps the cursor on the same line so the user types right after the prompt. <code>stdin.readLineSync()</code> blocks until the user presses Enter and returns the typed string, or <code>null</code> if the stream closes unexpectedly. Always handle the null case.</p>
<h3 id="heading-exit-codes">Exit Codes</h3>
<p>Every program returns an exit code when it finishes. This is how the shell – and any script or CI system calling your tool – knows whether it succeeded or failed.</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Error: please provide an argument');
    exit(1); // failure
  }

  stdout.writeln('Done');
  exit(0); // success — also the default if you don't call exit()
}
</code></pre>
<p>The conventions are:</p>
<ul>
<li><p><code>0</code> — success</p>
</li>
<li><p><code>1</code> — general failure</p>
</li>
<li><p><code>2</code> — incorrect usage (wrong arguments, missing flags)</p>
</li>
</ul>
<p>Exit codes are critical when your CLI is called inside shell scripts or GitHub Actions workflows. A non-zero exit code stops a pipeline immediately. That's exactly the behaviour you want from a quality gate or a validation step.</p>
<h3 id="heading-environment-variables">Environment Variables</h3>
<p>Your CLI can read environment variables set in the user's shell:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  final token = Platform.environment['API_TOKEN'];

  if (token == null) {
    stderr.writeln('Error: API_TOKEN environment variable is not set');
    exit(1);
  }

  stdout.writeln('Token found — proceeding...');
}
</code></pre>
<p>Set it in the terminal and run:</p>
<pre><code class="language-bash">export API_TOKEN=mytoken123
dart run bin/tool.dart
# Token found — proceeding...
</code></pre>
<p>This pattern is essential for CLI tools that interact with APIs, cloud services, or CI environments where credentials should never be hardcoded.</p>
<h3 id="heading-file-and-directory-operations">File and Directory Operations</h3>
<p>Many CLI tools read from or write to the file system. Dart's <code>dart:io</code> library covers everything you need:</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: tool &lt;filename&gt;');
    exit(2);
  }

  final file = File(args[0]);

  if (!file.existsSync()) {
    stderr.writeln('Error: "${args[0]}" not found');
    exit(1);
  }

  final contents = file.readAsStringSync();
  stdout.writeln(contents);

  final output = File('output.txt');
  output.writeAsStringSync('Processed:\n$contents');
  stdout.writeln('Written to output.txt');
}
</code></pre>
<p>Working with directories:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  // Where the command was run from
  final cwd = Directory.current.path;
  stdout.writeln('Working directory: $cwd');

  // Create a directory relative to current location
  final dir = Directory('$cwd/generated');

  if (!dir.existsSync()) {
    dir.createSync(recursive: true);
    stdout.writeln('Created: ${dir.path}');
  } else {
    stdout.writeln('Already exists: ${dir.path}');
  }
}
</code></pre>
<p>The <code>recursive: true</code> flag on <code>createSync</code> means it creates all intermediate directories — equivalent to <code>mkdir -p</code> in bash.</p>
<h3 id="heading-running-external-processes">Running External Processes</h3>
<p>One of the most powerful things a CLI can do is call other programs. Your Dart CLI can run <code>git</code>, <code>flutter</code>, <code>dart</code>, or any shell command programmatically:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  // Run a command and wait for it to finish
  final result = await Process.run('dart', ['pub', 'get']);

  stdout.write(result.stdout);

  if (result.exitCode != 0) {
    stderr.write(result.stderr);
    exit(result.exitCode);
  }

  stdout.writeln('Dependencies installed successfully');
}
</code></pre>
<p>For long-running commands where you want output to stream live as it happens:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  final process = await Process.start('flutter', ['build', 'apk']);

  // Pipe output directly to the terminal in real time
  process.stdout.pipe(stdout);
  process.stderr.pipe(stderr);

  final exitCode = await process.exitCode;
  exit(exitCode);
}
</code></pre>
<p><code>Process.run</code> — waits for completion, returns all output at once. Use for short commands.</p>
<p><code>Process.start</code> — streams output live as it arrives. Use for long-running commands where the user needs to see progress.</p>
<h3 id="heading-platform-detection">Platform Detection</h3>
<p>Sometimes your CLI needs to behave differently depending on the operating system it is running on:</p>
<pre><code class="language-dart">import 'dart:io';

void main() {
  if (Platform.isWindows) {
    stdout.writeln('Running on Windows');
  } else if (Platform.isMacOS) {
    stdout.writeln('Running on macOS');
  } else if (Platform.isLinux) {
    stdout.writeln('Running on Linux');
  }

  // Useful for path handling across operating systems
  stdout.writeln(Platform.pathSeparator); // \ on Windows, / elsewhere
  stdout.writeln(Platform.operatingSystem); // 'macos', 'linux', 'windows'
}
</code></pre>
<p>This matters when your CLI creates files, resolves paths, or calls shell commands that differ between operating systems.</p>
<h3 id="heading-async-in-cli">Async in CLI</h3>
<p>Dart CLIs support <code>async/await</code> natively. Any <code>main</code> function can be made async:</p>
<pre><code class="language-dart">import 'dart:io';

void main() async {
  stdout.writeln('Starting...');

  await Future.delayed(const Duration(seconds: 1)); // simulating async work

  stdout.writeln('Done');
}
</code></pre>
<p>Any operation involving file I/O, HTTP requests, or spawning processes will be asynchronous. Get comfortable with async <code>main</code> functions early — you'll use them constantly.</p>
<h2 id="heading-setting-up-your-dart-cli-project">Setting Up Your Dart CLI Project</h2>
<p>Create a new Dart console project:</p>
<pre><code class="language-bash">dart create -t console my_cli_tool
cd my_cli_tool
</code></pre>
<p>This generates a clean structure:</p>
<pre><code class="language-plaintext">my_cli_tool/
  bin/
    my_cli_tool.dart    ← entry point
  lib/                  ← shared library code
  test/                 ← tests
  pubspec.yaml
  README.md
</code></pre>
<p>The <code>bin/</code> directory is where your executable entry point lives. The <code>lib/</code> directory is where you put everything else — commands, utilities, models — that <code>bin/</code> imports and uses.</p>
<p>Open <code>pubspec.yaml</code>. You'll need to add an <code>executables</code> block before publishing:</p>
<pre><code class="language-yaml">name: my_cli_tool
description: A sample CLI tool built with Dart
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  my_cli_tool: my_cli_tool  # executable name: bin file name

dependencies:
  args: ^2.4.2

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>The <code>executables</code> block is what makes <code>dart pub global activate my_cli_tool</code> work. It tells Dart which script in <code>bin/</code> to expose as a runnable command after installation.</p>
<h2 id="heading-cli-1-hello-cli-the-fundamentals">CLI 1 — Hello CLI: The Fundamentals</h2>
<p>This first CLI uses pure Dart — no packages. The goal is to get comfortable with args, subcommands, input validation, and exit codes before introducing any external dependencies.</p>
<p>Replace the contents of <code>bin/my_cli_tool.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

void main(List&lt;String&gt; args) {
  if (args.isEmpty) {
    printHelp();
    exit(0);
  }

  final command = args[0];

  switch (command) {
    case 'greet':
      handleGreet(args.sublist(1));
    case 'time':
      handleTime();
    case 'echo':
      handleEcho(args.sublist(1));
    case 'help':
      printHelp();
    default:
      stderr.writeln('Unknown command: "$command"');
      stderr.writeln('Run "mytool help" to see available commands.');
      exit(1);
  }
}

void handleGreet(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: mytool greet &lt;name&gt;');
    exit(2);
  }

  final name = args[0];
  stdout.writeln('Hello, $name! Welcome to your first Dart CLI.');
}

void handleTime() {
  final now = DateTime.now();
  stdout.writeln(
    'Current time: ${now.hour.toString().padLeft(2, '0')}:'
    '${now.minute.toString().padLeft(2, '0')}:'
    '${now.second.toString().padLeft(2, '0')}',
  );
}

void handleEcho(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: mytool echo &lt;message&gt;');
    exit(2);
  }

  stdout.writeln(args.join(' '));
}

void printHelp() {
  stdout.writeln('''
mytool — a simple Dart CLI

Usage:
  mytool &lt;command&gt; [arguments]

Commands:
  greet &lt;name&gt;      Greet someone by name
  time              Show the current time
  echo &lt;message&gt;    Echo a message back to the terminal
  help              Show this help message

Examples:
  mytool greet Seyi
  mytool echo "Hello from the terminal"
  mytool time
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/my_cli_tool.dart help

dart run bin/my_cli_tool.dart greet Seyi
# Hello, Seyi! Welcome to your first Dart CLI.

dart run bin/my_cli_tool.dart time
# Current time: 14:32:10

dart run bin/my_cli_tool.dart echo "Dart CLIs are powerful"
# Dart CLIs are powerful

dart run bin/my_cli_tool.dart unknown
# Unknown command: "unknown"
# Run "mytool help" to see available commands.
</code></pre>
<p>Three things this CLI demonstrates that are worth internalising:</p>
<ol>
<li><p><strong>Subcommands are just a switch on</strong> <code>args[0]</code><strong>.</strong> The pattern is simple and scalable — add a new <code>case</code> to add a new command.</p>
</li>
<li><p><code>args.sublist(1)</code> <strong>passes remaining args to the handler.</strong> When <code>greet</code> receives <code>['greet', 'Seyi']</code>, it calls <code>handleGreet(['Seyi'])</code> — clean and isolated.</p>
</li>
<li><p><strong>Every error path has a message and a non-zero exit code.</strong> The user always knows what went wrong and what to do next.</p>
</li>
</ol>
<h2 id="heading-cli-2-darttodo-a-terminal-task-manager">CLI 2 — dart_todo: A Terminal Task Manager</h2>
<p>This CLI introduces the <code>args</code> package, JSON file persistence, and structured terminal output. It's meaningfully more complex than CLI 1 and reflects real patterns you will use in production tools.</p>
<h3 id="heading-introducing-the-args-package">Introducing the args Package</h3>
<p>Manually parsing <code>List&lt;String&gt; args</code> works for simple cases, but breaks down quickly when you add flags like <code>--priority=high</code>, boolean options like <code>--done</code>, or commands with multiple optional arguments.</p>
<p>The <code>args</code> package handles all of that cleanly.</p>
<p>Add it to your <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dependencies:
  args: ^2.4.2
</code></pre>
<p>Run:</p>
<pre><code class="language-bash">dart pub get
</code></pre>
<p>The core concept in <code>args</code> is the <code>ArgParser</code>. You define what your CLI accepts, and <code>args</code> handles parsing, validation, and generating help text automatically:</p>
<pre><code class="language-dart">import 'package:args/args.dart';

void main(List&lt;String&gt; arguments) {
  final parser = ArgParser()
    ..addCommand('add')
    ..addCommand('list')
    ..addFlag('help', abbr: 'h', negatable: false);

  final results = parser.parse(arguments);

  if (results['help'] as bool) {
    print(parser.usage);
    return;
  }
}
</code></pre>
<p>For more complex CLIs with subcommands that each have their own flags, use <code>ArgParser</code> per command:</p>
<pre><code class="language-dart">final parser = ArgParser();

final addCommand = ArgParser()
  ..addOption('priority', abbr: 'p', defaultsTo: 'normal');

parser.addCommand('add', addCommand);
</code></pre>
<h3 id="heading-building-darttodo">Building dart_todo</h3>
<p>Create a fresh project:</p>
<pre><code class="language-bash">dart create -t console dart_todo
cd dart_todo
</code></pre>
<p>Update <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">name: dart_todo
description: A terminal task manager built with Dart
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_todo: dart_todo

dependencies:
  args: ^2.4.2

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run <code>dart pub get</code>.</p>
<p>Create the folder structure:</p>
<pre><code class="language-plaintext">dart_todo/
  bin/
    dart_todo.dart
  lib/
    models/
      task.dart
    storage/
      task_storage.dart
    commands/
      add_command.dart
      list_command.dart
      complete_command.dart
      delete_command.dart
      clear_command.dart
  pubspec.yaml
</code></pre>
<h4 id="heading-step-1-the-task-model-libmodelstaskdart">Step 1 — The Task Model (<code>lib/models/task.dart</code>)</h4>
<pre><code class="language-dart">class Task {
  final int id;
  final String title;
  final String priority;
  final bool isComplete;
  final DateTime createdAt;

  Task({
    required this.id,
    required this.title,
    required this.priority,
    this.isComplete = false,
    required this.createdAt,
  });

  Task copyWith({bool? isComplete}) {
    return Task(
      id: id,
      title: title,
      priority: priority,
      isComplete: isComplete ?? this.isComplete,
      createdAt: createdAt,
    );
  }

  Map&lt;String, dynamic&gt; toJson() =&gt; {
        'id': id,
        'title': title,
        'priority': priority,
        'isComplete': isComplete,
        'createdAt': createdAt.toIso8601String(),
      };

  factory Task.fromJson(Map&lt;String, dynamic&gt; json) =&gt; Task(
        id: json['id'] as int,
        title: json['title'] as String,
        priority: json['priority'] as String,
        isComplete: json['isComplete'] as bool,
        createdAt: DateTime.parse(json['createdAt'] as String),
      );
}
</code></pre>
<h4 id="heading-step-2-storage-libstoragetaskstoragedart">Step 2 — Storage (<code>lib/storage/task_storage.dart</code>)</h4>
<p>This class handles reading and writing tasks to a local JSON file so they persist between CLI runs:</p>
<pre><code class="language-dart">import 'dart:convert';
import 'dart:io';

import '../models/task.dart';

class TaskStorage {
  static final _file = File(
    '${Platform.environment['HOME'] ?? Directory.current.path}/.dart_todo.json',
  );

  static List&lt;Task&gt; loadAll() {
    if (!_file.existsSync()) return [];

    try {
      final content = _file.readAsStringSync();
      final List&lt;dynamic&gt; json = jsonDecode(content) as List&lt;dynamic&gt;;
      return json
          .map((e) =&gt; Task.fromJson(e as Map&lt;String, dynamic&gt;))
          .toList();
    } catch (_) {
      return [];
    }
  }

  static void saveAll(List&lt;Task&gt; tasks) {
    final json = jsonEncode(tasks.map((t) =&gt; t.toJson()).toList());
    _file.writeAsStringSync(json);
  }
}
</code></pre>
<p>Tasks are stored in a hidden JSON file in the user's home directory — a common pattern for CLI tools that need lightweight local persistence.</p>
<h4 id="heading-step-3-commands">Step 3 — Commands</h4>
<p><code>lib/commands/add_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../models/task.dart';
import '../storage/task_storage.dart';

void runAdd(List&lt;String&gt; args, String priority) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo add &lt;title&gt; [--priority=high|normal|low]');
    exit(2);
  }

  final title = args.join(' ');
  final tasks = TaskStorage.loadAll();

  final newTask = Task(
    id: tasks.isEmpty ? 1 : tasks.last.id + 1,
    title: title,
    priority: priority,
    createdAt: DateTime.now(),
  );

  tasks.add(newTask);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Added task #\({newTask.id}: "\)title" [$priority]');
}
</code></pre>
<p><code>lib/commands/list_command.dart</code>:</p>
<pre><code class="language-cpp">import 'dart:io';

import '../storage/task_storage.dart';

void runList() {
  final tasks = TaskStorage.loadAll();

  if (tasks.isEmpty) {
    stdout.writeln('No tasks yet. Add one with: dart_todo add &lt;title&gt;');
    return;
  }

  stdout.writeln('');
  stdout.writeln('  ID   Status      Priority   Title');
  stdout.writeln('  ───  ──────────  ─────────  ────────────────────────');

  for (final task in tasks) {
    final status = task.isComplete ? 'done  ' : 'pending';
    final id = task.id.toString().padRight(4);
    final priority = task.priority.padRight(9);
    stdout.writeln('  \(id \)status  \(priority  \){task.title}');
  }

  stdout.writeln('');
}
</code></pre>
<p><code>lib/commands/complete_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runComplete(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo complete &lt;id&gt;');
    exit(2);
  }

  final id = int.tryParse(args[0]);
  if (id == null) {
    stderr.writeln('Error: "${args[0]}" is not a valid task ID');
    exit(1);
  }

  final tasks = TaskStorage.loadAll();
  final index = tasks.indexWhere((t) =&gt; t.id == id);

  if (index == -1) {
    stderr.writeln('Error: No task found with ID $id');
    exit(1);
  }

  if (tasks[index].isComplete) {
    stdout.writeln('Task #$id is already complete.');
    return;
  }

  tasks[index] = tasks[index].copyWith(isComplete: true);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Task #\(id marked as complete: "\){tasks[index].title}"');
}
</code></pre>
<p><code>lib/commands/delete_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runDelete(List&lt;String&gt; args) {
  if (args.isEmpty) {
    stderr.writeln('Usage: dart_todo delete &lt;id&gt;');
    exit(2);
  }

  final id = int.tryParse(args[0]);
  if (id == null) {
    stderr.writeln('Error: "${args[0]}" is not a valid task ID');
    exit(1);
  }

  final tasks = TaskStorage.loadAll();
  final index = tasks.indexWhere((t) =&gt; t.id == id);

  if (index == -1) {
    stderr.writeln('Error: No task found with ID $id');
    exit(1);
  }

  final title = tasks[index].title;
  tasks.removeAt(index);
  TaskStorage.saveAll(tasks);

  stdout.writeln('Deleted task #\(id: "\)title"');
}
</code></pre>
<p><code>lib/commands/clear_command.dart</code>:</p>
<pre><code class="language-dart">import 'dart:io';

import '../storage/task_storage.dart';

void runClear() {
  stdout.write('Are you sure you want to delete all tasks? (y/N): ');
  final input = stdin.readLineSync()?.trim().toLowerCase();

  if (input != 'y') {
    stdout.writeln('Cancelled.');
    return;
  }

  TaskStorage.saveAll([]);
  stdout.writeln('All tasks cleared.');
}
</code></pre>
<h4 id="heading-step-4-entry-point-bindarttododart">Step 4 — Entry Point (<code>bin/dart_todo.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:args/args.dart';

import '../lib/commands/add_command.dart';
import '../lib/commands/clear_command.dart';
import '../lib/commands/complete_command.dart';
import '../lib/commands/delete_command.dart';
import '../lib/commands/list_command.dart';

void main(List&lt;String&gt; arguments) {
  final parser = ArgParser();

  // Add subcommand parsers
  final addParser = ArgParser()
    ..addOption(
      'priority',
      abbr: 'p',
      defaultsTo: 'normal',
      allowed: ['high', 'normal', 'low'],
      help: 'Task priority level',
    );

  parser
    ..addCommand('add', addParser)
    ..addCommand('list')
    ..addCommand('complete')
    ..addCommand('delete')
    ..addCommand('clear')
    ..addFlag('help', abbr: 'h', negatable: false, help: 'Show help');

  ArgResults results;

  try {
    results = parser.parse(arguments);
  } catch (e) {
    stderr.writeln('Error: $e');
    stderr.writeln(parser.usage);
    exit(2);
  }

  if (results['help'] as bool || results.command == null) {
    printHelp(parser);
    exit(0);
  }

  final command = results.command!;

  switch (command.name) {
    case 'add':
      runAdd(command.rest, command['priority'] as String);
    case 'list':
      runList();
    case 'complete':
      runComplete(command.rest);
    case 'delete':
      runDelete(command.rest);
    case 'clear':
      runClear();
    default:
      stderr.writeln('Unknown command: "${command.name}"');
      exit(1);
  }
}

void printHelp(ArgParser parser) {
  stdout.writeln('''
dart_todo — a terminal task manager

Usage:
  dart_todo &lt;command&gt; [arguments]

Commands:
  add &lt;title&gt;        Add a new task
    -p, --priority   Priority: high, normal, low (default: normal)
  list               List all tasks
  complete &lt;id&gt;      Mark a task as complete
  delete &lt;id&gt;        Delete a task
  clear              Delete all tasks

Examples:
  dart_todo add "Write the CLI article" --priority=high
  dart_todo list
  dart_todo complete 1
  dart_todo delete 2
  dart_todo clear
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/dart_todo.dart add "Write the CLI article" --priority=high
# Added task #1: "Write the CLI article" [high]

dart run bin/dart_todo.dart add "Review PR comments"
# Added task #2: "Review PR comments" [normal]

dart run bin/dart_todo.dart list
#   ID   Status      Priority   Title
#   ───  ──────────  ─────────  ────────────────────────
#   1    ⬜ pending  high       Write the CLI article
#   2    ⬜ pending  normal     Review PR comments

dart run bin/dart_todo.dart complete 1
# Task #1 marked as complete: "Write the CLI article"

dart run bin/dart_todo.dart delete 2
# Deleted task #2: "Review PR comments"
</code></pre>
<p><code>dart_todo</code> demonstrates the patterns that form the backbone of almost every real CLI tool — argument parsing with <code>args</code>, JSON persistence, interactive prompts, structured output, and clean error handling across every command.</p>
<h2 id="heading-cli-3-darthttp-a-lightweight-api-request-runner">CLI 3 — dart_http: A Lightweight API Request Runner</h2>
<p>This is the most complex CLI in this article – and the most immediately useful. <code>dart_http</code> lets developers make HTTP requests directly from the terminal, with pretty-printed JSON responses, response metadata, header support, and the ability to save responses to a file.</p>
<pre><code class="language-bash">dart_http get https://jsonplaceholder.typicode.com/users/1
dart_http post https://jsonplaceholder.typicode.com/posts --body='{"title":"Hello"}'
dart_http get https://jsonplaceholder.typicode.com/users --save=users.json
dart_http get https://api.example.com/me --header="Authorization: Bearer mytoken"
</code></pre>
<h3 id="heading-building-darthttp">Building dart_http</h3>
<p>Create the project:</p>
<pre><code class="language-bash">dart create -t console dart_http
cd dart_http
</code></pre>
<p>Update <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">name: dart_http
description: A lightweight API request runner for the terminal
version: 1.0.0

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_http: dart_http

dependencies:
  args: ^2.4.2
  http: ^1.2.1

dev_dependencies:
  lints: ^3.0.0
  test: ^1.24.0
</code></pre>
<p>Run <code>dart pub get</code>.</p>
<p>Project structure:</p>
<pre><code class="language-plaintext">dart_http/
  bin/
    dart_http.dart
  lib/
    runner/
      request_runner.dart
    printer/
      response_printer.dart
    utils/
      headers_parser.dart
  pubspec.yaml
</code></pre>
<h4 id="heading-step-1-headers-parser-libutilsheadersparserdart">Step 1 — Headers Parser (<code>lib/utils/headers_parser.dart</code>)</h4>
<pre><code class="language-dart">Map&lt;String, String&gt; parseHeaders(List&lt;String&gt; rawHeaders) {
  final headers = &lt;String, String&gt;{};

  for (final header in rawHeaders) {
    final index = header.indexOf(':');
    if (index == -1) continue;

    final key = header.substring(0, index).trim();
    final value = header.substring(index + 1).trim();
    headers[key] = value;
  }

  return headers;
}
</code></pre>
<h4 id="heading-step-2-response-printer-libprinterresponseprinterdart">Step 2 — Response Printer (<code>lib/printer/response_printer.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:convert';
import 'dart:io';

void printResponse({
  required int statusCode,
  required String body,
  required int durationMs,
  required int bodyBytes,
}) {
  final statusLabel = _statusLabel(statusCode);
  final size = _formatSize(bodyBytes);

  stdout.writeln('');
  stdout.writeln('\(statusLabel | \){durationMs}ms | $size');
  stdout.writeln('─' * 50);

  try {
    final decoded = jsonDecode(body);
    const encoder = JsonEncoder.withIndent('  ');
    stdout.writeln(encoder.convert(decoded));
  } catch (_) {
    // Not JSON — print as plain text
    stdout.writeln(body);
  }

  stdout.writeln('');
}

String _statusLabel(int code) {
  if (code &gt;= 200 &amp;&amp; code &lt; 300) return '✅ $code';
  if (code &gt;= 300 &amp;&amp; code &lt; 400) return '↪️  $code';
  if (code &gt;= 400 &amp;&amp; code &lt; 500) return '❌ $code';
  return '$code';
}

String _formatSize(int bytes) {
  if (bytes &lt; 1024) return '${bytes}b';
  if (bytes &lt; 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)}kb';
  return '${(bytes / (1024 * 1024)).toStringAsFixed(1)}mb';
}
</code></pre>
<h4 id="heading-step-3-request-runner-librunnerrequestrunnerdart">Step 3 — Request Runner (<code>lib/runner/request_runner.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:http/http.dart' as http;

import '../printer/response_printer.dart';

Future&lt;void&gt; runRequest({
  required String method,
  required String url,
  required Map&lt;String, String&gt; headers,
  String? body,
  String? saveToFile,
}) async {
  final uri = Uri.tryParse(url);

  if (uri == null) {
    stderr.writeln('Error: "$url" is not a valid URL');
    exit(1);
  }

  stdout.writeln('→ \({method.toUpperCase()} \)url');

  http.Response response;
  final stopwatch = Stopwatch()..start();

  try {
    switch (method.toLowerCase()) {
      case 'get':
        response = await http.get(uri, headers: headers);
      case 'post':
        response = await http.post(uri, headers: headers, body: body);
      case 'put':
        response = await http.put(uri, headers: headers, body: body);
      case 'patch':
        response = await http.patch(uri, headers: headers, body: body);
      case 'delete':
        response = await http.delete(uri, headers: headers);
      default:
        stderr.writeln('Error: unsupported method "$method"');
        exit(2);
    }
  } catch (e) {
    stderr.writeln('Error: request failed — $e');
    exit(1);
  }

  stopwatch.stop();

  printResponse(
    statusCode: response.statusCode,
    body: response.body,
    durationMs: stopwatch.elapsedMilliseconds,
    bodyBytes: response.bodyBytes.length,
  );

  if (saveToFile != null) {
    final file = File(saveToFile);
    file.writeAsStringSync(response.body);
    stdout.writeln('Response saved to $saveToFile');
  }
}
</code></pre>
<h4 id="heading-step-4-entry-point-bindarthttpdart">Step 4 — Entry Point (<code>bin/dart_http.dart</code>)</h4>
<pre><code class="language-dart">import 'dart:io';

import 'package:args/args.dart';

import '../lib/runner/request_runner.dart';
import '../lib/utils/headers_parser.dart';

void main(List&lt;String&gt; arguments) async {
  final parser = ArgParser();

  for (final method in ['get', 'post', 'put', 'patch', 'delete']) {
    final commandParser = ArgParser()
      ..addMultiOption('header', abbr: 'H', help: 'Request header (repeatable)')
      ..addOption('body', abbr: 'b', help: 'Request body (for POST/PUT/PATCH)')
      ..addOption('save', abbr: 's', help: 'Save response body to a file');

    parser.addCommand(method, commandParser);
  }

  parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show help');

  ArgResults results;

  try {
    results = parser.parse(arguments);
  } catch (e) {
    stderr.writeln('Error: $e');
    printHelp();
    exit(2);
  }

  if (results['help'] as bool || results.command == null) {
    printHelp();
    exit(0);
  }

  final command = results.command!;
  final method = command.name!;
  final rest = command.rest;

  if (rest.isEmpty) {
    stderr.writeln('Error: please provide a URL');
    stderr.writeln('Usage: dart_http $method &lt;url&gt;');
    exit(2);
  }

  final url = rest[0];
  final rawHeaders = command['header'] as List&lt;String&gt;;
  final body = command['body'] as String?;
  final saveToFile = command['save'] as String?;

  final headers = parseHeaders(rawHeaders);

  // Default Content-Type for requests with a body
  if (body != null &amp;&amp; !headers.containsKey('Content-Type')) {
    headers['Content-Type'] = 'application/json';
  }

  await runRequest(
    method: method,
    url: url,
    headers: headers,
    body: body,
    saveToFile: saveToFile,
  );
}

void printHelp() {
  stdout.writeln('''
dart_http — a lightweight API request runner

Usage:
  dart_http &lt;method&gt; &lt;url&gt; [options]

Methods:
  get       Send a GET request
  post      Send a POST request
  put       Send a PUT request
  patch     Send a PATCH request
  delete    Send a DELETE request

Options:
  -H, --header    Add a request header (repeatable)
  -b, --body      Request body (JSON string)
  -s, --save      Save response body to a file
  -h, --help      Show this help message

Examples:
  dart_http get https://jsonplaceholder.typicode.com/users
  dart_http get https://api.example.com/me --header="Authorization: Bearer token"
  dart_http post https://api.example.com/posts --body=\'{"title":"Hello"}\'
  dart_http get https://api.example.com/users --save=users.json
  ''');
}
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">dart run bin/dart_http.dart get https://jsonplaceholder.typicode.com/users/1

# → GET https://jsonplaceholder.typicode.com/users/1
# 200 | 87ms | 510b
# ──────────────────────────────────────────────────
# {
#   "id": 1,
#   "name": "Leanne Graham",
#   "username": "Bret",
#   "email": "Sincere@april.biz"
# }

dart run bin/dart_http.dart get https://jsonplaceholder.typicode.com/users --save=users.json
# → GET https://jsonplaceholder.typicode.com/users
# 200 | 143ms | 5.3kb
# ──────────────────────────────────────────────────
# [ ... ]
# Response saved to users.json

dart run bin/dart_http.dart post https://jsonplaceholder.typicode.com/posts \
  --body='{"title":"Hello from dart_http","userId":1}'
# → POST https://jsonplaceholder.typicode.com/posts
# 201 | 312ms | 72b
</code></pre>
<h2 id="heading-adding-color-and-polish-to-your-cli">Adding Color and Polish to Your CLI</h2>
<p>The CLIs above are functional, but terminal output can be made significantly more readable with color. The <code>ansi_styles</code> package provides ANSI escape code support for coloring text in the terminal.</p>
<p>Add it to <code>pubspec.yaml</code>:</p>
<pre><code class="language-yaml">dependencies:
  ansi_styles: ^0.3.0
</code></pre>
<p>Using it:</p>
<pre><code class="language-dart">import 'package:ansi_styles/ansi_styles.dart';

stdout.writeln(AnsiStyles.green('✅ Success'));
stdout.writeln(AnsiStyles.red('❌ Error: something went wrong'));
stdout.writeln(AnsiStyles.yellow('⚠️  Warning: check your config'));
stdout.writeln(AnsiStyles.bold('dart_http — API request runner'));
stdout.writeln(AnsiStyles.cyan('→ GET https://api.example.com/users'));
</code></pre>
<p>Apply color intentionally and consistently:</p>
<ul>
<li><p><strong>Green</strong> — success states, completed operations</p>
</li>
<li><p><strong>Red</strong> — errors and failures</p>
</li>
<li><p><strong>Yellow</strong> — warnings and non-blocking issues</p>
</li>
<li><p><strong>Cyan</strong> — informational output, URLs, paths</p>
</li>
<li><p><strong>Bold</strong> — headers, tool names, important values</p>
</li>
</ul>
<p>Avoid coloring everything. Color loses meaning when it is everywhere. Use it to draw the user's eye to what actually matters.</p>
<h2 id="heading-testing-your-cli-tool">Testing Your CLI Tool</h2>
<p>CLI tools are testable, and they should be tested. The most reliable approach is to test the logic inside your commands directly — not the terminal output formatting, but the behaviour.</p>
<p>Add <code>test</code> to your dev dependencies if it's not already there:</p>
<pre><code class="language-yaml">dev_dependencies:
  test: ^1.24.0
</code></pre>
<p><strong>Testing command logic:</strong></p>
<pre><code class="language-dart">import 'package:test/test.dart';

import '../lib/models/task.dart';

void main() {
  group('Task model', () {
    test('copyWith updates isComplete correctly', () {
      final task = Task(
        id: 1,
        title: 'Write tests',
        priority: 'high',
        createdAt: DateTime.now(),
      );

      final completed = task.copyWith(isComplete: true);

      expect(completed.isComplete, isTrue);
      expect(completed.title, equals('Write tests'));
      expect(completed.id, equals(1));
    });

    test('toJson and fromJson round-trips correctly', () {
      final task = Task(
        id: 2,
        title: 'Ship the tool',
        priority: 'normal',
        createdAt: DateTime.parse('2025-01-01T00:00:00.000'),
      );

      final json = task.toJson();
      final restored = Task.fromJson(json);

      expect(restored.id, equals(task.id));
      expect(restored.title, equals(task.title));
      expect(restored.priority, equals(task.priority));
    });
  });
}
</code></pre>
<p><strong>Testing the headers parser:</strong></p>
<pre><code class="language-dart">import 'package:test/test.dart';

import '../lib/utils/headers_parser.dart';

void main() {
  group('parseHeaders', () {
    test('parses a single header correctly', () {
      final result = parseHeaders(['Authorization: Bearer mytoken']);
      expect(result['Authorization'], equals('Bearer mytoken'));
    });

    test('parses multiple headers', () {
      final result = parseHeaders([
        'Authorization: Bearer token',
        'Accept: application/json',
      ]);
      expect(result.length, equals(2));
      expect(result['Accept'], equals('application/json'));
    });

    test('ignores malformed headers without a colon', () {
      final result = parseHeaders(['malformed-header']);
      expect(result.isEmpty, isTrue);
    });
  });
}
</code></pre>
<p>Run your tests:</p>
<pre><code class="language-bash">dart test
</code></pre>
<h2 id="heading-deploying-and-distributing-your-cli">Deploying and Distributing Your CLI</h2>
<p>Building a CLI tool is half the work. Getting it into the hands of developers is the other half. There are five distribution paths available, each suited to a different use case.</p>
<h3 id="heading-mode-1-pubdev-public-package-distribution">Mode 1: pub.dev — Public Package Distribution</h3>
<p>Publishing to pub.dev makes your tool installable by anyone in the Dart and Flutter community with a single command.</p>
<h4 id="heading-prepare-your-package">Prepare your package:</h4>
<p>Your <code>pubspec.yaml</code> needs to be complete:</p>
<pre><code class="language-yaml">name: dart_http
description: A lightweight API request runner for Dart developers.
version: 1.0.0
homepage: https://github.com/yourname/dart_http

environment:
  sdk: '&gt;=3.0.0 &lt;4.0.0'

executables:
  dart_http: dart_http
</code></pre>
<p>The <code>executables</code> block is critical. It tells pub.dev which script in <code>bin/</code> to expose as a runnable command.</p>
<p>You also need:</p>
<ul>
<li><p><code>README.md</code> — what the tool does, how to install it, usage examples</p>
</li>
<li><p><code>CHANGELOG.md</code> — version history</p>
</li>
<li><p><code>LICENSE</code> — an open source license (MIT is standard)</p>
</li>
</ul>
<h4 id="heading-validate-before-publishing">Validate before publishing:</h4>
<pre><code class="language-bash">dart pub publish --dry-run
</code></pre>
<p>This runs all validation checks without actually publishing. Fix any warnings before proceeding.</p>
<h4 id="heading-publish">Publish:</h4>
<pre><code class="language-bash">dart pub publish
</code></pre>
<p>You will be prompted to authenticate with your pub.dev account. Once published, your tool is available globally:</p>
<pre><code class="language-bash">dart pub global activate dart_http
dart_http get https://api.example.com/users
</code></pre>
<h3 id="heading-mode-2-local-path-activation">Mode 2: Local Path Activation</h3>
<p>For internal team tools that you don't want to publish publicly, activate directly from a local or cloned repository:</p>
<pre><code class="language-bash">dart pub global activate --source path /path/to/dart_http
</code></pre>
<p>Any developer on the team clones the repo and runs this command once. The tool is then available globally in their terminal without needing a pub.dev publish.</p>
<p>This is the right distribution mode for:</p>
<ul>
<li><p>Internal company tooling</p>
</li>
<li><p>Tools that depend on private packages</p>
</li>
<li><p>Work-in-progress tools shared within a team before a public release</p>
</li>
</ul>
<h3 id="heading-mode-3-compiled-binary-via-github-releases">Mode 3: Compiled Binary via GitHub Releases</h3>
<p>Dart can compile to a self-contained native executable — no Dart SDK required on the target machine. This makes your tool accessible to developers outside the Dart ecosystem.</p>
<h4 id="heading-compile">Compile:</h4>
<pre><code class="language-bash"># macOS
dart compile exe bin/dart_http.dart -o dist/dart_http-macos

# Linux
dart compile exe bin/dart_http.dart -o dist/dart_http-linux

# Windows
dart compile exe bin/dart_http.dart -o dist/dart_http-windows.exe
</code></pre>
<p>The compiled binary is fully self-contained. Copy it to any machine and run it — no Dart installation needed.</p>
<h4 id="heading-automate-with-github-actions">Automate with GitHub Actions:</h4>
<p>Create <code>.github/workflows/release.yml</code>:</p>
<pre><code class="language-yaml">name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v3

      - uses: dart-lang/setup-dart@v1
        with:
          sdk: stable

      - name: Install dependencies
        run: dart pub get

      - name: Compile binary
        run: |
          mkdir -p dist
          dart compile exe bin/dart_http.dart -o dist/dart_http-${{ runner.os }}

      - name: Upload binary to release
        uses: softprops/action-gh-release@v1
        with:
          files: dist/dart_http-${{ runner.os }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
</code></pre>
<p>Every time you push a version tag (<code>v1.0.0</code>), GitHub Actions compiles binaries for all three platforms and attaches them to the GitHub Release automatically.</p>
<h4 id="heading-write-an-install-script">Write an install script:</h4>
<pre><code class="language-bash">#!/usr/bin/env bash
set -euo pipefail

VERSION="1.0.0"
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
BINARY="dart_http-$OS"
INSTALL_DIR="/usr/local/bin"

curl -L "https://github.com/yourname/dart_http/releases/download/v\(VERSION/\)BINARY" \
  -o "$INSTALL_DIR/dart_http"

chmod +x "$INSTALL_DIR/dart_http"
echo "dart_http installed successfully"
</code></pre>
<p>Developers install it with:</p>
<pre><code class="language-bash">curl -fsSL https://raw.githubusercontent.com/yourname/dart_http/main/install.sh | bash
</code></pre>
<h3 id="heading-mode-4-homebrew-tap">Mode 4: Homebrew Tap</h3>
<p>Homebrew is the standard package manager for macOS and is widely used on Linux. A Homebrew tap makes your tool installable with <code>brew install</code> — the most familiar installation pattern for macOS developers.</p>
<h4 id="heading-create-your-tap-repository">Create your tap repository:</h4>
<p>Create a new GitHub repository named <code>homebrew-tools</code> (the <code>homebrew-</code> prefix is required by Homebrew's naming convention).</p>
<h4 id="heading-write-the-formula">Write the formula:</h4>
<p>Create <code>Formula/dart_http.rb</code> in that repository:</p>
<pre><code class="language-ruby">class DartHttp &lt; Formula
  desc "A lightweight API request runner for the terminal"
  homepage "https://github.com/yourname/dart_http"
  version "1.0.0"

  on_macos do
    url "https://github.com/yourname/dart_http/releases/download/v1.0.0/dart_http-macOS"
    sha256 "YOUR_SHA256_HASH_HERE"
  end

  on_linux do
    url "https://github.com/yourname/dart_http/releases/download/v1.0.0/dart_http-Linux"
    sha256 "YOUR_SHA256_HASH_HERE"
  end

  def install
    bin.install "dart_http-#{OS.mac? ? 'macOS' : 'Linux'}" =&gt; "dart_http"
  end

  test do
    system "#{bin}/dart_http", "--help"
  end
end
</code></pre>
<p>Generate the SHA256 hash for each binary:</p>
<pre><code class="language-bash">shasum -a 256 dist/dart_http-macOS
</code></pre>
<h4 id="heading-install-from-the-tap">Install from the tap:</h4>
<pre><code class="language-bash">brew tap yourname/tools
brew install dart_http
</code></pre>
<p>When you release a new version, update the <code>url</code> and <code>sha256</code> values in the formula and push the change. Users run <code>brew upgrade dart_http</code> to update.</p>
<h3 id="heading-mode-5-docker">Mode 5: Docker</h3>
<p>Docker distribution is best suited for CI environments, teams that standardise on containers, or tools with complex dependencies.</p>
<h4 id="heading-write-a-dockerfile">Write a Dockerfile:</h4>
<pre><code class="language-dockerfile">FROM dart:stable AS build

WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart compile exe bin/dart_http.dart -o /app/dart_http

FROM debian:stable-slim
COPY --from=build /app/dart_http /usr/local/bin/dart_http

ENTRYPOINT ["dart_http"]
</code></pre>
<p>This uses a multi-stage build: the first stage compiles the binary using the Dart SDK image, and the second stage copies only the binary into a minimal Debian image. The final image has no Dart SDK — just the compiled binary.</p>
<h4 id="heading-build-and-run">Build and run:</h4>
<pre><code class="language-bash">docker build -t dart_http .
docker run dart_http get https://jsonplaceholder.typicode.com/users/1
</code></pre>
<h4 id="heading-publish-to-docker-hub">Publish to Docker Hub:</h4>
<pre><code class="language-bash">docker tag dart_http yourname/dart_http:1.0.0
docker push yourname/dart_http:1.0.0
</code></pre>
<p>Users can then run your tool without installing anything locally:</p>
<pre><code class="language-bash">docker run yourname/dart_http get https://api.example.com/users
</code></pre>
<h2 id="heading-choosing-the-right-distribution-mode">Choosing the Right Distribution Mode</h2>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Best for</th>
<th>Dart SDK required</th>
</tr>
</thead>
<tbody><tr>
<td>pub.dev</td>
<td>Public Dart/Flutter developer tools</td>
<td>Yes</td>
</tr>
<tr>
<td>Local path activation</td>
<td>Internal team tools, pre-release builds</td>
<td>Yes</td>
</tr>
<tr>
<td>Compiled binary</td>
<td>Language-agnostic tools, broad adoption</td>
<td>No</td>
</tr>
<tr>
<td>Homebrew tap</td>
<td>macOS/Linux developer tools</td>
<td>No</td>
</tr>
<tr>
<td>Docker</td>
<td>CI environments, complex dependencies</td>
<td>No</td>
</tr>
</tbody></table>
<p>For most tools, the practical recommendation is:</p>
<ul>
<li><p>Start with <strong>pub.dev</strong> if your audience is Dart developers</p>
</li>
<li><p>Add <strong>compiled binary + GitHub Releases</strong> once you want broader adoption</p>
</li>
<li><p>Add a <strong>Homebrew tap</strong> when macOS developers start asking for it</p>
</li>
<li><p>Use <strong>Docker</strong> only when it is already part of your team's workflow</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've gone from understanding what a CLI is to building three progressively complex tools and distributing them across five different channels.</p>
<p>The foundational skills – <code>args</code>, <code>stdin</code>, <code>stdout</code>, <code>stderr</code>, exit codes, file I/O, and process spawning – are the same building blocks that tools like <code>flutter</code>, <code>git</code>, and <code>dart</code> themselves are built on. Everything else is composition.</p>
<p>The three CLIs we built (Hello CLI, <code>dart_todo</code>, and <code>dart_http</code>) each introduced a new layer: raw Dart fundamentals, the <code>args</code> package with JSON persistence, and real-world HTTP interaction. The distribution section ensures that whatever you build next, you have a clear path to getting it in front of the developers who will use it.</p>
<p>Dart is a powerful language for CLI development. Its strong typing, async support, native compilation, and pub.dev ecosystem make it a serious choice for building developer tooling, not just mobile apps.</p>
<p>The next step is building something that solves a real problem for you or your team, and shipping it.</p>
<p>Happy coding!!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Unblock Your AI PR Review Bottleneck: A Tech Lead’s Guide to Building a Codebase-Aware Reviewer ]]>
                </title>
                <description>
                    <![CDATA[ A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By ev ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-unblock-ai-pr-review-bottleneck-handbook/</link>
                <guid isPermaLink="false">69f906a346610fd60629a300</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code review ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                    <category>
                        <![CDATA[ leadership ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Qudrat Ullah ]]>
                </dc:creator>
                <pubDate>Mon, 04 May 2026 20:50:43 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/c94dff21-66d0-4256-bf3e-25c1978364d9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A few months ago, I was reviewing a pull request that added three new API endpoints. The diff was clean. Tests passed. The agent that generated it had even written sensible authorisation checks. By every signal I usually rely on, it was ready to merge.</p>
<p>The problem only showed up when I checked which authentication middleware the agent had imported.</p>
<p>Our codebase had two: a v1 middleware backed by MongoDB and a v2 middleware backed by MySQL, which we had spent the previous quarter migrating.</p>
<p>New endpoints were supposed to use v2. The agent had used v1 for all three. Tests passed because user records still existed in both databases (that was the point of the migration), and the v1 middleware happily authenticated them. The code worked. But every new endpoint we shipped was reinforcing the legacy auth path we had just spent a quarter trying to retire.</p>
<p>I caught it on the second read. Twenty minutes after the comments, the engineer fixed it and reopened the PR. The third reviewer probably wouldn't have caught it. The migration timeline lived in a Slack thread from six months earlier. The rule that "new endpoints use v2" lived in my head.</p>
<p>This kind of catch is the slow-burn version of why AI changed my job as a tech lead. Code generation got faster. My review queue got longer. The hardest reviews were the ones where everything looked right, and the only thing wrong was something that lived in the team's collective memory rather than in the diff.</p>
<p>This handbook is about what we did to fix that. It's the story of how we went from drowning in clean-looking PRs to running a custom AI PR reviewer that catches a meaningful share of these mistakes before any human is pulled in. The fix turned out to be less about buying a better tool and more about moving the team's memory into a place the AI could actually read.</p>
<p>The lessons should transfer whether your team uses Claude Code, Cursor, Cline, GitHub Copilot, or any combination. The structure matters more than the tool.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</a></p>
</li>
<li><p><a href="#heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</a></p>
</li>
<li><p><a href="#heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</a></p>
</li>
<li><p><a href="#heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</a></p>
</li>
<li><p><a href="#heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</a></p>
</li>
<li><p><a href="#heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</a></p>
</li>
<li><p><a href="#heading-what-this-looks-like-on-disk">What This Looks Like on Disk</a></p>
</li>
<li><p><a href="#heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</a></p>
</li>
<li><p><a href="#heading-building-the-pr-review-command">Building the PR Review Command</a></p>
</li>
<li><p><a href="#heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</a></p>
</li>
<li><p><a href="#heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</a></p>
</li>
<li><p><a href="#heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</a></p>
</li>
<li><p><a href="#heading-what-still-needs-human-review">What Still Needs Human Review</a></p>
</li>
<li><p><a href="#heading-a-two-week-setup-plan">A Two-Week Setup Plan</a></p>
</li>
<li><p><a href="#heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</a></p>
</li>
<li><p><a href="#heading-sources">Sources</a></p>
</li>
</ul>
<h2 id="heading-the-old-bottleneck-and-the-one-ai-created">The Old Bottleneck, and the One AI Created</h2>
<p>To understand why this fix was needed, it helps to remember what reviewing code looked like a couple of years ago.</p>
<p>Back then, the slow part was upstream of the PR. A ticket would land, and before anyone could open a branch, there was a long preamble of context-gathering.</p>
<p>Junior engineers needed time to understand what the change was for. Senior engineers had to explain business rules and architectural decisions. Tickets sat in "ready" columns for days while someone with the right context made themselves available. Then the writing itself took time, because typing real code is slower than typing comments about it.</p>
<p>That bottleneck mostly dissolved when the team got serious about AI-assisted development. Engineers used the agent to read the codebase, ask clarifying questions, draft an implementation plan, and produce a working branch in hours instead of days. Tickets moved through the queue faster. Junior engineers shipped more without blocking on senior availability. From the outside, this looked like an unambiguous win.</p>
<p>But the bottleneck didn't disappear. It moved.</p>
<p>Within a few weeks of widespread AI adoption, my review queue had doubled. Then tripled. Engineers were opening PRs faster than I could read them.</p>
<p>The PRs themselves looked clean: well-formatted, with sensible variable names, passing tests, and AI-generated descriptions that read better than most human-written ones.</p>
<p>On the surface, this was great. In practice, it was creating a different kind of pain. I was the senior engineer who knew which patterns mattered and which paths through the codebase were the right ones, and I was the bottleneck. The team's velocity was now capped by my reading speed.</p>
<p>The CircleCI 2026 State of Software Delivery report confirmed I was not alone. Drawing on more than 28 million CI workflow runs across over 22,000 organisations, the report showed feature branch throughput had grown 59% year over year, the largest jump CircleCI had ever measured. Main branch throughput, where code actually gets promoted to production, fell by 7% for the median team in the same period. Build success rates dropped to 70.8%, the lowest in five years.</p>
<p>The pattern was consistent across the industry. AI accelerated writing. The rest of the system absorbed the cost.</p>
<p>So the question for me, as a tech lead, became concrete: how do I unblock myself without lowering the bar?</p>
<h2 id="heading-what-the-new-review-work-actually-looks-like">What the New Review Work Actually Looks Like</h2>
<p>Before I explain the fix, it helps to know what kinds of issues were actually piling up. They weren't the dramatic kind. None of them would crash production. They were small, recurring, and looked plausible at a glance.</p>
<p>Take the simplest case I kept catching. An engineer would ask the agent to add a delete button on a new screen. The button needed to call our existing backend delete endpoint. Instead of reusing the hook the team already had for that endpoint, the agent would write the fetch call inline.</p>
<p>The code worked. The tests passed. But a week later, when someone changed the backend response shape, only one of the two call sites got updated.</p>
<p>That kind of duplication doesn't show up in a code review unless the reviewer happens to remember that a hook exists.</p>
<p>Another example I saw constantly: the agent comparing a status field against the literal string <code>"completed"</code> instead of using the <code>Status.Completed</code> enum that the rest of the services used. The code ran. The tests ran. The next refactor of the enum quietly skipped the file. After a few days, someone would spend half a day debugging a state machine that was working fine until the agent's literal silently fell out of sync.</p>
<p>These were two-minute fixes once spotted, but spotting them took me a reasonable time per PR. The friction wasn't the difficulty. It was the repetition.</p>
<p>The pattern repeated across larger problems, too.</p>
<p>I once asked an agent to build an event creation wizard. The wizard needed several dropdowns and one new component.</p>
<p>We have a design system folder where shared UI components live, and the rule on the team is simple: check there first, and if you build something new, register it there.</p>
<p>The agent had no way to know that. It only loaded the wizard's own files, so it never opened the design system folder. It generated brand new dropdowns inline, with APIs that were almost identical to the ones we already had. The new component went straight into the wizard rather than into the design system. CI passed. The wizard worked. We caught the duplication in human review, but it was the kind of catch that depended entirely on a reviewer who happened to know the design system existed.</p>
<p>The same pattern hit in one of the repos I was looking at for backend architecture. Backend follows a strict four-layer pattern: route, controller, app, repo. Controllers must never call repository functions directly. That rule keeps authorisation centralised, business logic testable, and database concerns isolated.</p>
<p>One PR I reviewed had the agent calling repo functions straight from a controller, skipping the app layer entirely. The code worked. The tests passed because the agent had also written tests against the new shape. But it broke a discipline the team had spent years building. If that PR had landed, the next AI-assisted PR could have used it as a template, and the layering would have eroded one diff at a time.</p>
<p>The common thread is that all of these mistakes had something written down somewhere, in code, in a Slack thread, in a senior engineer's head, that would have prevented them. The information existed. The agent just couldn't see it.</p>
<h2 id="heading-why-i-did-not-just-buy-a-tool">Why I Did Not Just Buy a Tool</h2>
<p>The obvious next move was to install one of the AI PR reviewers that flooded the market in 2026.</p>
<p>I evaluated several. Anthropic launched Claude Code Review in March 2026, billed on token usage and averaging \(15 to \)25 per review. CodeRabbit Pro charges \(24 per developer per month on annual billing, or \)30 per developer per month on monthly billing, with seats counted against developers who actually open PRs. Greptile in March 2026 moved to a base-plus-usage model at $30 per seat per month, including 50 reviews, after which each additional review costs a dollar. GitHub announced that all Copilot plans will transition to usage-based billing on June 1, 2026, with code reviews consuming both AI Credits and GitHub Actions minutes from that date.</p>
<p>For a small team with low PR volume, none of these is a dealbreaker. For a larger team running heavy AI-assisted development, the costs compound fast. A 10-person team running five PRs each per day blows through Greptile's included reviews in a single week. CodeRabbit Pro at \(24 per seat scales linearly with developers. The premium Claude Code Review at \)15 to $25 per PR is the most expensive option per review by an order of magnitude.</p>
<p>I looked at the cost numbers, but cost wasn't actually the deciding factor. The deciding factor was that none of these tools would have caught the problems I just listed.</p>
<p>A generic reviewer wouldn't have caught the v1/v2 middleware. It had no way to know v2 was the canonical path. A generic reviewer wouldn't have caught the duplicate dropdowns. It had no way to know our design system existed. A generic reviewer wouldn't have caught the bypassed architecture. It had no way to know that controllers must not call repositories.</p>
<p>The information that lets a reviewer flag any of these is exactly the information that lives in the team's head, not in any tool's default prompt.</p>
<p>The better-rated tools support custom rules, and that's where I started to see the real shape of the problem. Once you are configuring custom rules, you've already accepted that the value is in the rules. The tool is just whatever runs them.</p>
<p>This raised a different question: if the rules are the product, why pay per seat or per review for someone else's wrapper around them?</p>
<p>This is what made me change direction.</p>
<h2 id="heading-the-realisation-move-the-rules-into-the-codebase">The Realisation: Move the Rules Into the Codebase</h2>
<p>Once I started thinking of the rules as the product, the path forward got clearer.</p>
<p>I asked myself a simple question: what was I actually doing in code review that the AI was not? The answer turned out to be the same thing, over and over. I was typing review comments that captured a piece of the team's memory.</p>
<p>"Use the Status enum, not a string literal." "There is already a hook for this in <code>/hooks/useDeleteItem</code>." "Controllers must not import from the repo layer; route this through the app layer." "Check the design system folder before creating new components."</p>
<p>Each of those comments was knowledge that lived in my head and arrived in the codebase one PR comment at a time. None of it was available to the agent the next time it generated a similar PR.</p>
<p>So the fix was not to buy a smarter reviewer. The fix was to write the rules down in a place every agent on the team would read before any review happened.</p>
<p>If I had typed "use the enum, not a literal" three times in three different PRs, that was a rule the agent should know about from now on. If I had pointed at the design system folder for the fourth time, that was a rule. If I had explained the four-layer architecture twice in PR comments, that was a rule.</p>
<p>I needed somewhere to put these rules. That turned out to be a less obvious decision than I expected.</p>
<h2 id="heading-two-files-that-changed-everything-agentsmd-and-claudemd">Two Files That Changed Everything: AGENTS.md and CLAUDE.md</h2>
<p>If you start looking into how to give an AI agent a persistent project context, you run into two competing conventions almost immediately.</p>
<p>The first is <strong>AGENTS.md</strong>, an open standard that has gathered real momentum. According to InfoQ, by mid-2025, the format had already been adopted by more than 20,000 GitHub repositories and was being positioned as a complement to traditional documentation: machine-readable context that lives alongside human-facing files like README.md.</p>
<p>The standard's own site reports it is now used by more than 60,000 open-source projects and has moved to stewardship under the Agentic AI Foundation, which sits inside the Linux Foundation. The format is supported by OpenAI Codex, GitHub Copilot, Google Gemini, Cursor, and Windsurf, among others.</p>
<p>The second is <strong>CLAUDE.md</strong>, which is Anthropic's convention for Claude Code. The Claude Code documentation describes two complementary memory systems: CLAUDE.md, where you write the persistent context yourself, and an auto-memory mechanism that lets Claude save its own notes from corrections and observed patterns. By default, Claude Code reads CLAUDE.md, not AGENTS.md.</p>
<p>This split mattered for us because half the team uses Claude Code and the other half uses Cursor. We had two practical options: maintain both files with the same content (and accept the duplication), or symlink one filename to the other so both ecosystems read the same source of truth. We went with the symlink. It's one less thing to drift.</p>
<p>The next question was what to actually put in the file. After a few iterations, here's the shape that worked. Think of it as a briefing document for a new engineer who has read no code and seen no Slack threads. The minimum content was:</p>
<ul>
<li><p>The tech stack (languages, frameworks, package manager)</p>
</li>
<li><p>The project structure, especially important for our monorepo</p>
</li>
<li><p>Where shared utilities, components, and helpers live, and the rule that new code should reuse them before creating new versions</p>
</li>
<li><p>Architectural patterns the project follows, with file path examples</p>
</li>
<li><p>Anti-patterns and what to do instead</p>
</li>
<li><p>Test conventions and where good examples live</p>
</li>
<li><p>Pointers to deeper documentation when more detail is needed</p>
</li>
</ul>
<p>Two practical rules emerged from the first month of using these files.</p>
<p><strong>Keep them lean:</strong> There is a counterintuitive failure mode with long instruction lists: the agent doesn't just skip the new ones at the bottom. The average compliance across all of them drops. A bloated memory file becomes a memory file that the agent skims. If a section runs more than a paragraph or two, move it to a separate document and link to it.</p>
<p><strong>Phrase rules as imperatives, not aspirations:</strong> "Controllers must not call repositories. Route through the app layer." beats "Try to keep controllers thin." The first is testable. The second is decorative.</p>
<p>That was the entry point. But a single root-level file was not enough for a monorepo with multiple services and frontends, which led to the next decision.</p>
<h2 id="heading-where-per-service-memory-files-earn-their-keep">Where Per-Service Memory Files Earn Their Keep</h2>
<p>A single <code>AGENTS.md</code> at the root of a monorepo collapses under its own weight pretty quickly. Each service in our codebase has its own architecture, conventions, and business rules. Trying to fit all of that into one file produced a long document that the agent treated as background noise, and we were back to the bloat problem from the previous section.</p>
<p>The pattern that worked: every service or app gets its own <code>AGENTS.md</code> at its root, and the project-level <code>AGENTS.md</code> becomes an index that points to them.</p>
<p>A per-service <code>AGENTS.md</code> covers things like:</p>
<ul>
<li><p>The architecture for this service (the four-layer pattern, the directory layout)</p>
</li>
<li><p>Naming conventions specific to this service</p>
</li>
<li><p>Test patterns and where good examples live</p>
</li>
<li><p>Business rules that this service is responsible for</p>
</li>
<li><p>Inter-service contracts and what other services consume from this one</p>
</li>
<li><p>Pointers to deeper docs in <code>docs/</code></p>
</li>
<li><p>A "Lessons learned" section, which I'll come back to in the section on the compounding loop</p>
</li>
</ul>
<p>The same lean rule applies. Keep it short, point at examples, and phrase guidance as imperatives.</p>
<p>The reason this works mechanically is that the agent loads the right files for the work at hand. When an engineer asks the agent to change something in <code>backend/</code>, the agent reads the project-level <code>AGENTS.md</code>, sees that work in <code>backend/</code> should be guided by <code>backend/AGENTS.md</code>, and loads that file. It doesn't load the frontend's <code>AGENTS.md</code>, because that work is somewhere else. The context window stays focused on what's relevant.</p>
<p>Without this split, you have two bad options. Either you put everything in the root file, where the agent ignores most of it, or you put nothing in the root file, where the agent has no team context at all. The per-service split gives you both depth and signal.</p>
<p>But these files only work if the deeper docs they point to actually exist, which is where the next piece of the system came in.</p>
<h2 id="heading-what-this-looks-like-on-disk">What This Looks Like on Disk</h2>
<p>Before going further, it helps to see the whole structure laid out. Here's the shape we settled on for our monorepo. The exact folder names follow Claude Code's conventions. If you use Cursor, it would be <code>.cursor/</code>, and if you use Cline, it would be <code>.clinerules</code> – but the shape transfers directly.</p>
<pre><code class="language-plaintext">project-root/
├── AGENTS.md                       # symlink to CLAUDE.md
├── CLAUDE.md                       # root memory file
├── README.md                       # human-facing project readme
│
├── .claude/                        # tool-specific config folder
│   ├── README.md                   # explains the .claude/ layout
│   ├── settings.json               # permissions and guardrails
│   ├── agents/                     # specialised subagents (optional)
│   ├── commands/                   # slash commands engineers run
│   │   ├── review-pr.md            # the PR review command
│   │   └── plan-feature.md         # implementation plan command
│   ├── hooks/                      # lifecycle hooks (optional)
│   ├── pr-rules/                   # rule files for PR review
│   │   ├── common.md               # rules that apply to every PR
│   │   ├── frontend.md             # rules for frontend changes
│   │   ├── backend.md              # rules for backend changes
│   │   ├── service-a.md            # rules for service-a
│   │   └── service-b.md            # rules for service-b
│   └── skills/                     # reusable workflows
│
├── frontend/
│   ├── AGENTS.md                   # frontend conventions
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # routing, state, data layer
│   │   ├── design-system.md        # design system reference
│   │   └── testing.md              # test conventions
│   └── src/
│
├── backend/
│   ├── AGENTS.md                   # the four-layer pattern
│   ├── docs/
│   │   ├── overview.md
│   │   ├── architecture.md         # route -&gt; controller -&gt; app -&gt; repo
│   │   ├── auth.md                 # v1 vs v2 middleware
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
├── service-a/
│   ├── AGENTS.md
│   ├── docs/
│   │   ├── overview.md
│   │   ├── business-rules.md
│   │   └── integrations.md
│   └── src/
│
└── service-b/
    ├── AGENTS.md
    ├── docs/
    │   ├── overview.md
    │   ├── business-rules.md
    │   └── integrations.md
    └── src/
</code></pre>
<p>A few things worth pointing out:</p>
<p>The <code>.claude/</code> folder uses standard subfolder names: <code>commands</code>, <code>agents</code>, <code>hooks</code>, <code>skills</code>. These follow Claude Code's plugin model, but most modern AI coding tools have similar slots. Following the conventions makes the structure recognisable to anyone on the team and lowers the cost of switching tools later.</p>
<p>The <code>pr-rules/</code> folder isn't a standard convention. It's a folder we created to hold per-area review rules that the PR review command loads selectively. You don't have to call it <code>pr-rules</code> – the name matters less than having one place where review rules live.</p>
<p>Each service has its own <code>AGENTS.md</code> plus a <code>docs/</code> folder. The root <code>AGENTS.md</code> is short and acts as an index. It tells the agent things like "if you touch files in <code>backend/</code>, also read <code>backend/AGENTS.md</code> first." The per-service file then points at the deeper docs as needed.</p>
<h2 id="heading-generated-documentation-as-a-side-effect">Generated Documentation as a Side Effect</h2>
<p>Setting up per-service <code>AGENTS.md</code> files surfaced a problem I had been quietly avoiding. Most of our services didn't have decent documentation. Not API reference material, which lives in code, but the higher-level "what does this service do, what business rules does it enforce, what does it consume and produce" information that lives in nobody's head except the original author's.</p>
<p>The honest reason was that writing this kind of documentation by hand had never paid back the time it took. By the time the doc was finished, half of it was already stale.</p>
<p>So I tried something I wouldn't have considered earlier. I used the AI itself to generate a first draft for each service. I pointed the agent at each service's code and asked it to produce a <code>docs/</code> folder with a specific structure: an overview, a list of business rules, an integrations document, a domain model, and any quirks worth knowing. The agent read the code, traced the call paths, and wrote a draft.</p>
<p>I then reviewed the output by hand, corrected the things it got wrong, and committed the result. The first drafts were 70-80% correct. The remaining 20-30% was where the agent had made plausible but wrong inferences, and those were exactly the cases where human review mattered.</p>
<p>The generated docs ended up serving two audiences. The agent uses them when reasoning about changes, which means it has real context for the service it's touching rather than guessing from local files. And new engineers use them on their first day, which has cut our onboarding time noticeably.</p>
<p>We used to write onboarding documents that drifted out of date within months. These docs stay closer to current because the agent reads them on every PR, and any drift gets surfaced when the agent gives wrong advice based on stale information.</p>
<p>The pattern that works is to keep the per-service <code>AGENTS.md</code> short and pointing at the docs, rather than duplicating their content. <code>AGENTS.md</code> is the always-loaded index. <code>docs/</code> holds the details. The agent loads the relevant doc on demand when the task calls for it.</p>
<p>With the rules in place and the docs in place, I had everything I needed to build the actual reviewer.</p>
<h2 id="heading-building-the-pr-review-command">Building the PR Review Command</h2>
<p>This is the piece that most directly unblocked my queue.</p>
<p>This command didn't appear out of nowhere. It started as the checklist I was running through in my head every time I opened a PR. I was reviewing every change manually, leaving the same comments, flagging the same patterns. So I wrote that checklist down, expanded it with references to the per-service docs for the harder rules, and turned it into a command anyone on the team could run.</p>
<p>Then I handed it to the engineers and changed the rule: run this on your own branch before marking the PR ready for review. That single shift moved the work from after the PR was opened to before. Engineers now catch 90-95% of the blockers, improvements, and nice-to-haves on their own machine, fix them locally, and only then push the change.</p>
<p>The PR description includes the AI's summary, so when anyone opens the PR, they can see the reviewer's green signal at the top before even reading the diff.</p>
<p>GitHub stays clean. The conversation on the PR becomes about the things that actually need a human, not the recurring stuff the team already knows how to fix.</p>
<p>The command lives in <code>.claude/commands/review-pr.md</code>. Here's a generalised version. Your tool's command structure may differ, but the shape is what matters.</p>
<pre><code class="language-markdown"># Review PR

Review the current branch's PR. Be direct. Cite `file:line`. Surface real issues,
no padding.

## 1. Scope the diff

Run, in order:

    gh pr view --json number,title,body,headRefName 2&gt;/dev/null || true
    git fetch origin main
    git log --no-merges origin/main..HEAD --oneline
    git diff origin/main...HEAD --stat
    git diff origin/main...HEAD

Read the PR body. Note the stated intent. Every change should trace to it. Flag
anything that does not.

Use `...` (three dots) for the diff. It compares against the merge base and
excludes commits brought in by merging main.

## 2. Load rules

Always read `.claude/pr-rules/common.md`.

Then read the per-area file for each workspace touched in the diff:

| Workspace path | Rules file                      |
| -------------- | ------------------------------- |
| `frontend/**`  | `.claude/pr-rules/frontend.md`  |
| `backend/**`   | `.claude/pr-rules/backend.md`   |
| `service-a/**` | `.claude/pr-rules/service-a.md` |
| `service-b/**` | `.claude/pr-rules/service-b.md` |

For non-trivial changes, follow doc pointers inside the rules files (for
example, `backend/AGENTS.md`, `backend/docs/architecture.md`).

Apply every entry under each file's "Lessons learned" section as a check.

## 3. Output

Use exactly this format.

    ## Summary
    &lt;one paragraph: what the PR does, whether it matches the stated intent&gt;

    ## Blocking
    - [file:line] issue, why it blocks

    ## Should fix
    - [file:line] issue

    ## Nice to have
    - issue

    ## Verified
    - what was checked and looks good

If nothing blocks, say so. Do not manufacture concerns.

If you find an issue worth remembering for future PRs, suggest the bullet to
add to the relevant rules file's "Lessons learned" section. Do not edit the
rules file yourself, leave that to the human.
</code></pre>
<p>A few of the design choices in this command turned out to matter more than I expected.</p>
<p>The structured output format (Summary, Blocking, Should fix, Nice to have, Verified) keeps the review easy to scan and easy to paste into a PR description. The "Verified" section is the most underrated of the five: it tells the human reviewer what the AI already checked, so they can spend their attention elsewhere. Without it, the human reviewer ends up doing the same checks twice.</p>
<p>The instruction to be direct and stop padding does real work. Without it, AI reviewers tend to manufacture concerns to look thorough, which trains engineers to skim past the bot. Telling it explicitly to say "nothing blocks" when nothing blocks made the signal-to-noise ratio of the output much better.</p>
<p>The "suggest a bullet for the rules file" instruction at the end is the heart of the whole system, and I'll explain why in the section on the compounding loop. The key constraint here is that the agent suggests the bullet but doesn't commit to it. A human evaluates whether it's general enough to be a rule, and only then adds it to the file. That manual step is what keeps the rules sharp instead of bloated.</p>
<p>With each PR, if humans fix something or the AI suggests something, you keep adding those to your MD files and keep improving your agents for the future. The result compounds quickly.</p>
<p>One more thing here: the diff-scoping commands are all read-only. The command shouldn't be able to push, edit PRs, or close anything. Which is the next piece of the system.</p>
<h2 id="heading-guardrails-read-only-by-default">Guardrails: Read-Only by Default</h2>
<p>Giving an AI agent broad permissions on your codebase is a security incident waiting to happen. Even if you trust the model to behave, an LLM occasionally does unexpected things, and a fast-moving agent on an unrestricted shell can cause damage in seconds.</p>
<p>The fix is a <code>settings.json</code> (in Claude Code – other tools have their own equivalents) at the root of <code>.claude/</code> that explicitly declares what the agent can and can't do. The deny list matters more than the allow list, and a good one is organised around four categories of risk.</p>
<p>The first is <strong>secrets and configuration</strong>. Any read against anything that appears to be a credential is blocked. That covers <code>.env</code> files of every variant (<code>.env</code>, <code>.env.local</code>, <code>.env.production</code>, <code>.env.test</code>, and so on), <code>.npmrc</code>, <code>.netrc</code>, <code>.pgpass</code>, <code>id_rsa</code>, <code>id_ed25519</code>, <code>*.pem</code>, <code>*.key</code>, <code>*.p12</code>, <code>**/credentials.json</code>, <code>**/secrets.json</code>, <code>**/.aws/**</code>, <code>**/.ssh/**</code>, <code>**/.gcloud/**</code>, and <code>**/.kube/**</code>. Environment dumps are blocked too: <code>env</code>, <code>printenv</code>, <code>set</code>, <code>export</code>. The agent has no legitimate reason to read or echo any of these, ever.</p>
<p>The second is <strong>destructive Git operations</strong>. The agent can read Git history but can't rewrite or push it. Blocked: <code>git push</code>, <code>git commit</code>, <code>git revert</code>, <code>git cherry-pick</code>, <code>git merge</code>, <code>git rebase</code>, <code>git reset --hard</code>, <code>git tag</code>. Allowed: <code>git fetch</code>, <code>git status</code>, <code>git log</code>, <code>git diff</code>, <code>git show</code>, <code>git branch</code>, <code>git rev-parse</code>, <code>git merge-base</code>, <code>git config --get</code>.</p>
<p>The third is <strong>write operations on PRs and issues</strong>. The agent can read your GitHub state but can't act on it. Blocked: <code>gh pr create</code>, <code>gh pr edit</code>, <code>gh pr merge</code>, <code>gh pr close</code>, <code>gh pr comment</code>, <code>gh pr review</code>, <code>gh issue create</code>, <code>gh issue edit</code>, <code>gh issue close</code>, <code>gh issue comment</code>, <code>gh release create</code>, <code>gh repo create</code>, <code>gh repo edit</code>, <code>gh repo delete</code>. Allowed: <code>gh pr view</code>, <code>gh pr list</code>, <code>gh pr diff</code>, <code>gh pr checks</code>, <code>gh issue view</code>, <code>gh issue list</code>, <code>gh release view</code>.</p>
<p>The fourth is <strong>workflow and automation control</strong>. These are the surfaces where a compromised or misled agent could do the most damage. Blocked: <code>gh workflow run</code>, <code>gh run rerun</code>, <code>gh run cancel</code>, <code>gh secret</code>, <code>gh variable</code>, <code>gh auth</code>, <code>gh ssh-key</code>, <code>gh gpg-key</code>, and the unrestricted <code>gh api</code>.</p>
<p>For shell commands the agent legitimately needs to run, like build and test commands, allowlist specific patterns: <code>pnpm test</code>, <code>pnpm lint</code>, <code>pnpm format:check</code>, <code>pnpm build</code>, <code>pnpm vitest</code>. Anything outside the allowed list requires human confirmation. These are your own settings&nbsp;– I've just mentioned what I prefer.</p>
<p>The pattern is simple: read-only by default, write-allowed only for the specific commands you have explicitly approved. The agent can investigate, plan, and recommend. It can't ship.</p>
<p>With the structure in place and the guardrails set, the system started doing its job. What I didn't expect was how much better it would get over the months that followed.</p>
<h2 id="heading-the-compounding-loop-that-made-the-real-difference">The Compounding Loop That Made the Real Difference</h2>
<p>When we started, the AI reviewer was useful but not transformative. It caught some obvious issues, missed plenty of subtle ones, and produced a fair amount of noise.</p>
<p>The first month, my review burden dropped by 35%. The time I was spending on PR checking was reduced to 1/3, almost. Decent, not life-changing.</p>
<p>What changed over time wasn't the tool. It was the rules.</p>
<p>Every time a PR creator and reviewer caught something the AI had missed, we were adding bullets to the relevant rules file. Every time the AI flagged something useful that turned out to be a recurring pattern, the agent's own suggestion at the end of the review went into the file.</p>
<p>After a few days, the rules files had grown into something that captured a meaningful fraction of the team's collective review knowledge, written down in a place every agent on the team would read.</p>
<p>The catch rate went up. The noise went down because the rules also said what was acceptable and what we already considered solved. New engineers stopped getting the same comments on their first three PRs because the AI caught the comments first. Engineers joining the team didn't have to absorb the conventions through six months of review feedback. They installed the project, opened it in their editor, and the agent already knew.</p>
<p>This is the part most teams miss when they evaluate AI PR review tools. They look at the catch rate today and decide whether the tool is worth the price. The catch rate today isn't the right number. The right number is what the catch rate looks like in six months, after the rules file has absorbed every recurring mistake your team has made.</p>
<p>A single rule written down today saves a small amount of review time. Over a hundred PRs, it saves more. After a year, the rules file is a written-down version of a tech lead's accumulated taste. We've switched between Claude Code, the GitHub Copilot CLI, and Cursor for various tasks during this period. The AI tool changes, but the rules file in the repo stays the same.</p>
<p>The discipline that makes this work is treating the rules file as living documentation. Every recurring review comment is a candidate for promotion into the file. If you catch yourself typing the same feedback in two different PRs, that's a rule that belongs in <code>pr-rules/</code>. The "suggest a bullet" instruction in the review command is what makes this practical: the AI does the typing, the human does the deciding.</p>
<p>This is also what made me realise the system was worth the time it took to set up. The PR review command, on its own, is useful but unremarkable. The compounding loop is what turns it into infrastructure.</p>
<h2 id="heading-starting-from-zero-on-an-existing-project">Starting From Zero on an Existing Project</h2>
<p>If you've read this far and feel like the gap between your project and what I just described is a sprint of work, that's the most common reaction. It's also not correct.</p>
<p>The blank <code>AGENTS.md</code> is intimidating, especially on an existing codebase. You know your team has a thousand conventions, and writing a thousand rules sounds like a project that takes weeks before it produces any value.</p>
<p>The honest answer is that you can't write all the rules up front, and you shouldn't try. The first version of any of these files should take an afternoon, not a sprint.</p>
<p>Here's how I would actually start.</p>
<p>Run <code>/init</code> (or your tool's equivalent). In Claude Code, <code>/init</code> scans the project, infers the obvious shape (language, framework, entry points, build commands), and writes an initial <code>CLAUDE.md</code>. The output is a starting point, not a finished file. Read it, delete most of what it generates, and keep the bones.</p>
<p>Then add three things, each one bullet long.</p>
<p>First, an architecture rule. Pick the single most important convention your team enforces. For us, that was the four-layer pattern. The bullet was: "Controllers must not call repository functions directly. They must go through the app layer."</p>
<p>Second, a discoverability rule. Pick the single most important shared resource the team has, the one new code is most likely to duplicate. For us, that was the design system. The bullet was: "Before creating a new UI component, check <code>/src/design-system/</code> first."</p>
<p>Third, a "do not touch" rule. Pick the single most dangerous file or area in the codebase. Auth, billing, or migrations whichever has the most production risk. The bullet was: "Do not modify files in <code>/auth/</code> without human approval."</p>
<p>That's enough to start. Three rules, ten minutes of writing, and most of your team's recurring AI mistakes start to drop.</p>
<p>If even three rules feels like too much, start with one. Pick a single line that matters in your codebase and write it down.</p>
<p>"No <code>any</code> types in TypeScript." "Always use the enum, never compare against the string literal." "Run the linter before opening a PR." It doesn't have to be sophisticated. It doesn't have to cover edge cases. It just has to capture one piece of judgement that lives in your head today and would otherwise stay there.</p>
<p>Tomorrow, add another. The first week, you might catch 5% of the recurring mistakes. By 20 or 30 PRs in, you might catch 20-30%. The rules file doesn't need to be impressive on day one. It needs to exist and keep growing.</p>
<p>This is the compounding effect I'll come back to soon, and it's the reason this approach works on real projects rather than just in theory.</p>
<p>From there, the file grows the same way it would grow for any team. Every review catch becomes a candidate rule. After a few weeks, you have ten or fifteen rules. After a few months, you have a real review system.</p>
<p>The mistake is trying to write the perfect file on day one. The right file is the one you start with and keep editing.</p>
<h2 id="heading-what-still-needs-human-review">What Still Needs Human Review</h2>
<p>This system doesn't replace human review, and it shouldn't be allowed to.</p>
<p>The AI reviewer catches what the rules describe, plus a fair number of obvious things it would have spotted anyway. It doesn't catch problems that depend on context the rules don't capture. It doesn't catch product judgement. It doesn't catch the question of whether the change should have been built at all.</p>
<p>It also has an important blind spot when reviewing AI-authored code. The reviewer shares the same training data and reasoning patterns as the agent that wrote the code. If the original agent missed the v1/v2 distinction because it had no way to see the migration timeline, an AI reviewer reading the same diff has the same problem. Two AIs in a review loop are not two independent reviewers. They share blind spots.</p>
<p>That is why the AI reviewer in this setup never approves a PR. It produces a structured review that goes into the PR description. A human still reads the change and approves it. The AI is the first pass, not the gate.</p>
<p>Accountability also has to live with a human. When something the AI approved breaks production, someone has to own the post-mortem and decide what changes are needed for next time. The AI can't be that person. What it can do, well, is reduce the stack of small mistakes a human reviewer has to find before they get to the harder questions.</p>
<h2 id="heading-a-two-week-setup-plan">A Two-Week Setup Plan</h2>
<p>If you want to set this up for your own team, here's a concrete plan that fits in a couple of weeks. None of this needs to happen in a single push.</p>
<h3 id="heading-day-1-bootstrap-the-memory-file">Day 1: Bootstrap the memory file.</h3>
<p>Run <code>/init</code> (or your tool's equivalent) at the root of the project. Read the generated <code>CLAUDE.md</code> (or <code>AGENTS.md</code>). Delete most of it. Keep the tech stack and project structure sections.</p>
<p>Add the three rules from the previous section: one architecture rule, one discoverability rule, and one "do not touch" rule. Decide whether you want both files or a symlink.</p>
<h3 id="heading-day-2-add-per-service-files-for-your-highest-risk-areas">Day 2: Add per-service files for your highest-risk areas</h3>
<p>Pick the two or three areas of the codebase that change most often or carry the most risk. Add an <code>AGENTS.md</code> to each, following the same lean pattern. Include the architectural pattern for that area, the naming conventions, where to find good test examples, and pointers to any existing docs. Skip anything that doesn't need to be there yet.</p>
<h3 id="heading-day-3-set-up-the-directory-structure-and-guardrails">Day 3: Set up the directory structure and guardrails</h3>
<p>Create a <code>.claude/</code> folder (or your tool's equivalent) at the root, with <code>commands/</code> and <code>pr-rules/</code> subfolders. Add a <code>settings.json</code> with the deny list categories from the guardrails section. Test that the agent can't read a <code>.env</code> file, run <code>git push</code>, or create a PR. If any of those work, fix the settings before doing anything else.</p>
<h3 id="heading-day-4-write-the-pr-review-command">Day 4: Write the PR review command</h3>
<p>Adapt the command in this article to your structure. Include the diff scoping, the rule loading, the output format, and the "suggest a new rule" instruction at the end. Run it on a branch you've already merged, and tune the output until it's useful.</p>
<h3 id="heading-day-5-run-it-on-real-prs">Day 5: Run it on real PRs</h3>
<p>Have one or two engineers run the command on their next PRs before opening them. Read the output. Note what it caught, what it missed, and what was noise. Add the missing catches to the rules files. The first week is mostly tuning.</p>
<h3 id="heading-week-2-roll-out-and-document">Week 2: Roll out and document</h3>
<p>Once the command produces useful output reliably, ask the whole team to run it before opening PRs and paste the output into the PR description. Add a short section to your contributing guide explaining the workflow. Set a recurring item in your team's rituals to review the rules files monthly and trim anything that has gone stale.</p>
<p>That gets you to a working system. From there, the maintenance is incremental. Every recurring review comment becomes a candidate rule. Every architectural decision becomes a candidate update to the relevant <code>AGENTS.md</code>. The system improves as a side effect of the work the team is already doing.</p>
<h2 id="heading-what-is-working-what-i-am-still-improving">What Is Working, What I Am Still Improving</h2>
<p>Here's my honest assessment after a few months of running this:</p>
<h3 id="heading-whats-working">What's Working</h3>
<p>My review burden is meaningfully smaller. Engineers fix most of the easy mistakes before I see the PR. The "Verified" section of the AI's output tells me what to skip past. New engineers ramp faster because the conventions live in a place their tooling reads. The rules files have grown into something I would actually use to onboard someone new.</p>
<h3 id="heading-what-isnt-finished">What Isn't Finished</h3>
<p>The AI still misses problems that depend on context, and the rules don't capture them. The rules files grow, but they also need pruning, and we haven't been disciplined about that.</p>
<p>We're still figuring out how to handle rules that apply only conditionally. Docs are helping in that case, but we need to keep those up to date. And no system survives a determined engineer who skips the workflow or docs when they're in a rush.</p>
<p>There's no shortcut here. The work is real, ongoing, and mostly about discipline. The discipline is treating your codebase as something the AI needs to learn, and treating every recurring review comment as something that should be written down once instead of typed thirty times. If you're willing to do that, the tools take care of the rest.</p>
<p>If you take three things from this article, take these.</p>
<ol>
<li><p>First, don't pay for a generic reviewer to do a job your codebase needs to inform. Generic reviewers catch generic problems. Most of your real review work is specific to your team.</p>
</li>
<li><p>Second, put the rules in a file the AI reads, not in your head. <code>AGENTS.md</code>, <code>CLAUDE.md</code>, per-service files, per-area rules files. Pick a structure and stick to it.</p>
</li>
<li><p>Third, treat every human review catch as a chance to update the rules. The compounding effect over months is the entire point. A review system that improves itself is worth more than any single tool.</p>
</li>
</ol>
<p>That's the system. It took a couple of weeks to build the foundation and a few months for the rules to mature. It costs very little to run, and it has done more for our PR throughput than any tool I evaluated.</p>
<h2 id="heading-sources">Sources</h2>
<ul>
<li><p>CircleCI's 2026 State of Software Delivery report, analysing more than 28 million CI workflows from over 22,000 organisations: <a href="https://circleci.com/resources/2026-state-of-software-delivery/">https://circleci.com/resources/2026-state-of-software-delivery/</a></p>
</li>
<li><p>CircleCI's blog post detailing the year-over-year throughput numbers, including the 59% feature branch growth and the main branch decline: <a href="https://circleci.com/blog/five-takeaways-2026-software-delivery-report/">https://circleci.com/blog/five-takeaways-2026-software-delivery-report/</a></p>
</li>
<li><p>GitHub announcement of Copilot's transition to usage-based billing on June 1, 2026: <a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/">https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/</a></p>
</li>
<li><p>GitHub changelog confirming Copilot code review will start consuming GitHub Actions minutes on June 1, 2026: <a href="https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/">https://github.blog/changelog/2026-04-27-github-copilot-code-review-will-start-consuming-github-actions-minutes-on-june-1-2026/</a></p>
</li>
<li><p>AGENTS.md, the open standard's official site, including its stewardship under the Agentic AI Foundation and the Linux Foundation: <a href="https://agents.md/">https://agents.md/</a></p>
</li>
<li><p>Anthropic's Claude Code documentation on the memory system, including CLAUDE.md, auto memory, and the /init command: <a href="https://code.claude.com/docs/en/memory">https://code.claude.com/docs/en/memory</a></p>
</li>
<li><p>Anthropic's Claude Code GitHub Actions documentation, including notes on token-based billing and recommended cost controls: <a href="https://code.claude.com/docs/en/github-actions">https://code.claude.com/docs/en/github-actions</a></p>
</li>
<li><p>CodeRabbit's pricing documentation, confirming the per-developer-per-month seat model: <a href="https://docs.coderabbit.ai/management/plans">https://docs.coderabbit.ai/management/plans</a></p>
</li>
<li><p>Greptile's March 2026 pricing announcement, introducing the base-plus-usage model at $30 per seat per month with 50 included reviews: <a href="https://www.greptile.com/blog/greptile-v4">https://www.greptile.com/blog/greptile-v4</a></p>
</li>
<li><p>HumanLayer's write-up on writing a good CLAUDE.md, including data on instruction-following degradation: <a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md">https://www.humanlayer.dev/blog/writing-a-good-claude-md</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Changed the Economics of Writing Clean Code ]]>
                </title>
                <description>
                    <![CDATA[ If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing." And honestly? It was a fair point. You'd write t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-changed-the-economics-of-writing-clean-code/</link>
                <guid isPermaLink="false">69f0bce210a70b3335bf635a</guid>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ best practices ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aaron Yong ]]>
                </dc:creator>
                <pubDate>Tue, 28 Apr 2026 13:57:54 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ecb13bda-70dd-437a-8d9a-4ef8b18ccc05.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've ever wanted to add an interface to a codebase and gotten pushback, you already know the argument: "That's twice the code for the same thing."</p>
<p>And honestly? It was a fair point. You'd write the contract — the interface, the abstract class, the protocol — and then write the implementation. Two files where one would do. That's more surface area, more indirection, and more to maintain.</p>
<p>The Ruby and Rails communities built an entire philosophy around this: convention over configuration, less ceremony, fewer keystrokes. If the framework could infer your intent, why spell it out?</p>
<p>Then AI happened.</p>
<p>I was recently chatting with a CEO about what current-generation software engineers get wrong, and he put it cleanly:</p>
<blockquote>
<p>"Abstract interfaces were challenging a few months ago just because it required twice as much code. But with AI, lines of code are free. The reason we still need such constructs is because at some point a human still needs to look at the code. Interfaces reduce the cognitive load."</p>
</blockquote>
<p>That framing stuck with me. The cost of writing code has collapsed. The cost of reading it hasn't moved. And that asymmetry changes everything about how you should think about abstraction.</p>
<p>Here's what I mean.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</a></p>
</li>
<li><p><a href="#heading-the-greats-already-knew-this">The Greats Already Knew This</a></p>
</li>
<li><p><a href="#heading-the-economics-have-flipped">The Economics Have Flipped</a></p>
</li>
<li><p><a href="#heading-the-data-backs-it-up">The Data Backs It Up</a></p>
</li>
<li><p><a href="#heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</a></p>
</li>
<li><p><a href="#heading-what-this-means-for-you">What This Means for You</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-your-brain-is-the-bottleneck">Your Brain Is the Bottleneck</h2>
<p>This isn't a vibes argument. There's actual neuroscience behind why interfaces help.</p>
<p>In 1988, educational psychologist John Sweller introduced Cognitive Load Theory. A <a href="https://dl.acm.org/doi/full/10.1145/3483843">2022 ACM review</a> covers how it's been applied to computing education since.</p>
<p>The short version: your brain juggles three types of load when processing information. <em>Intrinsic</em> load is the inherent difficulty of the problem itself. <em>Extraneous</em> load is the noise — poorly organized information, unnecessary details, bad naming. <em>Germane</em> load is the good stuff — the mental effort you spend building useful mental models.</p>
<p>Here's the kicker: your working memory can only hold a handful of chunks of information at a time — cognitive scientists typically estimate somewhere between 2 and 6. Not 2 to 6 files, or 2 to 6 classes — 2 to 6 <em>things</em>.</p>
<p>Felienne Hermans explores this in <em>The Programmer's Brain</em> (2021), arguing that design patterns act as chunking aids. When you recognize a Strategy pattern, your brain collapses an entire class hierarchy into a single cognitive unit. The word "Strategy" replaces five classes and their relationships. That's not hand-waving about clean code — that's how human memory actually works.</p>
<p>And we can literally see it on brain scans. In 2021, a team led by Norman Peitek and Janet Siegmund published <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">an fMRI study on program comprehension</a> that won the ACM SIGSOFT Distinguished Paper Award at ICSE.</p>
<p>They put developers in brain scanners and watched what happened when they read code. The finding: semantic-level comprehension — understanding <em>what</em> code does — required measurably less neural activation than bottom-up syntactic parsing — tracing <em>how</em> it does it.</p>
<p>An interface lets you comprehend at the semantic level. <code>UserRepository.findById(id)</code> tells you everything you need to know without opening the implementation. Your brain doesn't need to hold the SQL query, the connection pool logic, the error handling, and the result mapping in working memory simultaneously. The interface compresses all of that into one chunk.</p>
<p>That's not elegance. That's neuroscience.</p>
<h2 id="heading-the-greats-already-knew-this">The Greats Already Knew This</h2>
<p>The case for abstraction isn't new. The people who built the foundations of computer science were making this argument before most of us were born.</p>
<p>Dijkstra said it with precision:</p>
<blockquote>
<p><em>"The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise."</em></p>
</blockquote>
<p>Abstraction isn't about hiding things from people who can't handle complexity. It's about creating a level of discourse where you can reason clearly.</p>
<p>David Parnas formalized information hiding in his <a href="https://dl.acm.org/doi/10.1145/361598.361623">1972 ACM paper</a>: <em>"Every module is characterized by its knowledge of a design decision which it hides from all others."</em> He proved that decomposing systems by design decisions (rather than processing steps) produced modules that were both more flexible <em>and</em> easier to understand. Comprehensibility wasn't a bonus — it was the design criterion.</p>
<p>Tony Hoare argued that abstraction is the most powerful tool available to the human intellect — a way to manage complexity by focusing on what matters and ignoring what doesn't. Martin Fowler brought it down to earth:</p>
<blockquote>
<p><em>"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."</em></p>
</blockquote>
<p>And then there's John Ousterhout, whose book <em>A Philosophy of Software Design</em> (2018) makes the connection to cognitive load explicit. His central argument: more lines of code can actually be <em>simpler</em> if they reduce cognitive load.</p>
<p>His concept of <em>deep modules</em> — simple interfaces hiding complex implementations — is essentially the argument that interfaces are worth their weight in code. The Unix file system API (<code>open</code>, <code>close</code>, <code>read</code>, <code>write</code>, <code>lseek</code>) is five functions hiding an enormous amount of complexity. That's a deep module. That's the goal.</p>
<p>The Gang of Four put it first in their book for a reason. Page one: <em>"Program to an interface, not an implementation."</em></p>
<p>None of this is controversial. But it's easy to forget when your AI tool just generated 200 lines of perfectly functional inline code in three seconds.</p>
<h2 id="heading-the-economics-have-flipped">The Economics Have Flipped</h2>
<p>Here's where the CEO's insight becomes an economic argument.</p>
<p>The historical case against interfaces was always about <em>writing cost</em>. Interfaces meant more code to write, more files to create, more boilerplate to maintain. The entire dynamic typing movement — Python, Ruby, JavaScript — was partly a reaction to the ceremony that languages like Java imposed. Convention over configuration. Don't Repeat Yourself. Less is more.</p>
<p>But ask yourself: what exactly is the cost of writing boilerplate now?</p>
<p>GitHub's <a href="https://arxiv.org/abs/2302.06590">2022 controlled study</a> found that developers using Copilot completed tasks 55% faster. The boilerplate that used to justify skipping interfaces — the extra file, the type definitions, the method signatures — takes seconds to generate. The writing cost of an interface has effectively collapsed to zero.</p>
<p>But again, the reading cost hasn't budged.</p>
<p>Robert C. Martin argued in <em>Clean Code</em> (2008) that developers spend far more time reading code than writing it — an observation he framed as a ratio of 10 to 1.</p>
<p>You can quibble with the exact number (it's anecdotal), but the direction is consistent across studies. A <a href="https://ieeexplore.ieee.org/document/7997917/">large-scale field study</a> tracking 78 professional developers across 3,148 working hours found they spend roughly 58% of their time on program comprehension alone. New developer onboarding averages six weeks — most of which is spent understanding existing systems, not producing new ones.</p>
<p>Addy Osmani named this asymmetry perfectly. In a <a href="https://addyosmani.com/blog/comprehension-debt/">March 2026 piece</a>, he described <em>comprehension debt</em>:</p>
<blockquote>
<p>"When a developer on your team writes code, the human review process has always been a bottleneck — but a productive and educational one. Reading their PR forces comprehension. AI-generated code breaks that feedback loop. The volume is too high."</p>
</blockquote>
<p>The output looks clean, passes linting, follows conventions — precisely the signals that historically triggered merge confidence. But comprehension debt is distinct from technical debt because it accumulates invisibly — your velocity metrics, your DORA scores, your PR counts all look fine while your team's actual understanding of the codebase quietly erodes.</p>
<p>So here's the math: AI reduced the cost of writing abstractions to near zero. The cost of <em>not</em> having them — in human reading time, onboarding friction, and comprehension debt — hasn't changed at all. The break-even point for "is this interface worth it?" just shifted massively in favor of "yes."</p>
<h2 id="heading-the-data-backs-it-up">The Data Backs It Up</h2>
<p>This isn't theoretical. We have data on what happens when AI generates code without good abstractions.</p>
<p><a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">GitClear analyzed 211 million changed lines of code</a> between 2020 and 2024. Their findings: code churn — lines reverted or updated within two weeks — doubled compared to the pre-AI baseline. Copy-pasted code blocks rose from 8.3% to 12.3%. And refactoring-associated changes dropped from 25% to under 10%.</p>
<p>AI-generated code, as they put it, "resembles an itinerant contributor, prone to violate the DRY-ness of the repos visited."</p>
<p>The <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">METR study</a> (2025) found something even more striking. Experienced open-source developers <em>predicted</em> AI would make them 24% faster. They <em>perceived</em> being 20% faster while using it. They were actually 19% slower. The perception gap is the story — you <em>feel</em> productive while generating code that creates more work downstream.</p>
<p>And then there's a study from Anthropic (yes, the company that makes Claude — full disclosure). They observed 52 software engineers learning a new library. The AI-assisted group completed tasks at the same speed, but scored <a href="https://arxiv.org/abs/2601.20245">17% lower on comprehension quizzes</a> afterward — 50% versus 67%. The biggest declines were in debugging ability. You can ship code you don't understand. You can't debug code you don't understand.</p>
<p>Kent Beck <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">put it bluntly</a>: "The value of 90% of my skills just dropped to $0. The leverage for the remaining 10% went up 1000x." What that remaining 10% is, he leaves deliberately open — but it's hard to read that and not think about system design.</p>
<h2 id="heading-the-contrarian-case-and-why-it-actually-agrees">The Contrarian Case (And Why It Actually Agrees)</h2>
<p>I'd be dishonest if I didn't address the people who argue against abstraction. And some of them are very smart.</p>
<p>Casey Muratori's <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance"</a> demonstrated that polymorphism and virtual dispatch can make code 10 to 15 times slower than straightforward procedural alternatives.</p>
<p>His benchmark is real. If you're writing a game engine or a high-frequency trading system, abstract interfaces on your hot path will cost you.</p>
<p>Dan Abramov wrote <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code"</a> after watching a premature abstraction make his codebase harder to modify:</p>
<blockquote>
<p><em>"My code traded the ability to change requirements for reduced duplication, and it was not a good trade."</em></p>
</blockquote>
<p>Sandi Metz <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">put it more sharply</a>: <em>"Duplication is far cheaper than the wrong abstraction."</em></p>
<p>And Rich Hickey, in his talk <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy"</a>, draws the critical distinction: <em>simple</em> (not intertwined) is not the same as <em>easy</em> (familiar). Wrong abstractions <em>complect</em> — they braid concerns together rather than separating them.</p>
<p>Here's the thing: none of these are arguments against abstraction. They're arguments against <em>bad</em> abstraction.</p>
<p>Muratori's performance argument applies to hot paths in performance-critical systems — not to your REST API's service layer. Abramov and Metz argue against <em>premature</em> abstraction — pulling patterns out before you understand the domain. And Hickey's entire talk is a case <em>for</em> the right abstractions, the ones that genuinely decompose rather than complect.</p>
<p>The irony is that in an AI-assisted world, these arguments are <em>easier</em> to address. You can generate the explicit, unabstracted version first. Let it stabilize. Watch the patterns emerge. Then extract the abstraction — with AI handling the mechanical refactoring. The cost of the "duplicate first, abstract later" approach just dropped to near zero.</p>
<h2 id="heading-what-this-means-for-you">What This Means for You</h2>
<p>If you're writing code with AI tools — and at this point, <a href="https://survey.stackoverflow.co/2024/ai">most of us are</a> — the temptation is to let the AI produce whatever it produces and move on. It works. It passes the tests. Ship it.</p>
<p>But "it works" is table stakes. The harder question is: can the next person who opens this code understand it in under five minutes? Can <em>you</em> understand it in six months?</p>
<p>Interfaces aren't about making code prettier or satisfying some abstract (pun intended) design principle. They're compression algorithms for human cognition. They let your brain operate at the semantic level instead of the syntactic level. And now that AI has eliminated the only real cost of creating them — the boilerplate — there's no economic argument left for skipping them.</p>
<p>The rules haven't changed. The excuse has just expired.</p>
<h2 id="heading-references">References</h2>
<h3 id="heading-academic-papers">Academic Papers</h3>
<ul>
<li><p>Duran, R., Zavgorodniaia, A., &amp; Sorva, J. (2022). <a href="https://dl.acm.org/doi/full/10.1145/3483843">"Cognitive Load Theory in Computing Education Research: A Review."</a> <em>ACM Transactions on Computing Education, 22</em>(4), Article 40.</p>
</li>
<li><p>Parnas, D.L. (1972). <a href="https://dl.acm.org/doi/10.1145/361598.361623">"On the Criteria To Be Used in Decomposing Systems into Modules."</a> <em>Communications of the ACM, 15</em>(12), 1053–1058.</p>
</li>
<li><p>Peitek, N., Apel, S., Parnin, C., Brechmann, A., &amp; Siegmund, J. (2021). <a href="https://dl.acm.org/doi/10.1109/ICSE43902.2021.00056">"Program Comprehension and Code Complexity Metrics: An fMRI Study."</a> <em>ICSE 2021</em>. ACM SIGSOFT Distinguished Paper Award.</p>
</li>
<li><p>Peng, S., Kalliamvakou, E., Cihon, P., &amp; Demirer, M. (2023). <a href="https://arxiv.org/abs/2302.06590">"The Impact of AI on Developer Productivity: Evidence from GitHub Copilot."</a> <em>arXiv:2302.06590</em>.</p>
</li>
<li><p>Shen, J.H. &amp; Tamkin, A. (2026). <a href="https://arxiv.org/abs/2601.20245">"How AI Impacts Skill Formation."</a> <em>arXiv:2601.20245</em>.</p>
</li>
<li><p>Xia, X., Bao, L., Lo, D., Xing, Z., Hassan, A.E., &amp; Li, S. (2018). <a href="https://ieeexplore.ieee.org/document/7997917/">"Measuring Program Comprehension: A Large-Scale Field Study with Professionals."</a> <em>IEEE Transactions on Software Engineering, 44</em>(10), 951–976.</p>
</li>
<li><p>METR. (2025). <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">"Measuring the Impact of Early 2025 AI on Experienced Open Source Developer Productivity."</a> <em>metr.org</em>.</p>
</li>
</ul>
<h3 id="heading-talks-and-blog-posts">Talks and Blog Posts</h3>
<ul>
<li><p>Hickey, R. (2011). <a href="https://www.infoq.com/presentations/Simple-Made-Easy/">"Simple Made Easy."</a> <em>Strange Loop Conference</em>.</p>
</li>
<li><p>Beck, K. (2023). <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">"90% of My Skills Are Now Worth $0."</a> <em>Tidy First? Substack</em>.</p>
</li>
<li><p>Osmani, A. (2026). <a href="https://addyosmani.com/blog/comprehension-debt/">"Comprehension Debt: The Hidden Cost of AI-Generated Code."</a> <em>addyosmani.com</em>.</p>
</li>
<li><p>Muratori, C. (2023). <a href="https://www.computerenhance.com/p/clean-code-horrible-performance">"Clean Code, Horrible Performance."</a> <em>Computer Enhance</em>.</p>
</li>
<li><p>Abramov, D. (2020). <a href="https://overreacted.io/goodbye-clean-code/">"Goodbye, Clean Code."</a> <em>overreacted.io</em>.</p>
</li>
<li><p>Metz, S. (2016). <a href="https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction">"The Wrong Abstraction."</a> <em>sandimetz.com</em>.</p>
</li>
<li><p>GitClear. (2025). <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">"AI Assistant Code Quality in 2025."</a> <em>gitclear.com</em>.</p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ From Symptoms to Root Cause: How to Use the 5 Whys Technique ]]>
                </title>
                <description>
                    <![CDATA[ Most teams don't struggle because they can't fix problems. They struggle because they fix the wrong thing. An API fails in production. You restart the service, errors go away, and it feels resolved. U ]]>
                </description>
                <link>https://www.freecodecamp.org/news/from-symptoms-to-root-cause-how-to-use-the-5-whys-technique/</link>
                <guid isPermaLink="false">69ea4d69904b915438990f19</guid>
                
                    <category>
                        <![CDATA[ problem solving skills ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ashutosh Krishna ]]>
                </dc:creator>
                <pubDate>Thu, 23 Apr 2026 16:48:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b5dbd964-9a03-448d-92a5-92e3b4a47fef.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most teams don't struggle because they can't fix problems. They struggle because they fix the wrong thing.</p>
<p>An API fails in production. You restart the service, errors go away, and it feels resolved. Until it happens again. And again. What's happening here is simple: you're treating symptoms, not the underlying cause.</p>
<p>The <strong>5 Whys technique</strong> is a straightforward way to deal with this. It comes from the Toyota Production System and was designed to help teams dig deeper into problems instead of settling for quick fixes.</p>
<p>The idea is simple. Ask "why" repeatedly until you reach the real cause.</p>
<p>But in practice, this is where things go wrong.</p>
<p>Teams often:</p>
<ul>
<li><p>Stop too early</p>
</li>
<li><p>Assume answers without checking data</p>
</li>
<li><p>Focus on people instead of systems</p>
</li>
<li><p>Treat "five" as a rule instead of a guideline</p>
</li>
</ul>
<p>So even though the process looks structured, the outcome is still shallow.</p>
<p>In this article, we'll focus on how to actually use the 5 Whys in real situations. Not just the theory, but what it looks like when you apply it to an engineering problem.</p>
<h3 id="heading-heres-what-well-cover">Here's What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-is-the-5-whys-technique">What is the 5 Whys Technique?</a></p>
</li>
<li><p><a href="#heading-origins-of-the-5-whys-method">Origins of the 5 Whys Method</a></p>
</li>
<li><p><a href="#heading-how-to-conduct-an-effective-5-whys-analysis">How to Conduct an Effective 5 Whys Analysis</a></p>
</li>
<li><p><a href="#heading-real-world-example-applying-5-whys-in-an-engineering-scenario">Real-World Example: Applying 5 Whys in an Engineering Scenario</a></p>
</li>
<li><p><a href="#heading-when-to-use-and-when-not-to-use-5-whys">When to Use (and When Not to Use) 5 Whys</a></p>
</li>
<li><p><a href="#heading-benefits-of-the-5-whys-technique">Benefits of the 5 Whys Technique</a></p>
</li>
<li><p><a href="#heading-common-pitfalls-and-limitations">Common Pitfalls and Limitations</a></p>
</li>
<li><p><a href="#heading-tips-for-using-5-whys-effectively">Tips for Using 5 Whys Effectively</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-what-is-the-5-whys-technique">What is the 5 Whys Technique?</h2>
<p>The 5 Whys technique is a way to break down a problem by repeatedly asking why it happened, with the goal of reaching a cause that actually explains the issue and can be addressed.</p>
<p>At its core, it's not about the number five. The name can be misleading. What matters is the process of following a chain of cause and effect until the explanation stops being superficial and starts becoming useful.</p>
<p>Each answer you uncover should move you one level deeper. You start with what went wrong, then explore what led to it, and continue until you reach something that is both believable and actionable. In most real situations, that final answer is not a single event but a gap in a system, a missing check, or an assumption that was never validated.</p>
<p>The technique became widely known through the Toyota Production System, where it was used to improve processes by focusing on causes rather than quick fixes.</p>
<p>That context is important because it highlights the original intent. The goal was not just to explain problems, but to prevent them from happening again.</p>
<p>A simple example makes this clearer. Imagine a mobile app suddenly starts crashing after a release. Asking "Why?" might look like this:</p>
<ol>
<li><p>Why is the app crashing? → Because a null value is being accessed in the code.</p>
</li>
<li><p>Why is there a null value? → Because the API response is missing a required field</p>
</li>
<li><p>Why is the field missing? → Because a recent backend change made the field optional.</p>
</li>
<li><p>Why was this change not handled in the app? → Because the app assumes the field is always present.</p>
</li>
<li><p>Why was this assumption not caught earlier? → Because there are no contract tests validating API responses.</p>
</li>
</ol>
<p>At this point, the issue is no longer just "fix the null check". The deeper problem is the lack of validation between systems, which allows breaking changes to slip through.</p>
<p>A useful way to think about the 5 Whys is that it forces you to stay with the problem a little longer than you normally would. Most of the time, the first explanation feels sufficient, so it's easy to stop there. This method pushes you to go one step further, and then another, until the explanation holds up under scrutiny.</p>
<p>At the same time, it's not a rigid formula. You might reach a solid root cause in three steps, or it might take more than five. The quality of the reasoning matters more than the count.</p>
<h2 id="heading-origins-of-the-5-whys-method">Origins of the 5 Whys Method</h2>
<p>The 5 Whys method comes from the Toyota Production System, a manufacturing approach focused on continuous improvement and problem solving at the source.</p>
<p>It's often associated with Sakichi Toyoda, whose philosophy was simple: don’t just fix a problem. Understand why it happened so it doesn't happen again.</p>
<p>Inside Toyota, this wasn't treated as a formal tool or checklist. It was part of the day-to-day way of working. When something went wrong on the production line, the goal wasn't to get things running quickly and move on. The goal was to stop, investigate, and make sure the same issue wouldn't repeat.</p>
<p>That mindset is important to understand. The 5 Whys was never meant to be a rigid exercise where you ask five questions and stop. It was a way to encourage deeper thinking and accountability in processes.</p>
<p>Another key idea in the Toyota system is that problems are usually caused by processes, not people. Instead of asking "who made the mistake", the focus is on "what allowed this mistake to happen". The 5 Whys fits naturally into this approach because it pushes you toward system level causes rather than individual blame.</p>
<p>Over time, the method spread beyond manufacturing and is now used in software engineering, product teams, operations, and many other fields. The context has changed, but the core idea remains the same: if you don't understand the cause, you're likely to see the same problem again.</p>
<p>This origin story is useful not just as background, but as a reminder of intent. The value of the 5 Whys doesn't come from the questions themselves. It comes from the discipline of not settling for the first answer.</p>
<h2 id="heading-how-to-conduct-an-effective-5-whys-analysis">How to Conduct an Effective 5 Whys Analysis</h2>
<p>A 5 Whys analysis works best when it is treated as a structured way of thinking, not a checklist to rush through. The quality of the outcome depends less on how many times you ask "why" and more on how carefully you reason through each step.</p>
<p>It helps to approach it in stages, each with a clear purpose.</p>
<h3 id="heading-step-1-define-the-problem-clearly">Step 1: Define the Problem Clearly</h3>
<p>Start with a problem statement that is specific and observable. Avoid vague descriptions like "the system is slow" or "things are failing". Instead, describe what actually happened in a way that can be verified.</p>
<p>For example, "API response time exceeded 5 seconds for 30 percent of requests between 2 PM and 3 PM" is much more useful than "API is slow".</p>
<p>A clear problem statement keeps the analysis grounded. If the starting point is fuzzy, the entire chain of reasoning will drift.</p>
<h3 id="heading-step-2-ask-why-iteratively">Step 2: Ask "Why" Iteratively</h3>
<p>Once the problem is defined, begin asking why it happened. Each answer should directly address the question before it and naturally lead to the next one.</p>
<p>The key here is continuity. Every step should feel like a logical extension of the previous one. If you find yourself jumping topics or introducing unrelated explanations, it's a sign that the chain is breaking.</p>
<p>Keep going until the answers stop being immediate symptoms and start pointing toward underlying conditions or decisions.</p>
<p>Also, don't force the process to stop at five. Some problems may need fewer steps, while others may need more. What matters is reaching a point where the explanation is meaningful and actionable.</p>
<h3 id="heading-step-3-validate-each-answer-with-evidence">Step 3: Validate Each Answer with Evidence</h3>
<p>This is where many analyses go wrong. It's easy to come up with plausible answers, but plausibility is not enough.</p>
<p>Each "why" should be backed by some form of evidence. This could be logs, metrics, recent changes, or direct observation. If an answer can't be verified, treat it as a hypothesis and confirm it before moving forward.</p>
<p>Without validation, the entire analysis becomes a chain of assumptions. Even if the final answer sounds reasonable, it may not reflect reality.</p>
<h3 id="heading-step-4-identify-the-root-cause">Step 4: Identify the Root Cause</h3>
<p>A good root cause is one that explains the sequence of events and can be acted upon to prevent the issue in the future.</p>
<p>In many cases, this turns out to be a gap in a process rather than a single technical failure. It could be a missing validation step, an incomplete test, or an assumption that was never challenged.</p>
<p>If the final answer still feels like a symptom, you probably need to go one level deeper. On the other hand, if the answer points to something you can change in your system or workflow, you are likely in the right place.</p>
<h3 id="heading-step-5-define-corrective-actions">Step 5: Define Corrective Actions</h3>
<p>The analysis is only useful if it leads to meaningful action.</p>
<p>Once you've identified the root cause, the next step is to define changes that prevent the problem from happening again. These should go beyond quick fixes and address the underlying issue.</p>
<p>For example, instead of just fixing a bug, you might introduce better testing, add monitoring, or improve review processes.</p>
<p>Good corrective actions share a few traits: they're specific, practical to implement, and they directly address the root cause identified in the analysis.</p>
<h2 id="heading-real-world-example-applying-5-whys-in-an-engineering-scenario">Real-World Example: Applying 5 Whys in an Engineering Scenario</h2>
<p>To see how this works in practice, let’s walk through a realistic backend issue. The goal here is not just to reach an answer, but to show how each step builds on evidence and leads to something actionable.</p>
<h3 id="heading-the-problem">The Problem:</h3>
<p>Users report intermittent failures while fetching order details:</p>
<pre><code class="language-bash">GET /api/orders/{id}
→ HTTP 500 Internal Server Error
</code></pre>
<p>Application logs show:</p>
<pre><code class="language-plaintext">// Java 21 example (Spring Boot style logging)
logger.error("Database connection timeout while fetching order", ex);
</code></pre>
<p>At this point, it's tempting to conclude that the database is the problem. But that's only what we can see on the surface.</p>
<h3 id="heading-applying-the-5-whys">Applying the 5 Whys</h3>
<h4 id="heading-1-why-did-the-api-return-a-500-error">1. Why did the API return a 500 error?</h4>
<p>Because the database query timed out.</p>
<p>This is directly supported by the error logs, so we can treat it as a confirmed fact.</p>
<h4 id="heading-2-why-did-the-query-time-out">2. Why did the query time out?</h4>
<p>Because the database connection pool was exhausted.</p>
<p>Metrics show that all available connections were in use during peak traffic.</p>
<h4 id="heading-3-why-was-the-connection-pool-exhausted">3. Why was the connection pool exhausted?</h4>
<p>Because some requests were holding database connections for too long.</p>
<p>Slow query logs confirm that a subset of queries had unusually high execution times.</p>
<h4 id="heading-4-why-were-some-queries-slow">4. Why were some queries slow?</h4>
<p>Because a recently introduced feature added a query on a non-indexed column.</p>
<p>Looking at recent deployments reveals a change that introduced filtering without proper indexing.</p>
<h4 id="heading-5-why-was-an-unoptimized-query-deployed-to-production">5. Why was an unoptimized query deployed to production?</h4>
<p>Because there is no performance validation step in the development or release process.</p>
<p>There are no checks in code review or CI/CD to catch inefficient database queries before deployment.</p>
<h3 id="heading-root-cause">Root Cause</h3>
<p>The issue is not the timeout itself.</p>
<p>It's this:</p>
<blockquote>
<p>The system allows inefficient database queries to reach production without any safeguards.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/61c1acb4a90dea775da8262b/f93fb121-d5ac-45bc-8b3b-cc4f915c48a3.png" alt="f93fb121-d5ac-45bc-8b3b-cc4f915c48a3" style="display:block;margin:0 auto" width="423" height="544" loading="lazy">

<h3 id="heading-what-a-shallow-fix-would-look-like">What a Shallow Fix Would Look Like</h3>
<p>If we stopped early, we might:</p>
<ul>
<li><p>Increase the database timeout</p>
</li>
<li><p>Increase the connection pool size</p>
</li>
</ul>
<p>These might reduce the frequency of failures, but they don't solve the underlying problem.</p>
<h3 id="heading-what-a-strong-fix-looks-like">What a Strong Fix Looks Like</h3>
<p>A proper 5 Whys analysis leads to changes that improve the system:</p>
<ul>
<li><p>Add appropriate indexing for frequently queried fields</p>
</li>
<li><p>Introduce query performance checks in CI/CD pipelines</p>
</li>
<li><p>Add monitoring and alerts for slow queries</p>
</li>
<li><p>Include database considerations in code reviews</p>
</li>
</ul>
<h3 id="heading-why-this-example-matters">Why This Example Matters</h3>
<p>The difference between a shallow fix and a real solution is depth.</p>
<p>The first explanation often feels sufficient, especially under pressure. But stopping there means the issue is likely to return in a different form.</p>
<p>The value of the 5 Whys comes from following the chain all the way to something you can change in your system.</p>
<h2 id="heading-when-to-use-and-when-not-to-use-5-whys">When to Use (and When Not to Use) 5 Whys</h2>
<p>Like any problem-solving method, the 5 Whys is useful in the right context and less effective in others. Knowing when to apply it is just as important as knowing how to use it.</p>
<p>If used appropriately, it can uncover meaningful insights. If used in the wrong situation, it can lead to oversimplified or misleading conclusions</p>
<h3 id="heading-when-to-use-5-whys">When to Use 5 Whys</h3>
<p>The 5 Whys is most useful when your goal is to understand <strong>why something happened</strong>, not just to fix it and move on.</p>
<p>It works well in situations where problems are recurring or not fully explained by the first answer. For example, production incidents, repeated bugs, or issues that reappear after a quick fix are strong signals that you need deeper analysis. In these cases, the technique helps uncover what is happening beneath the surface.</p>
<p>It's also effective during retrospectives and postmortems. When a release doesn't go as expected or a sprint runs into issues, the 5 Whys helps teams move beyond observations like "this failed" and get to "why did this fail in the first place".</p>
<p>In general, use it when:</p>
<ul>
<li><p>The problem is not obvious</p>
</li>
<li><p>The issue has occurred more than once</p>
</li>
<li><p>You want to prevent recurrence, not just resolve the current instance</p>
</li>
</ul>
<h3 id="heading-when-not-to-use-5-whys">When Not to Use 5 Whys</h3>
<p>The 5 Whys has its limits, and using it in the wrong context can lead to oversimplified conclusions.</p>
<p>If a problem involves multiple interacting factors, a single chain of "why" questions may not capture the full picture. Complex systems often have several contributing causes, and forcing them into one linear explanation can hide important details. In such cases, the 5 Whys should be combined with other approaches.</p>
<p>It's also less effective when there's not enough data. If each answer is based on assumptions rather than evidence, the analysis quickly becomes unreliable. The method depends on validation at every step.</p>
<p>Another limitation is in time-critical situations. During an active incident, the priority is to restore the system. The deeper analysis should happen later, once things are stable.</p>
<p>Finally, if your goal is quantitative analysis or optimization, the 5 Whys alone isn't enough. You'll need more data-driven methods to support decision making.</p>
<p>A simple rule of thumb is this. If you are trying to <strong>learn from a problem</strong>, use the 5 Whys. If you are trying to <strong>fix something immediately or analyze complex data</strong>, use it carefully or alongside other techniques.</p>
<h2 id="heading-benefits-of-the-5-whys-technique">Benefits of the 5 Whys Technique</h2>
<p>The 5 Whys technique is simple, but it offers several powerful benefits that can help you solve problems more effectively and make lasting improvements. Here are the key advantages:</p>
<h3 id="heading-simple-and-easy-to-apply">Simple and Easy to Apply</h3>
<p>One of the biggest strengths of the 5 Whys is how easy it is to start using. You don't need special tools, training, or complex frameworks. It can be applied in a quick discussion, during debugging, or as part of a formal postmortem.</p>
<p>This low barrier makes it accessible across teams, regardless of experience level.</p>
<h3 id="heading-encourages-deeper-thinking">Encourages Deeper Thinking</h3>
<p>The method naturally pushes you to go beyond the first explanation. Instead of reacting to what's visible, it encourages you to question why the problem occurred in the first place.</p>
<p>This shift from surface-level fixes to deeper understanding often leads to better decisions.</p>
<h3 id="heading-promotes-system-level-improvements">Promotes System-Level Improvements</h3>
<p>When used correctly, the focus moves away from individual people and toward systems. Instead of asking who made a mistake, the analysis asks what allowed the mistake to happen.</p>
<p>This leads to improvements in processes, safeguards, and overall system design rather than one-off fixes.</p>
<h3 id="heading-works-well-in-team-settings">Works Well in Team Settings</h3>
<p>Because the approach is simple, it's easy for multiple people to contribute. Different perspectives help uncover gaps that might otherwise be missed.</p>
<p>It also creates a shared understanding of the problem, which is valuable during retrospectives and incident reviews.</p>
<h3 id="heading-helps-prevent-recurring-issues">Helps Prevent Recurring Issues</h3>
<p>Quick fixes often solve the immediate problem but don't stop it from happening again. The 5 Whys helps identify underlying causes, which makes it easier to prevent similar issues in the future.</p>
<p>Over time, this leads to more stable systems and fewer repeated incidents.</p>
<h2 id="heading-common-pitfalls-and-limitations">Common Pitfalls and Limitations</h2>
<p>While the 5 Whys technique is useful, it’s not always perfect. There are some limitations to keep in mind, so you can use it effectively and know when it might not be enough.</p>
<h3 id="heading-stopping-too-early">Stopping Too Early</h3>
<p>One of the most common mistakes is ending the analysis after the first or second answer. These early answers usually describe symptoms, not causes.</p>
<p>Stopping too soon leads to fixes that address the surface but leave the underlying issue unresolved.</p>
<h3 id="heading-treating-assumptions-as-facts">Treating Assumptions as Facts</h3>
<p>It's easy to come up with explanations that sound reasonable. But without evidence, they're just assumptions.</p>
<p>If each step isn't validated with logs, metrics, or observations, the entire analysis can drift away from reality.</p>
<h3 id="heading-focusing-on-individuals-instead-of-systems">Focusing on Individuals Instead of Systems</h3>
<p>Answers like "someone made a mistake" don't add much value. While they may be true, they don't explain why the system allowed that mistake to have an impact.</p>
<p>Focusing on processes and safeguards leads to more meaningful improvements.</p>
<h3 id="heading-oversimplifying-complex-problems">Oversimplifying Complex Problems</h3>
<p>The 5 Whys follows a linear chain of reasoning, but real-world systems often have multiple contributing factors.</p>
<p>Relying on a single chain can hide important interactions. In such cases, the method should be combined with other approaches.</p>
<h3 id="heading-treating-it-as-a-rigid-formula">Treating It as a Rigid Formula</h3>
<p>The name suggests asking "why" five times, but this shouldn't be taken literally. Some problems require fewer steps, while others need more.</p>
<p>Forcing the structure can lead to artificial or weak conclusions.</p>
<h3 id="heading-not-a-replacement-for-deeper-analysis">Not a Replacement for Deeper Analysis</h3>
<p>The 5 Whys isn't designed for every type of problem. For complex system failures, performance optimization, or data-heavy investigations, additional tools and methods are often required.</p>
<p>It works best as a starting point or a complement to other techniques, not a complete solution on its own.</p>
<h2 id="heading-tips-for-using-5-whys-effectively">Tips for Using 5 Whys Effectively</h2>
<p>To get the most out of the 5 Whys technique, there are a few tips that can help you use it effectively. These will guide you to ask the right questions and reach useful, actionable insights.</p>
<h3 id="heading-start-with-a-clear-specific-problem">Start with a Clear, Specific Problem</h3>
<p>A vague problem leads to vague answers. Spend a little extra time making sure the problem statement is precise and based on observable facts. This keeps the analysis grounded and avoids unnecessary detours.</p>
<h3 id="heading-base-every-step-on-evidence">Base Every Step on Evidence</h3>
<p>Treat each answer as something that needs to be verified. Use logs, metrics, recent changes, or direct observations to support your reasoning. If something can't be validated, call it out as a hypothesis and confirm it before moving forward.</p>
<h3 id="heading-keep-the-chain-logical-and-connected">Keep the Chain Logical and Connected</h3>
<p>Each "why" should naturally follow from the previous answer. If the reasoning starts to jump between unrelated ideas, pause and re-evaluate. A clean, logical chain is a strong indicator that you're on the right track.</p>
<h3 id="heading-focus-on-systems-not-individuals">Focus on Systems, Not Individuals</h3>
<p>Avoid stopping at explanations that point to human error. Instead, ask what allowed that error to have an impact. This shift in thinking leads to improvements that actually reduce the chances of similar issues in the future.</p>
<h3 id="heading-do-not-force-exactly-five-steps">Do Not Force Exactly Five Steps</h3>
<p>The number five is a guideline, not a rule. Some problems become clear in three steps, while others need more exploration. Stop when you reach a cause that's both convincing and actionable.</p>
<h3 id="heading-involve-the-right-people">Involve the Right People</h3>
<p>If possible, do the analysis as a group. People from different parts of the system bring different perspectives, which helps uncover details that might otherwise be missed. It also creates shared ownership of both the problem and the solution.</p>
<h3 id="heading-turn-insights-into-actions">Turn Insights into Actions</h3>
<p>The analysis only matters if it leads to change. Make sure the final outcome includes clear, practical steps that address the root cause. Without this, even a well-done analysis has limited impact.</p>
<h2 id="heading-summary">Summary</h2>
<p>The 5 Whys is a simple technique, but using it well takes some discipline.</p>
<p>At its core, it's about resisting the urge to stop at the first explanation. By following the chain of cause and effect, you move from symptoms to something you can actually fix. In many cases, that turns out to be a gap in a process rather than a one-off failure.</p>
<p>When applied thoughtfully, it helps teams learn from problems instead of just reacting to them. Over time, this leads to better systems, fewer recurring issues, and more confidence in how problems are handled.</p>
<p>The key is to treat it as a way of thinking, not just a set of steps.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Storyteller: A Medium For Guiding Others Through Code ]]>
                </title>
                <description>
                    <![CDATA[ As a computer science instructor, I have long wished that there was a better way to guide others through my code. When I was first learning to program, I was a big fan of traditional programming books ]]>
                </description>
                <link>https://www.freecodecamp.org/news/storyteller-a-medium-for-guiding-others-through-code/</link>
                <guid isPermaLink="false">69a23fd4d4053a09f35c3d3e</guid>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ coding ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ code playbacks ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mark Mahoney ]]>
                </dc:creator>
                <pubDate>Sat, 28 Feb 2026 01:07:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/902c2299-ea98-4136-8ee8-36668f0c08ee.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a computer science instructor, I have long wished that there was a better way to guide others through my code. When I was first learning to program, I was a big fan of traditional programming books. I have shelves and shelves of 800+ page books covering different programming languages and technologies.</p>
<p>I have known for a while now that most learners today don't share my love of big thick books, and to be honest, I rarely read those books in their entirety. Those big books often had a lot more exposition about the code than was probably needed. As a book buyer I wanted to make sure that I was getting my money's worth so the thicker they were, the better. It is much more common these days for learners to consume blog based tutorials and videos.</p>
<p>If you're learning to code right now, you've probably experienced the frustration of these formats too. I want to share something I've been working on that might help.</p>
<h2 id="heading-blogs-and-videos"><strong>Blogs and Videos</strong></h2>
<p>Blog-style tutorials mix code and the explanation of it in a top-to-bottom fashion. Scrolling through these web-based explanations feels familiar and one can copy and paste with ease. However, linking the explanation of the code and the code itself has always been less than ideal. Often I find myself jumping around the blog post wishing I could see the entire code example while working through the explanation. Instead, I am only able to see small parts of the code and it is challenging to see how those parts relate to other parts.</p>
<p>Video tutorials are very popular these days. They solve some of the problems associated with blog-style tutorials. Videos are great because you get two streams of information: the author's audible narrative and the code being written. A viewer can focus on the two streams simultaneously. However, videos have some problems too.</p>
<h3 id="heading-viewing-videos"><strong>Viewing Videos</strong></h3>
<p>From the perspective of the viewer, videos are hard to search through and are not useful as a copy and paste source or a code reference. More importantly, though, they discourage the viewer from taking their time and reflecting on the material. Often, when I am viewing a video tutorial I don't pause and let concepts sink in before the video moves on. Yes, I could be more disciplined and pause and rewind more often but usually I don't.</p>
<h3 id="heading-making-videos"><strong>Making videos</strong></h3>
<p>From the perspective of the video creator, it is clear that not all code being developed is interesting to watch. Some of it is not really worth showing the viewer. Not all video creators can keep the narrative interesting the whole time.</p>
<p>I know I struggle with the 'performance' aspect of making videos (you won't find me coding on Twitch anytime soon). Many times after I am done making a video, as I review it, I wish I had mentioned something that I forgot. It is hard to go back and edit the video without scrapping it and starting over.</p>
<h2 id="heading-storyteller"><strong>Storyteller</strong></h2>
<p>I have created a new medium to guide viewers through code examples. It combines the best of books, blog posts, and videos. This new medium allows a developer to write code using a top-notch editor (Visual Studio Code) and then replay the development of that code in the browser.</p>
<p>The author can add comments at important points in the evolution of the code. The comments can include text, hand drawn pictures, screenshots, and audio and video recordings. This allows the author to add visualizations that we have in our heads but don't make it into the code itself. The tool is called <a href="https://github.com/markm208/storyteller">Storyteller</a>.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/67df75cfc82238bba0f330b3/82dcb5c8-999f-432f-bd60-adcb3d8b9889.png" alt="82dcb5c8-999f-432f-bd60-adcb3d8b9889" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Here are a few examples of a 'playback':</p>
<ul>
<li><p><a href="https://playbackpress.com/books/pybook/chapter/2/10">Enlarging a Picture (Python)</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/cppbook/chapter/8/8">Dynamic Variables and Pointers (C++)</a></p>
</li>
</ul>
<p>These work best on a big screen. If you are viewing a playback on a small screen you can view it in 'blog' mode (there is button in the top right to switch from 'code' mode to 'blog' mode).</p>
<p>I have created groups of these guided code walk-throughs to help me teach different topics to my students. These are all free and hosted on a website I created called <a href="https://playbackpress.com/books">Playback Press</a>. Here are some of the 'books' I have created so far:</p>
<ul>
<li><p><a href="https://playbackpress.com/books/cppbook/">An Animated Introduction to Programming in C++</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/pybook/">An Animated Introduction to Programming with Python</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/webdevbook/">An Introduction to Web Development from Back to Front</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/cljbook/">An Animated Introduction to Clojure</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/exbook/">An Animated Introduction to Elixir</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/sqlbook/">Database Design and SQL for Beginners</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/flutterbook/">Mobile App Development with Dart and Flutter</a></p>
</li>
<li><p><a href="https://playbackpress.com/books/patternsbook/">OO Design Patterns with Java</a></p>
</li>
</ul>
<p>I usually assign these as readings in my classes instead of using expensive textbooks. It is a lot easier for me to write several programs than it is to find a perfect textbook.</p>
<p>I also use them for in-class demos instead of writing code live. This makes code demos flow much faster and smoother. If I make an interesting mistake while preparing the code I can still highlight it with a comment. If I make an uninteresting or embarrassing mistake I can just ignore it and the students won't focus on it.</p>
<h3 id="heading-the-advantages-of-code-playbacks"><strong>The Advantages of Code Playbacks:</strong></h3>
<ul>
<li><p>The primary focus is on the code. It is always visible and easy to search and navigate.</p>
</li>
<li><p>Since the code is so accessible, the explanation of it tends to be short and concise.</p>
</li>
<li><p>The narrative can include whiteboard style drawings, screenshots, or videos of running code in addition to a text explanation.</p>
</li>
<li><p>As an author, I can review the code several times and add/edit comments each time I go through it. I don't have to give a perfect performance like I do with a video.</p>
</li>
<li><p>Comment points highlight when the author wants the viewer to take a moment to really think about the code and reflect on it. The playback only moves forward when the viewer is ready.</p>
</li>
<li><p>The code mentioned in a comment can be highlighted so the viewer knows exactly where they should be looking.</p>
</li>
<li><p>The code can be downloaded at any point in the playback. Then a viewer can run it, change it, and add to it.</p>
</li>
<li><p>The tool is a language independent editor plug-in and can be used to describe programs in any language.</p>
</li>
<li><p>Viewers only need a web browser to go through a playback.</p>
</li>
</ul>
<p>Recently, I've been exploring how to make playbacks even more useful for learners.</p>
<h2 id="heading-ai-as-an-infinitely-patient-tutor"><strong>AI as an Infinitely Patient Tutor</strong></h2>
<p>I have extended code playbacks to include an AI tutor. One thing I've learned in my years of teaching is that students often hesitate to ask questions. They worry about looking foolish, or they don't want to slow down the class, or they simply can't articulate what's confusing them.</p>
<p>What if every student had access to a patient tutor who never got frustrated with repeated questions and could explain concepts in multiple ways until something clicked?</p>
<p>I've integrated AI directly into the playback experience. As students work through a playback, they can ask questions about anything they're seeing. This might be a specific line of code, a concept I mentioned in a comment, or how something connects to material from earlier in the playback. The AI has full context. It can see the code, it understands where the student is in the playback, and it can provide explanations tailored to that exact moment. The AI is right there <em>with</em> the student, looking at the same code, understanding the same context.</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/WAPql5KZFR4?si=jFnCqidSTtfaZA4e" frameborder="0" allowfullscreen="" title="Embedded content" loading="lazy"></iframe></div>

<p>The AI can also generate self-grading multiple choice questions based on the code and comments in a playback. These low-stakes quizzes make the learning experience more engaging and help learners check their understanding as they go.</p>
<p>Let me be clear: the AI doesn't replace me as an instructor. I still create the playbacks. I still decide what concepts to cover, what order to present them, and what examples best illustrate the ideas. The AI is an extension of my teaching, not a replacement for it.</p>
<p>Note: The AI features are available to registered users on <a href="https://playbackpress.com/books">Playback Press</a>. Registration is free but logging in is required to access the AI tutor. If you want to see what this feels like, try one of the playbacks linked above and ask the AI a question about what you're seeing.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>My goal has always been to help people learn to code. Books gave us depth but demanded commitment. Blogs gave us accessibility but fragmented the code. Videos gave us narrative but took away control. Playbacks keep the code front and center while letting learners move at their own pace and reflect when they need to. Adding AI doesn't change that philosophy, it just means there's always someone available to answer questions. Together, they get closer to the experience of having an expert sit beside you and walk you through a program. That's what I've been trying to build, and I think we're getting there.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use the Singleton Design Pattern in Flutter: Lazy, Eager, and Factory Variations ]]>
                </title>
                <description>
                    <![CDATA[ In software engineering, sometimes you need only one instance of a class across your entire application. Creating multiple instances in such cases can lead to inconsistent behavior, wasted memory, or resource conflicts. The Singleton Design Pattern i... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-singleton-design-pattern-in-flutter-lazy-eager-and-factory-variations/</link>
                <guid isPermaLink="false">69740b7bc3e68b8de44a179f</guid>
                
                    <category>
                        <![CDATA[ Singleton Design Pattern ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Object Oriented Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ design patterns ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ood ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ flutter development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Factory Design Pattern ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Mobile Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwaseyi Fatunmole ]]>
                </dc:creator>
                <pubDate>Fri, 23 Jan 2026 23:59:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769212761076/11d41d2a-8efa-4ddb-9ee2-218f5be00d9f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In software engineering, sometimes you need only one instance of a class across your entire application. Creating multiple instances in such cases can lead to inconsistent behavior, wasted memory, or resource conflicts.</p>
<p>The Singleton Design Pattern is a creational design pattern that solves this problem by ensuring that a class has exactly one instance and provides a global point of access to it.</p>
<p>This pattern is widely used in mobile apps, backend systems, and Flutter applications for managing shared resources such as:</p>
<ul>
<li><p>Database connections</p>
</li>
<li><p>API clients</p>
</li>
<li><p>Logging services</p>
</li>
<li><p>Application configuration</p>
</li>
<li><p>Security checks during app bootstrap</p>
</li>
</ul>
<p>In this article, we'll explore what the Singleton pattern is, how to implement it in Flutter/Dart, its variations (eager, lazy, and factory), and physical examples. By the end, you'll understand the proper way to use this pattern effectively and avoid common pitfalls.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-the-singleton-pattern">What is the Singleton Pattern?</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-when-to-use-the-singleton-pattern">When to Use the Singleton Pattern</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-singleton-class">How to Create a Singleton Class</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-eager-singleton">Eager Singleton</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-lazy-singleton">Lazy Singleton</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-choosing-between-eager-and-lazy">Choosing Between Eager and Lazy</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-factory-constructors-in-the-singleton-pattern">Factory Constructors in the Singleton Pattern</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-factory-constructors">What Are Factory Constructors?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-singleton-with-factory-constructor">Implementing Singleton with Factory Constructor</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-when-not-to-use-a-singleton">When Not to Use a Singleton</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-singletons-can-be-problematic">Why Singletons Can Be Problematic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-scenarios-where-you-should-avoid-singletons">Scenarios Where You Should Avoid Singletons</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-general-guidelines">General Guidelines</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before diving into this tutorial, you should have:</p>
<ol>
<li><p>Basic understanding of the Dart programming language</p>
</li>
<li><p>Familiarity with Object-Oriented Programming (OOP) concepts, particularly classes and constructors</p>
</li>
<li><p>Basic knowledge of Flutter development (helpful but not required)</p>
</li>
<li><p>Understanding of static variables and methods in Dart</p>
</li>
<li><p>Familiarity with the concept of class instantiation</p>
</li>
</ol>
<h2 id="heading-what-is-the-singleton-pattern">What is the Singleton Pattern?</h2>
<p>The Singleton pattern is a creational design pattern that ensures a class has only one instance and that there is a global point of access to the instance.</p>
<p>Again, this is especially powerful when managing shared resources across an application.</p>
<h3 id="heading-when-to-use-the-singleton-pattern">When to Use the Singleton Pattern</h3>
<p>You should use a Singleton when you are designing parts of your system that must exist once, such as:</p>
<ol>
<li><p>Global app state (user session, auth token, app config)</p>
</li>
<li><p>Shared services (logger, API client, database connection)</p>
</li>
<li><p>Resource heavy logic (encryption handlers, ML models, cache manager)</p>
</li>
<li><p>Application boot security (run platform-specific root/jailbreak checks)</p>
</li>
</ol>
<p>For example, in a Flutter app, Android may check developer mode or root status, while iOS checks jailbroken device state. A Singleton security class is a perfect way to enforce that these checks run once globally during app startup.</p>
<h2 id="heading-how-to-create-a-singleton-class">How to Create a Singleton Class</h2>
<p>We have two major ways of creating a singleton class:</p>
<ol>
<li><p>Eager Instantiation</p>
</li>
<li><p>Lazy Instantiation</p>
</li>
</ol>
<h3 id="heading-eager-singleton">Eager Singleton</h3>
<p>This is where the Singleton is created at load time, whether it's used or not.</p>
<p>In this case, the instance of the singleton class as well as any initialization logic runs at load time, regardless of when this class is actually needed or used. Here's how it works:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EagerSingleton</span> </span>{
  EagerSingleton._internal();
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> EagerSingleton _instance = EagerSingleton._internal();

  <span class="hljs-keyword">static</span> EagerSingleton <span class="hljs-keyword">get</span> instance =&gt; _instance;

  <span class="hljs-keyword">void</span> sayHello() =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">"Hello from Eager Singleton"</span>);
}

<span class="hljs-comment">//usage</span>
<span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// Accessing the singleton globally</span>
  EagerSingleton.instance.sayHello();
}
</code></pre>
<h4 id="heading-how-the-eager-singleton-works">How the Eager Singleton Works</h4>
<p>Let's break down what's happening in this implementation:</p>
<p>First, <code>EagerSingleton._internal()</code> is a private named constructor (notice the underscore prefix). This prevents external code from creating new instances using <code>EagerSingleton()</code>. The only way to get an instance is through the controlled mechanism we're about to define.</p>
<p>Next, <code>static final EagerSingleton _instance = EagerSingleton._internal();</code> is the key line. This creates the single instance immediately when the class is first loaded into memory. Because it's <code>static final</code>, it belongs to the class itself (not any particular instance) and can only be assigned once. The instance is created right here, at declaration time.</p>
<p>The <code>static EagerSingleton get instance =&gt; _instance;</code> getter provides global access to that single instance. Whenever you call <code>EagerSingleton.instance</code> anywhere in your code, you're getting the exact same object that was created when the class loaded.</p>
<p>Finally, <code>sayHello()</code> is just a regular method to demonstrate that the singleton works. You could replace this with any business logic your singleton needs to perform.</p>
<p>When you run the code in <code>main()</code>, the class loads, the instance is created immediately, and <code>EagerSingleton.instance.sayHello()</code> accesses that pre-created instance to call the method.</p>
<h4 id="heading-pros">Pros:</h4>
<ol>
<li><p>This is simple and thread safe, meaning it's not affected by concurrency, especially when your app runs on multithreads.</p>
</li>
<li><p>It's ideal if the instance is lightweight and may be accessed frequently.</p>
</li>
</ol>
<h4 id="heading-cons">Cons:</h4>
<ol>
<li>If this instance is never used through the runtime, it results in wasted memory and could impact application performance.</li>
</ol>
<h3 id="heading-lazy-singleton">Lazy Singleton</h3>
<p>In this case, the singleton instance is only created when the class is called or needed in runtime. Here, a trigger needs to happen before the instance is created. Let's see an example:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LazySingleton</span> </span>{
  LazySingleton._internal(); 
  <span class="hljs-keyword">static</span> LazySingleton? _instance;

  <span class="hljs-keyword">static</span> LazySingleton <span class="hljs-keyword">get</span> instance {
    _instance ??= LazySingleton._internal();
    <span class="hljs-keyword">return</span> _instance!;
  }

  <span class="hljs-keyword">void</span> sayHello() =&gt; <span class="hljs-built_in">print</span>(<span class="hljs-string">"Hello from LazySingleton"</span>);
}

<span class="hljs-comment">//usage </span>
<span class="hljs-keyword">void</span> main() {
  <span class="hljs-comment">// Accessing the singleton globally</span>
  LazySingleton.instance.sayHello();
}
</code></pre>
<h4 id="heading-how-the-lazy-singleton-works">How the Lazy Singleton Works</h4>
<p>The lazy implementation differs from eager in one crucial way: timing.</p>
<p>Again, <code>LazySingleton._internal()</code> is a private constructor that prevents external instantiation.</p>
<p>But notice that <code>static LazySingleton? _instance;</code> is declared as nullable and not initialized. Unlike the eager version, no instance is created at load time. The variable simply exists as <code>null</code> until it's needed.</p>
<p>The magic happens in the getter: <code>_instance ??= LazySingleton._internal();</code> uses Dart's null-aware assignment operator. This line says "if <code>_instance</code> is null, create a new instance and assign it. Otherwise, keep the existing one." This is the lazy initialization: the instance is only created the first time someone accesses it.</p>
<p>The first time you call <code>LazySingleton.instance</code>, <code>_instance</code> is null, so a new instance is created. Every subsequent call finds that <code>_instance</code> already exists, so it just returns that same instance.</p>
<p>The <code>return _instance!;</code> uses the null assertion operator because we know <code>_instance</code> will never be null at this point (we just ensured it's not null in the previous line).</p>
<p>This approach saves memory because if you never call <code>LazySingleton.instance</code> in your app, the instance never gets created.</p>
<h4 id="heading-pros-1">Pros:</h4>
<ol>
<li><p>Saves application memory, as it only creates what is needed in runtime.</p>
</li>
<li><p>Avoids memory leaks.</p>
</li>
<li><p>Is ideal for resource heavy objects while considering application performance.</p>
</li>
</ol>
<h4 id="heading-cons-1">Cons:</h4>
<ol>
<li>Could be difficult to manage in multithreaded environments, as you have to ensure thread safety while following this pattern.</li>
</ol>
<h3 id="heading-choosing-between-eager-and-lazy">Choosing Between Eager and Lazy</h3>
<p>Now that we've broken down these two major types of singleton instantiation, it's worthy of note that you'll need to be intentional while deciding whether to create a singleton the eager or lazy way. Your use case/context should help you determine what singleton pattern you need to apply during object creation.</p>
<p>As an engineer, you need to ask yourself these questions when using a singleton for object creation:</p>
<ol>
<li><p>Do I need this class instantiated when the app loads?</p>
</li>
<li><p>Based on the user journey, will this class always be needed during every session?</p>
</li>
<li><p>Can a user journey be completed without needing to call any logic in this class?</p>
</li>
</ol>
<p>These three questions will determine what pattern (eager or lazy) you should use to fulfill best practices while maintaining scalability and high performance in your application.</p>
<h2 id="heading-factory-constructors-in-the-singleton-pattern">Factory Constructors in the Singleton Pattern</h2>
<p>Applying factory constructors in the Singleton pattern can be powerful if you use them properly. But first, let's understand what factory constructors are.</p>
<h3 id="heading-what-are-factory-constructors">What Are Factory Constructors?</h3>
<p>A factory constructor in Dart is a special type of constructor that doesn't always create a new instance of its class. Unlike regular constructors that must return a new instance, factory constructors can:</p>
<ol>
<li><p>Return an existing instance (perfect for singletons)</p>
</li>
<li><p>Return a subclass instance</p>
</li>
<li><p>Apply logic before deciding what to return</p>
</li>
<li><p>Perform validation or initialization before returning an object</p>
</li>
</ol>
<p>The <code>factory</code> keyword tells Dart that this constructor has the flexibility to return any instance of the class (or its subtypes), not necessarily a fresh one.</p>
<h3 id="heading-implementing-singleton-with-factory-constructor">Implementing Singleton with Factory Constructor</h3>
<p>This allows you to apply initialization logic while your class instance is being created before returning the instance.</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FactoryLazySingleton</span> </span>{
  FactoryLazySingleton._internal();
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FactoryLazySingleton _instance = FactoryLazySingleton._internal();

  <span class="hljs-keyword">static</span> FactoryLazySingleton <span class="hljs-keyword">get</span> instance =&gt; _instance;

  <span class="hljs-keyword">factory</span> FactoryLazySingleton() {
    <span class="hljs-comment">// Your logic runs here</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Factory constructor called"</span>);
    <span class="hljs-keyword">return</span> _instance;
  }
}
</code></pre>
<h4 id="heading-how-the-factory-constructor-singleton-works">How the Factory Constructor Singleton Works</h4>
<p>This implementation combines aspects of both eager and lazy patterns with additional control.</p>
<p>The <code>FactoryLazySingleton._internal()</code> private constructor and <code>static final _instance</code> create an eager singleton. The instance is created immediately when the class loads.</p>
<p>The <code>static get instance</code> provides the traditional singleton access pattern we've seen before.</p>
<p>But the interesting part is the <code>factory FactoryLazySingleton()</code> constructor. This is a public constructor that looks like a normal constructor call, but behaves differently. When you call <code>FactoryLazySingleton()</code>, instead of creating a new instance, it runs whatever logic you've placed inside (in this case, a print statement), then returns the existing <code>_instance</code>.</p>
<p>This pattern is powerful because:</p>
<ol>
<li><p>You can log when someone tries to create an instance</p>
</li>
<li><p>You can validate conditions before returning the instance</p>
</li>
<li><p>You can apply configuration based on parameters passed to the factory</p>
</li>
<li><p>You can choose to return different singleton instances based on conditions</p>
</li>
</ol>
<p>For example, you might have different configuration singletons for development vs production:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">factory</span> FactoryLazySingleton({<span class="hljs-built_in">bool</span> isProduction = <span class="hljs-keyword">false</span>}) {
  <span class="hljs-keyword">if</span> (isProduction) {
    <span class="hljs-comment">// Apply production configuration</span>
    _instance.configure(productionSettings);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Apply development configuration</span>
    _instance.configure(devSettings);
  }
  <span class="hljs-keyword">return</span> _instance;
}
</code></pre>
<h4 id="heading-pros-2">Pros</h4>
<ol>
<li><p>You can add logic before returning an instance</p>
</li>
<li><p>You can cache or reuse the same object</p>
</li>
<li><p>You can dynamically return a subtype if needed</p>
</li>
<li><p>You avoid unnecessary instantiation</p>
</li>
<li><p>You can inject configuration or environment logic</p>
</li>
</ol>
<h4 id="heading-cons-2">Cons</h4>
<ol>
<li><p>Adds slight complexity compared to simple getter access</p>
</li>
<li><p>The factory constructor syntax might confuse developers unfamiliar with the pattern</p>
</li>
<li><p>If overused with complex logic, it can make debugging harder</p>
</li>
<li><p>Can create misleading code where <code>FactoryLazySingleton()</code> looks like it creates a new instance but doesn't</p>
</li>
</ol>
<h2 id="heading-when-not-to-use-a-singleton">When Not to Use a Singleton</h2>
<p>While singletons are powerful, they're not always the right solution. Understanding when to avoid them is just as important as knowing when to use them.</p>
<h3 id="heading-why-singletons-can-be-problematic">Why Singletons Can Be Problematic</h3>
<p>Singletons create global state, which can make your application harder to reason about and test. They introduce tight coupling between components that shouldn't necessarily know about each other, and they can make it difficult to isolate components for unit testing.</p>
<h3 id="heading-scenarios-where-you-should-avoid-singletons">Scenarios Where You Should Avoid Singletons</h3>
<p>Avoid using the Singleton pattern if:</p>
<h4 id="heading-you-need-multiple-independent-instances">You need multiple independent instances</h4>
<p>If different parts of your app need their own separate configurations or states, singletons force you into a one-size-fits-all approach.</p>
<p>For example, if you're building a multi-tenant application where each tenant needs isolated data, a singleton would cause data to bleed between tenants.</p>
<p><strong>Alternative</strong>: Use dependency injection to pass different instances to different parts of your app. Each component receives the specific instance it needs through its constructor or a service locator.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Instead of singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserRepository</span> </span>{
  <span class="hljs-keyword">final</span> DatabaseConnection db;
  UserRepository(<span class="hljs-keyword">this</span>.db); 
}

<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">final</span> dbForTenantA = DatabaseConnection(tenantId: <span class="hljs-string">'A'</span>);
<span class="hljs-keyword">final</span> dbForTenantB = DatabaseConnection(tenantId: <span class="hljs-string">'B'</span>);
<span class="hljs-keyword">final</span> repoA = UserRepository(dbForTenantA);
<span class="hljs-keyword">final</span> repoB = UserRepository(dbForTenantB);
</code></pre>
<h4 id="heading-your-architecture-avoids-shared-global-state">Your architecture avoids shared global state</h4>
<p>Modern architectural patterns like BLoC, Provider, or Riverpod in Flutter specifically aim to avoid global mutable state. Singletons work against these patterns by reintroducing global state.</p>
<p><strong>Alternative</strong>: Use state management solutions designed for Flutter. Provider, Riverpod, BLoC, or GetX offer better ways to share data across your app while maintaining testability and avoiding tight coupling.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Using Provider instead of singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{
  <span class="hljs-keyword">final</span> <span class="hljs-built_in">String</span> apiUrl;
  AppConfig(<span class="hljs-keyword">this</span>.apiUrl);
}

<span class="hljs-comment">// Provide it at the top level</span>
<span class="hljs-keyword">void</span> main() {
  runApp(
    Provider&lt;AppConfig&gt;(
      create: (_) =&gt; AppConfig(<span class="hljs-string">'https://api.example.com'</span>),
      child: MyApp(),
    ),
  );
}

<span class="hljs-comment">// Access it anywhere in the widget tree</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyWidget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">final</span> config = Provider.of&lt;AppConfig&gt;(context);

  }
}
</code></pre>
<h4 id="heading-it-forces-tight-coupling-between-unrelated-classes">It forces tight coupling between unrelated classes</h4>
<p>When multiple unrelated classes depend on the same singleton, they become indirectly coupled. Changes to the singleton affect all these classes, making the codebase fragile and hard to refactor.</p>
<p><strong>Alternative</strong>: Use interfaces and dependency injection. Define what behavior you need through an interface, then inject implementations. This way, classes depend on abstractions, not concrete singletons.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Define an interface</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message);
}

<span class="hljs-comment">// Implementation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ConsoleLogger</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message) =&gt; <span class="hljs-built_in">print</span>(message);
}

<span class="hljs-comment">// Classes depend on the interface, not a singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PaymentService</span> </span>{
  <span class="hljs-keyword">final</span> Logger logger;
  PaymentService(<span class="hljs-keyword">this</span>.logger);

  <span class="hljs-keyword">void</span> processPayment() {
    logger.log(<span class="hljs-string">'Processing payment'</span>);
  }
}

<span class="hljs-comment">// Easy to test with mock</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MockLogger</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
  <span class="hljs-built_in">List</span>&lt;<span class="hljs-built_in">String</span>&gt; logs = [];
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> log(<span class="hljs-built_in">String</span> message) =&gt; logs.add(message);
}
</code></pre>
<h4 id="heading-you-need-clean-isolated-testing">You need clean, isolated testing</h4>
<p>Singletons maintain state between tests, causing test pollution where one test affects another. This makes tests unreliable and order-dependent.</p>
<p><strong>Alternative</strong>: Use dependency injection and create fresh instances for each test. Most testing frameworks support this pattern, allowing you to inject mocks or fakes easily.</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Testable code</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderService</span> </span>{
  <span class="hljs-keyword">final</span> PaymentProcessor processor;
  OrderService(<span class="hljs-keyword">this</span>.processor);
}

<span class="hljs-comment">// In tests</span>
<span class="hljs-keyword">void</span> main() {
  test(<span class="hljs-string">'processes order successfully'</span>, () {
    <span class="hljs-keyword">final</span> mockProcessor = MockPaymentProcessor();
    <span class="hljs-keyword">final</span> service = OrderService(mockProcessor); 

  });
}
</code></pre>
<h3 id="heading-general-guidelines">General Guidelines</h3>
<p>Use singletons sparingly and only when you truly need exactly one instance of something for the entire application lifecycle. Good candidates include logging systems, application-level configuration, and hardware interface managers.</p>
<p>For most other cases, prefer dependency injection, state management solutions, or simply passing instances where needed. These approaches make your code more flexible, testable, and maintainable.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Singleton pattern is a powerful creational tool, but like every tool, you should use it strategically.</p>
<p>Overusing singletons can make apps tightly coupled, hard to test, and less maintainable.</p>
<p>But when used correctly, the Singleton pattern helps you save memory, enforce consistency, and control object lifecycle beautifully.</p>
<p>The key is understanding your specific use case and choosing the right implementation approach – whether eager, lazy, or factory-based – that best serves your application's needs while maintaining clean, testable code.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Vibe Coding Effectively as a Dev ]]>
                </title>
                <description>
                    <![CDATA[ It may seem like everyone is a vibe coder these days, and prompting seemed like it would become the new coding. But is this AI-generated code really deployable? Bragging on social media about a clever script is one thing, but pushing a vibe coded app... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-vibe-coding-effectively-as-a-dev/</link>
                <guid isPermaLink="false">6925deb0b459e862808eb04c</guid>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vibe coding ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ankur Tyagi ]]>
                </dc:creator>
                <pubDate>Tue, 25 Nov 2025 16:52:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764089459731/0122c0b7-08e2-434a-b5eb-518025401951.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>It may seem like everyone is a vibe coder these days, and prompting seemed like it would become the new coding. But is this AI-generated code really deployable?</p>
<p>Bragging on social media about a clever script is one thing, but pushing a vibe coded app to prod comes with many security risks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758881769141/9bedc585-5608-4660-a304-bbb10f10b8f2.png" alt="Vibe-debug, vibe-refactor, and vibe-check" class="image--center mx-auto" width="4276" height="3094" loading="lazy"></p>
<p>With so many AI dev tools out there now, <a target="_blank" href="https://www.freecodecamp.org/news/how-to-perform-code-reviews-in-tech-the-painless-way/">code reviews</a> become more critical than ever.</p>
<p>This article will explore what <strong>vibe coding</strong> means and how code reviews should adapt in the era of AI.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents:</strong></h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-vibe-coding">What is Vibe Coding?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-vibe-coding-in-practice">How to Implement Vibe Coding in Practice</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-isnt-vibe-coded-output-production-ready">Why isn’t Vibe Coded Output Production Ready?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-context-gaps-are-the-first-crack">Context gaps are the first crack.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-those-gaps-lead-directly-to-integration-blind-spots">Those gaps lead directly to integration blind spots.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-most-serious-risk-is-security-by-omission">The most serious risk is security by omission.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-and-correctness-evidence-are-thin">Testing and correctness evidence are thin.</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-operability-lags-behind">Operability lags behind.</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-guidelines-for-ai-code-reviews">Guidelines for AI Code Reviews</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-code-review-process-in-vibe-coding">Code Review Process in Vibe Coding</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-checklist-for-reviewing-ai-generated-code">Checklist for Reviewing AI Generated Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-work-effectively-with-ai-tools">How to Work Effectively with AI Tools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-is-vibe-coding">What is Vibe Coding?</h2>
<p>In early 2025, AI researcher <a target="_blank" href="https://x.com/karpathy">Andrej Karpathy</a> popularized the term vibe coding to describe a new way of development in which you “fully give in to the vibes” and let AI write code while you focus on high level intent.</p>
<p>A developer expresses their desired functionality in plain language, and an AI system (like an <a target="_blank" href="https://en.wikipedia.org/wiki/Large_language_model">LLM</a>) generates the source code to implement it.</p>
<p>This code-by-prompt approach allows even beginners to produce working code without deep knowledge of programming languages. Karpathy joked that with advanced IDE agents (like <a target="_blank" href="https://www.devtoolsacademy.com/blog/cursor-vs-windsurf/">Cursor’s</a> Composer mode), “I barely even touch the keyboard... I ‘Accept All’ always, I don’t read the diffs anymore... and it mostly works”.</p>
<p>So, vibe coding is coding by vibe and trusting AI to handle the heavy lifting.</p>
<h2 id="heading-how-to-implement-vibe-coding-in-practice">How to Implement Vibe Coding in Practice</h2>
<p>In practice, vibe coding usually involves using AI assistants and adapting your workflow to a more interactive, prompt-driven style.</p>
<p>Here’s an overview of how you can “vibe code” a project:</p>
<h3 id="heading-step-1-choose-an-ai-assistant">Step 1: Choose an AI assistant</h3>
<p>Select a development env that supports AI code generation. Popular choices include <a target="_blank" href="https://cursor.com/">Cursor</a> and <a target="_blank" href="https://github.com/features/copilot">GitHub Copilot</a>.</p>
<h3 id="heading-step-2-define-your-requirements">Step 2: Define your requirements</h3>
<p>Instead of writing boilerplate code, describe what you want to build. Provide AI with a specific prompt detailing functionality. The more <a target="_blank" href="https://www.philschmid.de/context-engineering">context</a> and detail you give, the better AI can fulfill your intent.</p>
<p>For example, when I ran an SEO inspection for my website, DevTools Academy, I used this prompt in Cursor:</p>
<blockquote>
<p>“Now, act as a senior product engineer and UX strategist. Evaluate and improve <a target="_blank" href="https://www.devtoolsacademy.com">https://www.devtoolsacademy.com</a> with a practical, no-fluff lens.</p>
<p>Scope:</p>
<ul>
<li><p>UX</p>
</li>
<li><p>SEO and technical SEO</p>
</li>
<li><p>Positioning and messaging</p>
</li>
<li><p>Copywriting and information architecture</p>
</li>
<li><p>What to add to stand out in the developer tools space.”</p>
</li>
</ul>
</blockquote>
<p>This prompt works well because it gives the AI a clear role, a defined scope, and a specific intent. AI knows it’s not just fixing SEO but also reviewing how the site communicates value to devs. That combination of clarity and context produces actionable insights instead of surface-level suggestions.</p>
<p>Below is a screenshot of that audit in progress and showing how I reviewed code, metadata, and UX recommendations side by side.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761258218099/91e93726-1a7d-4d1a-9839-531355037dfc.png" alt="cursor screenshot showing CodeRabbit reviewing a pull request with comments and summary." class="image--center mx-auto" width="4276" height="2680" loading="lazy"></p>
<p>You can checkout the full code on my open source <a target="_blank" href="https://github.com/tyaga001/devtoolsacademy">blog</a> here and check out closed PRs. This will help you learn how I use all these coding agents on a production ready app.</p>
<h3 id="heading-step-3-review-the-code">Step 3: Review the code</h3>
<p>AI will produce initial code based on your prompt. Think of this as a prototype – it’s not perfect. Run the code and see how it behaves.</p>
<p>Let’s look at an example: here, CodeRabbit is reviewing one of my <a target="_blank" href="https://github.com/tyaga001/devtoolsacademy/pull/145">pull requests</a> on GitHub. I had pushed a small fix to sort blog posts correctly and make sure the RSS feed reflects the latest publish date. Within seconds, CodeRabbit analyzed the diff, understood the intent behind my change, and explained exactly what the new code does.</p>
<p>It pointed out that the fix now sorts posts before mapping them, uses the sorted data for both items and the lastBuildDate, and ensures proper chronological order throughout the feed.</p>
<p>It’s like having a senior reviewer who not only checks syntax but also validates logic and confirms that your reasoning holds up.</p>
<p><a target="_blank" href="https://github.com/tyaga001/devtoolsacademy/pull/145"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758879621613/95bee1e7-3953-4416-b48b-e844332be950.png" alt="GitHub pull request showing CodeRabbit review comments on code changes with highlighted fixes." class="image--center mx-auto" width="4276" height="2644" loading="lazy"></a></p>
<p>This is just a reminder to expect imperfections. Vibe coding embraces a <em>“code first, refine later”</em> mindset. This means you get a working version quickly, then iteratively improve it. You might go through a few cycles of prompt -&gt; code -&gt; test -&gt; tweak.</p>
<h3 id="heading-step-4-validate-debug-polish">Step 4: Validate, debug, polish</h3>
<p>Once AI generated code meets your expectations, do a final review.</p>
<p>Throughout the process, the core idea is that you collaborate with the AI. The AI agent serves as a coding assistant, making real-time suggestions, automating tedious boilerplate, and even generating entire modules on your behalf.</p>
<h2 id="heading-why-isnt-vibe-coded-output-production-ready">Why Isn’t Vibe Coded Output Production Ready?</h2>
<p>Vibe coding moves fast: you describe intent, the AI produces something that runs, and you’re off to the next prompt. What’s missing is the slow, unglamorous work that usually turns a draft into shippable software, like shared context, architectural alignment, verification, and documentation.</p>
<p>AI generates plausible code based on patterns it has seen. But it doesn’t understand your team’s history, your system’s constraints, or the implicit rules that keep everything coherent over time.</p>
<p>That mismatch shows up the moment a “works on my machine” demo meets a real codebase.</p>
<p>Let’s explore the common pitfalls of vibe-coded code, so you’ll know what to watch for. Then, in the checklist section below, I’ll outline practical strategies to address or prevent each issue.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758271815928/5f763a0f-2dda-4318-8c19-0c9e58447abe.png" alt="AI is Limited by Context" class="image--center mx-auto" width="740" height="520" loading="lazy"></p>
<h3 id="heading-context-gaps-are-the-first-crack">Context gaps are the first crack.</h3>
<p>AI only sees what you show it, so it’s easy for it to make the right local choice and the wrong global one: duplicating logic that already exists, choosing defaults that conflict with prior decisions, or introducing functions that don’t respect domain boundaries.</p>
<p>The result is code that looks reasonable in isolation but collides with existing assumptions and conventions once integrated.</p>
<h3 id="heading-those-gaps-lead-directly-to-integration-blind-spots">Those gaps lead directly to integration blind spots.</h3>
<p>Drafts often ignore the lived details of your environment – shared utilities, cross-cutting concerns, configuration, deployment hooks, and operational policies. Interfaces may line up at a glance and still fail at runtime because the draft doesn’t fit how your system composes modules, handles errors, or manages state across services.</p>
<h3 id="heading-the-most-serious-risk-is-security-by-omission">The most serious risk is security by omission.</h3>
<p>AI rarely includes robust input validation, clear authentication and authorization paths, or rate limiting unless you spell it out. Secrets handling and logging tend to be superficial or missing. That leaves common exposure points like request handlers, job processors, and webhook endpoints without the checks that prevent injection, SSRF, mass assignment, or data exfiltration.</p>
<p>Even when the surface looks tidy, the absence of explicit security controls means you’re trusting defaults you didn’t choose.</p>
<h3 id="heading-testing-and-correctness-evidence-are-thin">Testing and correctness evidence are thin.</h3>
<p>Quality suffers in quieter ways, too. Beyond “it runs,” there’s little to demonstrate behavior across edge cases or to guard against regressions.</p>
<p>Performance and scalability remain unknowns: extra network calls, N+1 patterns, and quadratic loops sneak in because nobody measured them. Dependencies and environments drift as versions aren’t pinned, infrastructure isn’t declared, and configuration lives only in the author’s head, making behavior differ across machines and CI.</p>
<h3 id="heading-operability-lags-behind">Operability lags behind.</h3>
<p>A lack of metrics, missing health/readiness probes, and no runbook make failures harder to detect and slower to recover from. Add in data quality and compliance concerns (PII handling, encoding assumptions, transitive license obligations), and you have code that demos well but isn’t ready for production’s reliability, security, and audit demands.</p>
<p>In short, vibe-coded output accelerates drafting but skips the shared understanding and evidence that make software safe to ship.</p>
<p>Until those gaps are closed, it’s a prototype, not a release.</p>
<h2 id="heading-guidelines-for-ai-code-reviews">Guidelines for AI Code Reviews</h2>
<p>Your team should keep pre-AI engineering standards as the bar, including security, tests, readability, maintainability, performance, and docs. AI should change how fast you gather the evidence for those standards, not how much evidence you require. In other words, use AI to accelerate the path to your existing bar, never to lower it.</p>
<p>Using AI, you can generate code at speed. But if reviews take the same amount of time (or more time), you lose some of the benefit. The goal isn’t to relax standards, it’s to shorten the time to prove you met them. That means layering in automation (tests, static analysis, secret scans, SCA) and AI-assisted review to catch obvious issues quickly so human reviewers can focus on intent, architecture, and risk.</p>
<p>Well-used assistants can help here. For example, tools like CodeRabbit, GitHub Copilot PR Reviewer, Claude Code, Cursor’s Bugbot, Graphite’s AI Review, and Greptile can highlight potential bugs, security gaps, style deviations, and mismatched intent, and summarize diffs for faster context. Treat these as accelerators for your existing process, not as replacements for judgment.</p>
<h3 id="heading-code-review-process-in-vibe-coding">Code Review Process in Vibe Coding</h3>
<p>The fundamentals of good code reviews haven’t changed – and in fact, they’re more critical now.</p>
<p>Below are some key principles to maintain speed without sacrificing quality.</p>
<h4 id="heading-1-trust-but-verify">1. Trust, but verify.</h4>
<p>A reviewer usually assumes the author understands the system. With vibe-coded output, the “author” may be an AI with limited context. If something looks odd or unnecessary, question it. Run the code, add/execute tests, or ask the developer/AI for clarification on intent and constraints.</p>
<h4 id="heading-2-dont-let-reviews-become-a-bottleneck">2. Don’t let reviews become a bottleneck.</h4>
<p>Vibe coding generates code quickly. If human review takes as long as hand-writing the change, you’ve erased the gain.</p>
<p>Combat this by front-loading automation: run unit/integration tests, static analysis (lint/SAST), secret scans, SCA, and basic perf checks in CI to clear the noise. Then reviewers spend their time on design trade-offs, boundary cases, and risk. The balance is: high standards, faster evidence.</p>
<h4 id="heading-3-use-ai-code-reviews-wisely">3. Use AI code reviews wisely</h4>
<p>AI can help review code just as it helps generate it. Modern “pair reviewer” tools scan a PR and surface likely bugs, security issues, missing tests, or style violations in minutes plus give natural-language summaries of the change.</p>
<p>Tools you can consider include CodeRabbit, GitHub Copilot PR Reviewer, Claude Code, Cursor Bugbot, Graphite, and Greptile. Many integrate with the CLI/IDE and GitHub/GitLab to leave actionable comments.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758272500586/a9cc891f-ab1a-47d8-a607-a772cbaef2e0.png" alt="coderabbit CLI" class="image--center mx-auto" width="4276" height="2722" loading="lazy"></p>
<p>Think of them as fast first-pass reviewers that increase coverage and consistency across PRs.</p>
<h4 id="heading-4-human-judgment-is-still-irreplaceable">4. Human judgment is still irreplaceable.</h4>
<p>Even the best AI reviewer is an assistant. Keep humans accountable for correctness, security posture, architectural fit, and user impact. A healthy pattern is AI first-pass &gt; human second-pass that inspects invariants, failure modes, and long-term maintainability.</p>
<h4 id="heading-5-maintain-a-high-bar-for-quality">5. Maintain a high bar for quality.</h4>
<p>It’s tempting to accept “it runs” when an AI wrote it. Don’t. Stakeholders still expect software to be robust, secure, and maintainable. Keep DRY, readability, and testability standards. Insist on input validation, authZ checks where relevant, and sensible logging/metrics. If you can’t provide evidence that you met the bar, you haven’t met it.</p>
<h4 id="heading-6-educate-and-document">6. Educate and document</h4>
<p>When reviewers find bugs or security flaws in AI-generated code, capture the lesson.</p>
<p>Update internal guides with patterns like “When generating handlers, validate and bound inputs, add rate limits, log request IDs, avoid N+1 queries, and sanitize user-visible output.” Over time, bake these into prompts, templates, repo scaffolds, and CI checks so the next AI draft starts closer to done.</p>
<h2 id="heading-checklist-for-reviewing-ai-generated-code">Checklist for Reviewing AI Generated Code</h2>
<p>Before approving any vibe-coded change, make the standards explicit and verifiable. Use this checklist to confirm behavior, security, performance, integration, and documentation so the draft you got from AI becomes code you can safely ship.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1762510535966/85ea547a-f955-446b-9e22-965dc18f9e49.png" alt="Checklist for Reviewing AI Generated Code" class="image--center mx-auto" width="2696" height="2044" loading="lazy"></p>
<p>Here’s a checklist a human reviewer should go through before approving vibe-coded output:</p>
<h3 id="heading-1-define-the-codes-purpose-scope-amp-non-goals">1. Define the code’s purpose (scope &amp; non-goals).</h3>
<p>Be explicit about what this change does and does not do. Tie it to a user story/ticket and call out non-goals so “helpful” AI changes don’t creep in.</p>
<h3 id="heading-2-verify-x-and-y-behavior-and-edge-cases">2. Verify X and Y (behavior and edge cases).</h3>
<p>Be clear about what you’re verifying. For example, verify input parsing and pagination boundaries, verify that error paths return the correct status and body, and verify that database writes are idempotent. Run existing tests, add missing unit/integration tests, and reproduce edge inputs (empty, null, huge, unicode).</p>
<h3 id="heading-3-perform-code-quality-checks-readability-dry-refactor-needs">3. Perform code-quality checks (readability, DRY, refactor needs).</h3>
<p>AI often produces verbose or duplicated logic. Ensure names are meaningful, side effects are clearly stated, and duplication is removed or minimized. Run linters/formatters, collapse repetition, and extract helpers where they aid clarity.</p>
<h3 id="heading-4-analyze-organization-and-structure-make-sure-it-fits-the-architecture">4. Analyze organization and structure (make sure it fits the architecture).</h3>
<p>AI writes code in isolation. Confirm the change uses existing utilities, layers, and boundaries (domain/services/controllers/jobs). Check imports and module placement, avoid reinventing existing helpers, and align with repository conventions.</p>
<h3 id="heading-5-validate-inputs-and-assumptions-make-the-implicit-explicit">5. Validate inputs and assumptions (make the implicit explicit).</h3>
<p>List the assumptions the AI made (default locale/timezone, allowed ranges, required fields). Add schema validation (DTO/class validators/JSON Schema). Empty, null, over-max, non-ASCII, unexpected enum, malicious strings. And finally, enforce limits/timeouts.</p>
<h3 id="heading-6-perform-security-audits-minimum-pass">6. Perform security audits (minimum pass).</h3>
<ul>
<li><p><strong>AuthN/AuthZ:</strong> Confirm endpoint checks identity and authorization paths; deny-by-default.</p>
</li>
<li><p><strong>Inputs:</strong> Sanitize/validate inputs, prevent injection (SQL/NoSQL/command), and escape user-visible output.</p>
</li>
<li><p><strong>Secrets</strong>: No secrets in code/diff/logs, use env/secret manager, and rotate any test keys.</p>
</li>
<li><p><strong>Abuse controls:</strong> Add rate limits, size limits, and timeouts on network and disk operations. Run SAST/secret scan/SCA, and fix or justify findings.</p>
</li>
</ul>
<h3 id="heading-7-do-a-performance-evaluation-right-now-at-a-small-scale">7. Do a performance evaluation (right now, at a small scale).</h3>
<p>Look for N+1s, needless network calls, unbounded loops, quadratic sorts. Add a micro-benchmark or run a quick load test for hot paths. Set sensible cache/timeout/retry with jitter where applicable.</p>
<h3 id="heading-8-manage-dependencies-pin-justify-minimize">8. Manage dependencies (pin, justify, minimize).</h3>
<p>Review any new libraries. Are they necessary? Maintained? License compatible? Pin versions, add lockfiles, or remove unused transitive adds.</p>
<h3 id="heading-9-review-documentation-what-to-add-and-where">9. Review documentation (what to add and where).</h3>
<p>Ensure the docs are in line with the code. AI often changes some parts or adds code blocks at different places while resolving various issues. These changes might not make it into the docs.</p>
<h3 id="heading-10-observability-see-problems-early">10. Observability (see problems early).</h3>
<p>Use structured logs with request/trace IDs, key counters/timers (success/error/latency), health/readiness probes, and a basic dashboard or alert stub.</p>
<h3 id="heading-11-compliance-and-data-handling-when-applicable">11. Compliance and data handling (when applicable).</h3>
<p>Identify any personally identifiable information (PII), document collection/retention, ensure masking/redaction in logs, verify dependency licenses and data-residency constraints.</p>
<h2 id="heading-how-to-work-effectively-with-ai-tools">How to Work Effectively with AI Tools</h2>
<p>At this point, you can probably see why it’s very important to understand the actual skills involved in AI-assisted development.</p>
<p>There’s a pretty big difference between an experienced developer who uses AI tools to help them get more done, and a newbie who thinks AI can build the next Facebook or Google just with a simple prompt.</p>
<p>An inexperienced dev will ask AI something like "Hey, Build me Twitter and make no mistakes"</p>
<p>But an experienced developer who has a solid fundamentals might say say something like:</p>
<ul>
<li><p>"AI, we're building a Twitter replica. Use $SQL_Database, Use $Language, Avoid $Common_Pitfalls, Follow $Standard_Practices."</p>
</li>
<li><p>"The generated code is prone to X problem, implement this fix."</p>
</li>
<li><p>"Implementation of $X is flawed because of $Y, do $Z instead."</p>
</li>
</ul>
<p>So as you can see, you still need to know the how's and the why's and what depends on what. Often you’ll just need to make the changes manually, because it will be faster. And you don’t want to outsource the critical thinking part, which is the part that AI can't actually do.</p>
<p>LLMs are good at information retrieval. If you know nothing about what you’re looking for, then asking an AI isn’t going to be that helpful (or that reliable). But if you have an idea, some background knowledge/context, and the skills to verify AI’s responses, then it can be really helpful.</p>
<p>Last month, I shared in my <a target="_blank" href="https://bytesizedbets.com/">newsletter</a> how my current coding loop looks in practice.</p>
<p>I draft with Claude Code (or Copilot/Cursor), open a PR, and let an AI reviewer like CodeRabbit (or Copilot PR Reviewer / Cursor Bugbot or Greptile) do the first pass. CI runs tests and scans.</p>
<p>I repeat until everything’s green and the PR is ready to merge. It’s fast, but it’s still disciplined.</p>
<p>If you want to understand why this kind of workflow is becoming essential, read this article: <a target="_blank" href="https://bytesizedbets.com/p/era-of-ai-slop-cleanup-has-begun">Era of AI Slop Cleanup Has Begun</a>. I talk about what’s happening in AI-assisted engineering, where generating code is easy, but keeping it clean and production ready takes experience – and you must have good programming skills.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI-generated code can boost productivity – but production value still comes from software that is robust, secure, and maintainable.</p>
<p>Mindless code generation creates technical debt. But when you integrate AI thoughtfully, with guardrails, verification, tests, security checks, and documentation, you can go faster without lowering your standards.</p>
<p>That's it for this article. I hope you learned something new today.</p>
<p>If you have any questions about code reviews, engineering, startups, or business in general, please find me on Twitter: <a target="_blank" href="https://x.com/TheAnkurTyagi">@TheAnkurTyag</a>i. I’d be more than happy to discuss them.</p>
<h3 id="heading-want-to-read-more-interesting-articles-like-this">Want to read more interesting articles like this?</h3>
<p>You can read more about the latest dev tools like this one on my <a target="_blank" href="https://www.devtoolsacademy.com/">website</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Concurrency vs. Parallelism: What’s the Difference and Why Should You Care? ]]>
                </title>
                <description>
                    <![CDATA[ In software engineering, certain concepts appear deceptively simple at first glance but fundamentally shape the way we design and architect systems. Concurrency and parallelism are two such concepts that warrant careful examination. These terms are f... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/concurrency-vs-parallelism-whats-the-difference-and-why-should-you-care/</link>
                <guid isPermaLink="false">68f25bf491a578e35f37419a</guid>
                
                    <category>
                        <![CDATA[ Computer Science ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ concurrency ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Parallel Programming ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Wisdom Usa ]]>
                </dc:creator>
                <pubDate>Fri, 17 Oct 2025 15:08:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760622633358/ad43bbd8-116c-42eb-95b7-0ef70156983a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In software engineering, certain concepts appear deceptively simple at first glance but fundamentally shape the way we design and architect systems. Concurrency and parallelism are two such concepts that warrant careful examination.</p>
<p>These terms are frequently used interchangeably, even among experienced developers. But while they may sound similar and occasionally overlap in practice, they address distinctly different problems and serve separate architectural goals. Understanding this distinction is not just an academic exercise. It directly impacts how you build scalable, efficient systems.</p>
<p>Whether you’re developing a high-traffic web server, training complex machine learning models, or optimising application performance, a solid grasp of these concepts can mean the difference between a solution that merely functions and one that scales elegantly under real-world conditions.</p>
<p>This article provides a comprehensive breakdown of both concepts through visual analogies, practical examples, and technical implementations. By the end, you will be equipped to confidently apply these principles in your software projects.</p>
<h3 id="heading-heres-what-well-cover">Here’s what we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-understanding-the-fundamental-concepts">Understanding the Fundamental Concepts</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-the-kitchen-analogy">The Kitchen Analogy</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-what-concurrency-looks-like-in-practice">What Concurrency Looks Like in Practice</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-python-example-implementing-concurrency-with-asyncio">Python Example: Implementing Concurrency with asyncio</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-what-parallelism-looks-like-in-practice">What Parallelism Looks Like in Practice</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-python-example-implementing-parallelism-with-multiprocessing">Python Example: Implementing Parallelism with multiprocessing</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-concurrency-vs-parallelism-a-detailed-comparison">Concurrency vs. Parallelism: A Detailed Comparison</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-when-to-use-each">When to Use Each</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-real-world-applications-and-use-cases">Real-World Applications and Use Cases</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-concurrency-in-production-systems">Concurrency in Production Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-parallelism-in-production-systems">Parallelism in Production Systems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-hybrid-approaches">Hybrid Approaches</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-choosing-the-right-approach-for-your-problem">Choosing the Right Approach for Your Problem</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-common-pitfall-to-avoid">Common Pitfall to Avoid</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-why-this-distinction-matters-in-practice">Why This Distinction Matters in Practice</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-misconceptions-and-clarifications">Common Misconceptions and Clarifications</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-practical-implementation-strategies">Practical Implementation Strategies</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-when-implementing-concurrency">When Implementing Concurrency</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-when-implementing-parallelism">When Implementing Parallelism</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-and-technologies-by-language">Tools and Technologies by Language</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-further-learning-resources">Further Learning Resources</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-understanding-the-fundamental-concepts">Understanding the Fundamental Concepts</h2>
<p>Before diving into implementations, let’s establish some clear definitions:</p>
<p><strong>Concurrency</strong> refers to the ability of a system to manage multiple tasks within overlapping time periods. It does not necessarily mean these tasks execute at the exact same instant. Rather, concurrency is about structuring a program to handle multiple operations by interleaving their execution, often on a single processor core.</p>
<p><strong>Parallelism</strong>, by contrast, involves the simultaneous execution of multiple tasks. This typically requires multiple CPU cores or processors working in tandem, with each handling a separate portion of the workload at the same time.</p>
<h3 id="heading-the-kitchen-analogy">The Kitchen Analogy</h3>
<p>Consider the process of cooking as a helpful mental model:</p>
<p>A concurrent kitchen employs a single chef who rapidly switches between preparing multiple dishes. The chef might chop vegetables for one dish, then stir a sauce for another, then return to the first dish to continue preparation. From an observer's perspective, it appears that multiple dishes are being prepared "at once," but in reality, the chef is performing one action at a time in rapid succession.</p>
<p>A parallel kitchen has multiple chefs, each working on different dishes simultaneously. One chef prepares the appetiser while another works on the main course, and a third handles dessert. True simultaneous work is happening across multiple workers.</p>
<p>Same kitchen, different strategies, different outcomes.</p>
<h2 id="heading-what-concurrency-looks-like-in-practice">What Concurrency Looks Like in Practice</h2>
<p><a target="_blank" href="https://mechdampiitb.github.io/cs751_shyamsundar/"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760010754774/e4563560-b9b2-42e0-8c69-5c052db2656c.png" alt="Visual comparison between concurrency and parallelism in task execution. On the left, a single CPU alternates between Task 1 and Task 2 (or Thread 1 and Thread 2), illustrating concurrency without true parallelism. On the right, two CPUs execute Task 1 and Task 2 simultaneously, illustrating both concurrency and parallelism." class="image--center mx-auto" width="866" height="643" loading="lazy"></a></p>
<p>Concurrency is fundamentally about task scheduling, coordination, and resource management. It enables a program to handle multiple operations by strategically interleaving their execution, whether on a single core or across multiple threads.</p>
<p>A practical example: when you stream a video on YouTube while your device downloads a file in the background and your messaging app checks for new messages, your CPU is rapidly context-switching between these tasks. Each task gets a slice of processing time, creating the illusion of simultaneous execution even on a single-core processor.</p>
<h3 id="heading-python-example-implementing-concurrency-with-asyncio">Python Example: Implementing Concurrency with asyncio</h3>
<p>To examine concurrency in more detail, we’ll create a simple application which gets data on various APIs asynchronously. This is an example of how Python’s library, asyncio, lets us spawn multiple network operations without blocking so we can effectively use the waiting time.</p>
<p>In this implementation, we’ll be simulating API calls to a weather service, a news service, and a user profile database. Pay attention to the fact that all three requests begin nearly at the same time, yet the program doesn’t wait until one of them is completed before it begins the next one.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> asyncio

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_data_from_api</span>(<span class="hljs-params">api_name, delay</span>):</span>
    print(<span class="hljs-string">f"Starting request to <span class="hljs-subst">{api_name}</span>..."</span>)
    <span class="hljs-keyword">await</span> asyncio.sleep(delay)  <span class="hljs-comment"># Simulates network I/O wait</span>
    print(<span class="hljs-string">f"Received response from <span class="hljs-subst">{api_name}</span>"</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-string">f"Data from <span class="hljs-subst">{api_name}</span>"</span>

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_user_profile</span>(<span class="hljs-params">user_id</span>):</span>
    print(<span class="hljs-string">f"Fetching profile for user <span class="hljs-subst">{user_id}</span>..."</span>)
    <span class="hljs-keyword">await</span> asyncio.sleep(<span class="hljs-number">1.5</span>)
    print(<span class="hljs-string">f"Profile loaded for user <span class="hljs-subst">{user_id}</span>"</span>)
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"user_id"</span>: user_id, <span class="hljs-string">"name"</span>: <span class="hljs-string">"John Doe"</span>}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-comment"># All tasks start and are managed concurrently</span>
    results = <span class="hljs-keyword">await</span> asyncio.gather(
        fetch_data_from_api(<span class="hljs-string">"Weather API"</span>, <span class="hljs-number">2</span>),
        fetch_data_from_api(<span class="hljs-string">"News API"</span>, <span class="hljs-number">1</span>),
        fetch_user_profile(<span class="hljs-number">12345</span>)
    )
    print(<span class="hljs-string">"\nAll operations completed!"</span>)
    print(<span class="hljs-string">"Results:"</span>, results)

asyncio.run(main())
</code></pre>
<p><strong>What happens during execution:</strong></p>
<ol>
<li><p>All three async functions are initiated at approximately the same time.</p>
</li>
<li><p>The event loop manages their execution, switching between tasks when one is waiting (during <code>await</code> statements).</p>
</li>
<li><p>While one task waits for simulated I/O, the event loop allows other tasks to make progress.</p>
</li>
<li><p>The task with the shortest delay completes first, even though all were started together.</p>
</li>
<li><p>No task blocks the others, resulting in efficient use of the single thread.</p>
</li>
</ol>
<p><strong>Key insight:</strong> Concurrency optimises responsiveness and resource utilisation. It doesn’t inherently make individual tasks complete faster. Instead, it allows multiple tasks to make progress during the same time period, particularly when those tasks involve waiting for external resources.</p>
<h2 id="heading-what-parallelism-looks-like-in-practice">What Parallelism Looks Like in Practice</h2>
<p><a target="_blank" href="https://www.researchgate.net/figure/Parallelism-mechanism-in-OpenMP-with-multiple-CPU-threads_fig3_311454812"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760010925777/c85a1976-96da-4f74-abba-62d1d0489d60.png" alt="Diagram illustrating the execution timeline of a parallel region using OpenMP. It shows how threads are forked and joined, with timestamps at the fork (`t_f`) and join (`t_j`), individual thread creation (`t_{i,s}`) and completion (`t_{i,c}`), as well as allocated memory per CPU/thread." class="image--center mx-auto" width="850" height="460" loading="lazy"></a></p>
<p>Parallelism concerns itself with genuine simultaneous execution. This approach leverages multiple CPU cores or processors to divide work and execute portions concurrently in real time.</p>
<p>Parallelism shines when dealing with CPU-intensive operations such as mathematical computations, image processing, video rendering, or training deep learning models.</p>
<h3 id="heading-python-example-implementing-parallelism-with-multiprocessing">Python Example: Implementing Parallelism with multiprocessing</h3>
<p>To better understand parallel execution, we’re going to make a program that carries out intensive calculations in a set of cores of CPUs. The given example relies on Python and the multiprocessing module to create different processes that are executed on different processor cores.</p>
<p>To work with a sufficiently complex example, we’ll compute the sum of the squares of millions of numbers. In contrast to the concurrent code sample, where we were waiting to receive I/O, we are actually doing some CPU-intensive work. You’ll notice the reduction in the time taken to execute the work when it’s shared by a number of cores.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> multiprocessing <span class="hljs-keyword">import</span> Process, current_process
<span class="hljs-keyword">import</span> time

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">compute_heavy_task</span>(<span class="hljs-params">task_name, iterations</span>):</span>
    <span class="hljs-string">"""Simulates a CPU-intensive operation"""</span>
    process_name = current_process().name
    print(<span class="hljs-string">f"<span class="hljs-subst">{task_name}</span> started on <span class="hljs-subst">{process_name}</span>"</span>)

    <span class="hljs-comment"># Simulate CPU-bound work</span>
    result = <span class="hljs-number">0</span>
    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(iterations):
        result += i ** <span class="hljs-number">2</span>

    time.sleep(<span class="hljs-number">1</span>)  <span class="hljs-comment"># Additional simulated work</span>
    print(<span class="hljs-string">f"<span class="hljs-subst">{task_name}</span> completed on <span class="hljs-subst">{process_name}</span>. Result: <span class="hljs-subst">{result}</span>"</span>)
    <span class="hljs-keyword">return</span> result

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    start_time = time.time()

    <span class="hljs-comment"># Create separate processes for each task</span>
    p1 = Process(target=compute_heavy_task, args=(<span class="hljs-string">"Task 1"</span>, <span class="hljs-number">10000000</span>))
    p2 = Process(target=compute_heavy_task, args=(<span class="hljs-string">"Task 2"</span>, <span class="hljs-number">10000000</span>))
    p3 = Process(target=compute_heavy_task, args=(<span class="hljs-string">"Task 3"</span>, <span class="hljs-number">10000000</span>))

    <span class="hljs-comment"># Start all processes (they run on separate CPU cores)</span>
    p1.start()
    p2.start()
    p3.start()

    <span class="hljs-comment"># Wait for all processes to complete</span>
    p1.join()
    p2.join()
    p3.join()

    end_time = time.time()
    print(<span class="hljs-string">f"\nAll tasks completed in <span class="hljs-subst">{end_time - start_time:<span class="hljs-number">.2</span>f}</span> seconds"</span>)
</code></pre>
<p><strong>What happens during execution:</strong></p>
<ol>
<li><p>Three separate processes are spawned, each allocated to available CPU cores.</p>
</li>
<li><p>Each process runs independently with its own memory space and Python interpreter.</p>
</li>
<li><p>All three CPU-intensive calculations execute truly simultaneously across multiple cores.</p>
</li>
<li><p>The total runtime is determined by the longest-running task, not the cumulative sum of all tasks.</p>
</li>
<li><p>On a multi-core system, this completes approximately three times faster than sequential execution.</p>
</li>
</ol>
<p><strong>Key insight:</strong> Parallelism achieves actual speedup by distributing computational workload across multiple processors. This directly reduces total execution time for CPU-bound operations.</p>
<h2 id="heading-concurrency-vs-parallelism-a-detailed-comparison">Concurrency vs. Parallelism: A Detailed Comparison</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>Concurrency</td><td>Parallelism</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Core Definition</strong></td><td>Managing and coordinating multiple tasks within overlapping time periods</td><td>Executing multiple tasks simultaneously across multiple processors</td></tr>
<tr>
<td><strong>Primary Goal</strong></td><td>Improve structure, responsiveness, and resource efficiency</td><td>Increase raw computational throughput and speed</td></tr>
<tr>
<td><strong>CPU Utilization</strong></td><td>Can work on single or multiple cores through interleaving</td><td>Requires multiple cores or processors for true parallelism</td></tr>
<tr>
<td><strong>Execution Model</strong></td><td>Task switching and scheduling</td><td>Simultaneous execution across hardware</td></tr>
<tr>
<td><strong>Optimal Use Case</strong></td><td>I/O-bound operations (network requests, file operations, database queries)</td><td>CPU-bound operations (mathematical computations, data processing, rendering)</td></tr>
<tr>
<td><strong>Common Implementation Techniques</strong></td><td>Async/await patterns, threads, coroutines, event loops</td><td>Multiprocessing, GPU computing, and distributed computing frameworks</td></tr>
<tr>
<td><strong>Performance Characteristic</strong></td><td>Reduces idle time and improves throughput without necessarily speeding up individual tasks</td><td>Directly reduces execution time by dividing the work</td></tr>
<tr>
<td><strong>Typical Applications</strong></td><td>Web servers, REST APIs, GUI applications, chat systems, and real-time notifications</td><td>Video encoding, scientific simulations, machine learning training, big data analytics</td></tr>
<tr>
<td><strong>Resource Overhead</strong></td><td>Lower (shared memory, lightweight context switching)</td><td>Higher (separate memory spaces, inter-process communication costs)</td></tr>
</tbody>
</table>
</div><h3 id="heading-when-to-use-each">When to Use Each:</h3>
<p>Use <strong>concurrency</strong> when you want to handle more tasks effectively within the same time period, particularly when those tasks spend time waiting for external resources.</p>
<p>Use <strong>parallelism</strong> when you want to complete tasks faster by leveraging multiple processors to divide the computational workload.</p>
<h2 id="heading-real-world-applications-and-use-cases">Real-World Applications and Use Cases</h2>
<h3 id="heading-concurrency-in-production-systems">Concurrency in Production Systems</h3>
<h4 id="heading-1-web-servers-and-apis">1. Web Servers and APIs</h4>
<p>Modern web frameworks like Node.js, Django with async views, and FastAPI handle thousands of simultaneous client connections. Each request may involve database queries, external API calls, or file operations. Concurrency allows the server to handle new requests while waiting for I/O operations from previous requests to complete.</p>
<h4 id="heading-2-real-time-communication">2. Real-Time Communication</h4>
<p>Chat applications, collaborative editing tools, and live streaming platforms manage multiple simultaneous connections. Messages must be received, processed, and broadcast to multiple clients concurrently without blocking any single connection.</p>
<h4 id="heading-3-mobile-applications">3. Mobile Applications</h4>
<p>Mobile apps perform background synchronization, push notification handling, and data caching while maintaining a responsive user interface. The UI thread remains free while background operations proceed concurrently.</p>
<h4 id="heading-4-microservices-orchestration">4. Microservices Orchestration</h4>
<p>Service meshes coordinate multiple API calls to different microservices, aggregating results efficiently without waiting for each call to complete sequentially.</p>
<h3 id="heading-parallelism-in-production-systems">Parallelism in Production Systems</h3>
<h4 id="heading-1-machine-learning-and-ai">1. Machine Learning and AI</h4>
<p>Training neural networks involves massive matrix computations that can be distributed across multiple GPU cores or even multiple machines. Frameworks like TensorFlow and PyTorch automatically parallelise operations across available hardware.</p>
<h4 id="heading-2-big-data-processing">2. Big Data Processing</h4>
<p>Distributed computing frameworks such as Apache Spark, Hadoop, and Dask divide large datasets across cluster nodes. Each node processes its portion of the data in parallel, enabling analysis of petabyte-scale datasets.</p>
<h4 id="heading-3-media-processing">3. Media Processing</h4>
<p>Video transcoding, image batch processing, and audio rendering leverage multiple CPU cores or GPUs. Each frame or segment can be processed independently in parallel.</p>
<h4 id="heading-4-scientific-computing">4. Scientific Computing</h4>
<p>Computational physics simulations, genome sequencing, and climate modelling require enormous computational resources. Parallelism across supercomputer clusters enables these calculations to complete in reasonable time frames.</p>
<h4 id="heading-5-financial-modelling">5. Financial Modelling</h4>
<p>Risk analysis and portfolio optimisation involve running thousands of scenarios. Parallel processing allows these computations to execute simultaneously, providing results quickly enough for real-time decision making.</p>
<h3 id="heading-hybrid-approaches">Hybrid Approaches</h3>
<p>In practice, sophisticated systems frequently combine both paradigms. Consider a modern web application:</p>
<ol>
<li><p>The web server handles client requests concurrently (handling multiple users simultaneously).</p>
</li>
<li><p>Each request may trigger parallel data processing tasks (such as image resizing across multiple cores).</p>
</li>
<li><p>The database connection pool manages concurrent query execution.</p>
</li>
<li><p>Background job workers process tasks in parallel (such as sending emails or generating reports).</p>
</li>
</ol>
<p>This layered approach leverages the strengths of both concurrency and parallelism to create systems that are both responsive and computationally efficient.</p>
<h2 id="heading-choosing-the-right-approach-for-your-problem">Choosing the Right Approach for Your Problem</h2>
<p>Understanding which paradigm to apply requires analysing the nature of your workload:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>If Your Task Is...</td><td>Choose...</td><td>Reasoning</td></tr>
</thead>
<tbody>
<tr>
<td><strong>I/O-bound</strong> (waiting for network, disk, or database operations)</td><td><strong>Concurrency</strong></td><td>Maximises efficiency by allowing other work to proceed during wait times. The bottleneck is not CPU computation but external resource availability.</td></tr>
<tr>
<td><strong>CPU-bound</strong> (heavy mathematical computation, data processing, rendering)</td><td><strong>Parallelism</strong></td><td>Distributes computational load across multiple processors, directly reducing execution time. The bottleneck is CPU capacity.</td></tr>
<tr>
<td><strong>Mixed workload</strong> (both I/O operations and intensive computation)</td><td><strong>Concurrency + Parallelism</strong></td><td>Concurrent handling of I/O operations combined with parallel processing of CPU-intensive segments provides optimal performance.</td></tr>
<tr>
<td><strong>Many small, independent tasks</strong></td><td><strong>Concurrency</strong> (if I/O) or <strong>Parallelism</strong> (if CPU)</td><td>Choose based on whether tasks are waiting or computing.</td></tr>
<tr>
<td><strong>Few large, divisible computations</strong></td><td><strong>Parallelism</strong></td><td>Split each computation across cores for maximum speedup.</td></tr>
</tbody>
</table>
</div><h3 id="heading-common-pitfall-to-avoid">Common Pitfall to Avoid</h3>
<p>A frequent mistake is attempting to use threading for CPU-bound tasks in languages with a Global Interpreter Lock (like Python's CPython) and expecting parallel speedups. In such cases, threads provide concurrency but not true parallelism.</p>
<p>The GIL ensures only one thread executes Python bytecode at a time, leading to context-switching overhead without genuine parallel execution. For CPU-bound work in Python, multiprocessing or C extensions are necessary for true parallelism.</p>
<h2 id="heading-why-this-distinction-matters-in-practice">Why This Distinction Matters in Practice</h2>
<p>Grasping the difference between concurrency and parallelism extends beyond writing faster code. It fundamentally influences how you architect systems and make technological decisions:</p>
<p>First of all, choosing the appropriate execution model for each component of your system leads to cleaner, more maintainable code. You avoid over-engineering solutions or applying the wrong tool to a problem.</p>
<p>Understanding these concepts also prevents wasteful patterns such as spawning unnecessary processes for I/O-bound work or using single-threaded approaches for parallelizable computations. This directly translates to reduced infrastructure costs.</p>
<p>Systems designed with proper concurrency models also scale horizontally more effectively. Those leveraging parallelism appropriately utilise hardware resources fully as you scale vertically.</p>
<p>In addition, you’ll get some key performance optimisations by choosing the right approach. When profiling reveals bottlenecks, knowing whether to optimise for concurrency or parallelism guides your refactoring efforts in the right direction.</p>
<p>Beyond this, in cloud environments where you pay for compute resources, efficient use of concurrency and parallelism directly affects operational costs. An efficiently concurrent system might handle 10x the load on the same hardware compared to a poorly designed synchronous alternative.</p>
<p>And these concepts are fundamental to backend engineering, distributed systems, DevOps, machine learning engineering, and systems programming. They appear frequently in technical interviews and are essential for senior engineering roles.</p>
<h2 id="heading-common-misconceptions-and-clarifications">Common Misconceptions and Clarifications</h2>
<h3 id="heading-using-threads-automatically-gives-me-parallelism">"Using threads automatically gives me parallelism."</h3>
<p>In reality, threads enable concurrency but do not guarantee parallel execution. In systems with a Global Interpreter Lock (like CPython) or on single-core machines, threads run concurrently but not in parallel. True parallelism requires multiple CPU cores and mechanisms that avoid locking constraints.</p>
<h3 id="heading-parallelism-is-always-faster-than-sequential-execution">"Parallelism is always faster than sequential execution."</h3>
<p>In fact, parallelism introduces overhead, including process creation, inter-process communication, and data synchronisation costs. For small tasks or I/O-bound operations, this overhead can outweigh benefits. Parallelism shows gains when the computational work justifies the overhead.</p>
<h3 id="heading-concurrency-and-parallelism-are-mutually-exclusive">"Concurrency and parallelism are mutually exclusive."</h3>
<p>As you’ve learned, modern high-performance systems routinely combine both. A web server can handle requests concurrently, with each request triggering parallel processing. Understanding how to layer these approaches is key to building sophisticated systems.</p>
<h3 id="heading-more-threads-or-processes-always-mean-better-performance">"More threads or processes always mean better performance."</h3>
<p>Beyond a certain point, adding more threads or processes leads to diminishing returns and even performance degradation due to increased context switching and resource contention. The optimal number depends on workload characteristics and available hardware.</p>
<h3 id="heading-asyncawait-makes-my-code-run-faster">“Async/await makes my code run faster."</h3>
<p>Async/await improves efficiency for I/O-bound operations by reducing idle time, but it does not speed up CPU-bound computations. It changes how waiting is handled, not how quickly individual operations execute.</p>
<h2 id="heading-practical-implementation-strategies">Practical Implementation Strategies</h2>
<h3 id="heading-how-to-implement-concurrency">How to Implement Concurrency</h3>
<p>To introduce concurrency into your programs, first you’ll need to find where time is wasted. Blocking operations that are held waiting on external resources are the best candidates to be put under concurrent execution.</p>
<p>Say you’re building a web scraper to fetch a bunch of data on a variety of sites. Every single HTTP request is most likely waiting until the server gets a response back. Other requests might be underway in your program instead of waiting around during this waiting period. These wait points are identifiable by profiling your application and searching the operations with network calls, file I/O, or database queries.</p>
<p>After you’ve discovered these wait points, the next big step will be to select the concurrency primitive. In Python, I/O-bound operations perform very well using the patterns of async/await with the support of the asyncio framework. It also comes with a minimal cost.</p>
<p>Take a situation when you have to retrieve user data in a REST API and query a database at the same time. With asyncio, you can write code that initiates both tasks almost simultaneously, and then have the event loop alternate between them during periods of waiting.</p>
<p>Here's a practical example:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> asyncio
<span class="hljs-keyword">import</span> aiohttp

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">fetch_user_api</span>(<span class="hljs-params">user_id</span>):</span>
    <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> aiohttp.ClientSession() <span class="hljs-keyword">as</span> session:
        <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> session.get(<span class="hljs-string">f'https://api.example.com/users/<span class="hljs-subst">{user_id}</span>'</span>) <span class="hljs-keyword">as</span> response:
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> response.json()

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">query_database</span>(<span class="hljs-params">user_id</span>):</span>
    <span class="hljs-comment"># Simulating database query</span>
    <span class="hljs-keyword">await</span> asyncio.sleep(<span class="hljs-number">0.5</span>)
    <span class="hljs-keyword">return</span> {<span class="hljs-string">'preferences'</span>: <span class="hljs-string">'theme:dark'</span>, <span class="hljs-string">'notifications'</span>: <span class="hljs-literal">True</span>}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_complete_user_data</span>(<span class="hljs-params">user_id</span>):</span>
    api_data, db_data = <span class="hljs-keyword">await</span> asyncio.gather(
        fetch_user_api(user_id),
        query_database(user_id)
    )
    <span class="hljs-keyword">return</span> {**api_data, **db_data}
</code></pre>
<p>This gives a thorough look at how concurrency works in practice.</p>
<h3 id="heading-when-implementing-parallelism">When Implementing Parallelism</h3>
<p>Before committing parallelism into your system, you’ll need to profile the system and make sure that what causes your bottleneck is CPU-bound computation. Many developers think that their code requires parallelism when it should actually employ concurrency.</p>
<p>You can use profiling tools such as Python cProfile or line profilers to determine where time is being used or wasted in your program. When the time spent in computational loops is as large as compared to waiting in I/O, then parallelism can be beneficial.</p>
<p>To take an example, when processing images, the execution time in pixel manipulation algorithms consumes 90% of the execution time. This is a good sign that parallelism would be useful.</p>
<p>Deciding how to partition the work between multiple processors is sometimes a complex issue that you should consider carefully (in terms of dividing tasks into independent points). These chunks should be able to be processed individually without needing to communicate with each other on a regular basis.</p>
<p>Imagine that you have to examine the log files of several servers. Processing each file may happen on a different core, and the results will get added at the final stage.</p>
<p>Here's how you might structure this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> multiprocessing <span class="hljs-keyword">import</span> Pool
<span class="hljs-keyword">import</span> re

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">analyze_log_file</span>(<span class="hljs-params">filepath</span>):</span>
    error_count = <span class="hljs-number">0</span>
    <span class="hljs-keyword">with</span> open(filepath, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> f:
        <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> f:
            <span class="hljs-keyword">if</span> re.search(<span class="hljs-string">r'ERROR|CRITICAL'</span>, line):
                error_count += <span class="hljs-number">1</span>
    <span class="hljs-keyword">return</span> filepath, error_count

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    log_files = [<span class="hljs-string">'server1.log'</span>, <span class="hljs-string">'server2.log'</span>, <span class="hljs-string">'server3.log'</span>, <span class="hljs-string">'server4.log'</span>]

    <span class="hljs-keyword">with</span> Pool(processes=<span class="hljs-number">4</span>) <span class="hljs-keyword">as</span> pool:
        results = pool.map(analyze_log_file, log_files)

    <span class="hljs-keyword">for</span> filepath, count <span class="hljs-keyword">in</span> results:
        print(<span class="hljs-string">f'<span class="hljs-subst">{filepath}</span>: <span class="hljs-subst">{count}</span> errors found'</span>)
</code></pre>
<p>In this example, each log file is processed entirely on one core without needing to communicate with other processes until the final result aggregation.</p>
<h2 id="heading-tools-and-technologies-by-language">Tools and Technologies by Language</h2>
<p>Various programming languages offer different methods of achieving concurrency and parallelism with their own advantages and disadvantages. And when you understand the available tools in your language of choice, you’ll be able to make a wise architectural choice.</p>
<h3 id="heading-python">Python</h3>
<p>Python has a concurrent and parallel environment. For concurrent programming, the asyncio library offers a more modern syntax of async/await that’s ideal in I/O-bound tasks such as web scraping or API communication.</p>
<p>The threading module allows shared memory execution, but is restricted on CPU-bound tasks by the Global Interpreter Lock. The concurrent futures module is a high-level interface to concurrent task execution, which can be useful when you want to parallelize I/O operations without having to write the low-level code of asynchronous operations.</p>
<p>Sometimes you’ll need actual parallelism because your job requires a lot of CPU time. Multiprocessing starts individual Python processes, which don’t use the GIL at all.</p>
<p>In the case of data science and machine learning processes, distributed parallelism is offered in libraries such as joblib, ray, and dask and can run on your laptop up to a cluster of computers.</p>
<h3 id="heading-javascript-and-nodejs">JavaScript and Node.js</h3>
<p>The event loop architecture had concurrency as its foundation in JavaScript and Node.js. Asynchronous programming is now intuitive with native syntax and Promises being used as the standard model of dealing with I/O operations (like HTTP requests or file system access).</p>
<p>JavaScript is single-threaded, and Node.js is designed to execute single-thread programs that make good use of I/O bound concurrent tasks, such as web servers, which support thousands of parallel connections.</p>
<p>In cases of actual parallelism (for example, image processing or cryptographic tasks), worker threads enable you to execute JavaScript on multiple cores. The child processes module can launch individual instances of Node.js, and the cluster module allows you to launch a pool of workers to accept incoming connections and make the most of all CPU cores in a web server.</p>
<h3 id="heading-java">Java</h3>
<p>Java has mature and battle-tested concurrency and parallelism support. CompletableFuture offers a fluent interface to asynchronous operations, so it’s easier to sequence dependent asynchronous tasks together without any callback hell.</p>
<p>The ExecutorService model also provides detailed management of thread pools and task scheduling, which is necessary in developing high performance server programs. Parallelism Java thread pools are effective at handling worker threads to execute CPU-bound tasks, whereas ForkJoinPool uses work-stealing algorithms that are useful in divide-and-conquer problems.</p>
<p>Java 8 offers parallel streams, which allow you to process collections in parallel with a minimal amount of code rewrites – but you have to pay close attention to when they actually will or will not improve performance.</p>
<h3 id="heading-go">Go</h3>
<p>Go introduced concurrency as a first-class language: goroutines and channels. Goroutines are lightweight threads controlled by the Go runtime, which means that you can run thousands or even millions of operations concurrently with minimal overhead.</p>
<p>The philosophy of communication in Channels offers a secure means of communication between goroutines, and it includes the expression "do not communicate by sharing memory; share memory by communicating." Such a design makes concurrent programming more user-friendly and error-free.</p>
<p>In parallelism, Go automatically allocates goroutines to multiple CPU cores according to the GOMAXPROCS environment variable, and parallel execution is achieved automatically. This renders Go especially effective in the construction of parallel systems such as web servers, network tools, and distributed systems.</p>
<h3 id="heading-rust">Rust</h3>
<p>Rust provides concurrent and parallel programming with memory safety without performance degradation. The ownership system of the language eliminates all forms of data races at compile-time, which means that the entire category of concurrency bugs found in other languages doesn’t exist.</p>
<p>In the case of async operations, you can apply the syntax of Rust to operations of an asynchronous type with runtime libraries such as tokio or async-std and achieves similar performance to C++ without sacrificing safety.</p>
<p>The Rayon library makes parallelism of data exceedingly easy. At times, you can parallelise a calculation by substituting .iter() with .par_iter(). Rust thread pools and channels give you low-level control where necessary, and the type system keeps the threads safe, making sure that problems don’t arise in your code.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Concurrency and parallelism represent fundamental pillars of modern computing architecture. They are not interchangeable buzzwords, but are rather distinct paradigms that address different challenges:</p>
<p>Concurrency focuses on program structure and efficient task coordination. It allows systems to handle multiple operations within overlapping time periods, maximizing resource utilization and responsiveness.</p>
<p>Parallelism focuses on computational throughput and execution speed. It divides work across multiple processors to complete tasks faster through simultaneous execution.</p>
<p>The most powerful systems strategically combine both approaches, applying each where it provides the greatest benefit.</p>
<p>The next time you face a performance challenge, ask yourself these critical questions:</p>
<ol>
<li><p>Is my bottleneck caused by waiting (I/O-bound) or by computation (CPU-bound)?</p>
</li>
<li><p>Am I trying to handle more tasks simultaneously or complete tasks faster?</p>
</li>
<li><p>Do I need better resource utilisation or raw computational throughput?</p>
</li>
</ol>
<p>Your answers will guide you toward the right solution. Understanding when to apply concurrency, when to leverage parallelism, and when to combine them is what separates adequate solutions from exceptional ones. This knowledge empowers you to build systems that are not only fast but also efficient, scalable, and economically viable.</p>
<p>Master these concepts, and you will find yourself equipped to tackle increasingly complex engineering challenges with confidence and precision.</p>
<h3 id="heading-further-learning-resources">Further Learning Resources</h3>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=f6kdp27TYZs">"Go Concurrency Patterns" by Rob Pike (Google Tech Talk)</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=oV9rvDllKEg&amp;utm_source=chatgpt.com">"Concurrency is not Parallelism" by Rob Pike</a></p>
</li>
<li><p><a target="_blank" href="https://docs.python.org/3/library/asyncio.html">Python AsyncIO Official Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://realpython.com/python-concurrency/">Real Python: Concurrency and Parallelism in Python</a></p>
</li>
<li><p><a target="_blank" href="https://jcip.net/?utm_source=chatgpt.com">Java Concurrency in Practice by Brian Goetz</a></p>
</li>
<li><p><a target="_blank" href="https://spark.apache.org/docs/latest/">Apache Spark Documentation for Big Data Parallelism</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker Build Tutorial: Learn Contexts, Architecture, and Performance Optimization Techniques ]]>
                </title>
                <description>
                    <![CDATA[ Docker build is a fundamental concept every developer needs to understand. Whether you're containerizing your first application or optimizing existing Docker workflows, understanding Docker build contexts and Docker build architecture is essential fo... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/docker-build-tutorial-learn-contexts-architecture-and-performance-optimization-techniques/</link>
                <guid isPermaLink="false">68e559d8ac28fbe4acae92be</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Destiny Erhabor ]]>
                </dc:creator>
                <pubDate>Tue, 07 Oct 2025 18:20:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759861193876/871b72e7-9673-4572-b788-48f082a6b380.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Docker build is a fundamental concept every developer needs to understand. Whether you're containerizing your first application or optimizing existing Docker workflows, understanding Docker build contexts and Docker build architecture is essential for creating efficient, scalable containerized applications.</p>
<p>This comprehensive guide covers everything from basic concepts to advanced optimization techniques, helping you avoid common pitfalls and build better Docker images.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-docker-build">What is Docker Build?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-docker-build-architecture-how-it-all-works">Docker Build Architecture: How It All Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-docker-build-features">Docker Build Features</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-docker-build-context">Docker Build Context</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-docker-build-contexts">Types of Docker Build Contexts</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-docker-build-mistakes-and-how-to-fix-them">Common Docker Build Mistakes (And How to Fix Them)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-optimize-and-monitor-build-performance">How to Optimize and Monitor Build Performance</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-for-docker-build-performance">Best Practices for Docker Build Performance</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting-docker-build-issues">Troubleshooting Docker Build Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-is-docker-build">What is Docker Build?</h2>
<p>Docker build is the process of creating a Docker image from a Dockerfile and a set of files called the <strong>build context</strong>. When you run <code>docker build</code>, you're instructing Docker to:</p>
<ol>
<li><p>Read your Dockerfile instructions</p>
</li>
<li><p>Gather the necessary files (build context)</p>
</li>
<li><p>Execute each instruction step-by-step</p>
</li>
<li><p>Create a final Docker image</p>
</li>
</ol>
<p>Think of it like following a recipe: the Dockerfile is your recipe, and the build context contains all the ingredients you might need.</p>
<h2 id="heading-docker-build-architecture-how-it-all-works">Docker Build Architecture: How It All Works</h2>
<p>Docker Build uses a client-server architecture where two separate components (<strong>Buildx and BuildKit</strong>) work together to build your Docker images. This is different from how many people think Docker works, as it's not just one monolithic program doing everything.</p>
<h3 id="heading-what-is-buildx-the-client">What is Buildx (The Client)?</h3>
<p>Buildx serves as the user interface that you interact with directly whenever you work with Docker builds. When you type <code>docker build .</code> in your terminal, you're actually communicating with Buildx, which acts as the intermediary between you and the actual build engine.</p>
<h4 id="heading-buildxs-primary-jobs">Buildx’s primary jobs:</h4>
<ul>
<li><p>Interprets your build command and options</p>
</li>
<li><p>Sends structured build requests to BuildKit</p>
</li>
<li><p>Manages multiple BuildKit instances (builders)</p>
</li>
<li><p>Handles authentication and secrets</p>
</li>
<li><p>Displays build progress to you</p>
</li>
</ul>
<h3 id="heading-what-is-buildkit-the-serverbuilder">What is BuildKit (The Server/Builder)</h3>
<p>BuildKit functions as the actual build engine that performs all the heavy lifting during the Docker build process. This powerful backend component receives the structured build requests from Buildx and immediately begins reading and interpreting your Dockerfiles line by line.</p>
<h4 id="heading-buildkits-primary-jobs">BuildKit’s primary jobs:</h4>
<ul>
<li><p>Receives build requests from Buildx</p>
</li>
<li><p>Reads and interprets Dockerfiles</p>
</li>
<li><p>Executes build instructions step by step</p>
</li>
<li><p>Manages build cache and layers</p>
</li>
<li><p>Requests only the files it needs from the client</p>
</li>
<li><p>Creates the final Docker image</p>
</li>
</ul>
<h3 id="heading-how-they-communicate">How They Communicate</h3>
<p>Here's what happens when you run <code>docker build .</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758733757378/d3322dad-efac-4c4a-b8f8-69f17a4920e8.png" alt="Diagram showing Docker build process with BuildKit, including sending build request with Dockerfile and build arguments, requesting and receiving package.json, running npm install, requesting and receiving src directory files, copying files, completing build, and optionally pushing to registry." class="image--center mx-auto" width="2947" height="2628" loading="lazy"></p>
<p>When you run <code>docker build</code>, the command initiates a multi-step process with BuildKit (as illustrated in the above image).</p>
<p>First, it sends a build request containing your Dockerfile, build arguments, export options, and cache options. BuildKit then intelligently requests only the files it needs when it needs them, starting with <code>package.json</code> to run <code>npm install</code> for dependency installation.</p>
<p>After that's complete, it requests the <code>src/</code> directory containing your application code and copies those files into the image with the <code>COPY</code> command.</p>
<p>Once all build steps are finished, BuildKit sends back the completed image. Optionally, you can then push this image to a container registry for distribution or deployment.</p>
<p>This on-demand file transfer approach is one of BuildKit's key optimizations: rather than sending your entire build context upfront, it only requests specific files as each build step needs them, making the build process more efficient.</p>
<h3 id="heading-key-communication-details">Key Communication Details</h3>
<p>Build request contains:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"dockerfile"</span>: <span class="hljs-string">"FROM node:18\nWORKDIR /app\n..."</span>,
  <span class="hljs-attr">"buildArgs"</span>: {<span class="hljs-attr">"NODE_ENV"</span>: <span class="hljs-string">"production"</span>},
  <span class="hljs-attr">"exportOptions"</span>: {<span class="hljs-attr">"type"</span>: <span class="hljs-string">"image"</span>, <span class="hljs-attr">"name"</span>: <span class="hljs-string">"my-app:latest"</span>},
  <span class="hljs-attr">"cacheOptions"</span>: {<span class="hljs-attr">"type"</span>: <span class="hljs-string">"registry"</span>, <span class="hljs-attr">"ref"</span>: <span class="hljs-string">"my-app:cache"</span>}
}
</code></pre>
<p>Resource requests:</p>
<ul>
<li><p>BuildKit asks: "I need the file at <code>./package.json</code>"</p>
</li>
<li><p>Buildx responds: Sends the actual file content</p>
</li>
<li><p>BuildKit asks: "I need the directory <code>./src/</code>"</p>
</li>
<li><p>Buildx responds: Sends all files in that directory</p>
</li>
</ul>
<h3 id="heading-why-this-architecture-exists">Why This Architecture Exists</h3>
<h4 id="heading-1-efficiency">1. Efficiency</h4>
<p>The old Docker builder had a major flaw: it always copied your entire build context upfront, regardless of what was actually needed. Even if your Dockerfile only used a few files, Docker would transfer hundreds of megabytes before starting the build.</p>
<p>BuildKit fixes this through on-demand file transfers. It only requests specific files at each step.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Old Docker Builder (legacy)</span>
<span class="hljs-comment"># Always copied ENTIRE context upfront</span>
$ docker build .
Sending build context to Docker daemon  245.7MB  <span class="hljs-comment"># Everything!</span>

<span class="hljs-comment"># New BuildKit Architecture  </span>
<span class="hljs-comment"># Only requests files when needed</span>
$ docker build .
<span class="hljs-comment">#1 [internal] load build definition from Dockerfile    0.1s</span>
<span class="hljs-comment">#2 [internal] load .dockerignore                       0.1s</span>
<span class="hljs-comment">#3 [1/4] FROM node:18                                  0.5s</span>
<span class="hljs-comment">#4 [internal] load build context                       0.1s</span>
<span class="hljs-comment">#4 transferring context: 234B  # Only package.json initially!</span>
<span class="hljs-comment">#5 [2/4] WORKDIR /app                                  0.2s  </span>
<span class="hljs-comment">#6 [3/4] COPY package*.json ./                         0.1s</span>
<span class="hljs-comment">#7 [4/4] RUN npm install                               5.2s</span>
<span class="hljs-comment">#8 [internal] load build context                       0.3s  </span>
<span class="hljs-comment">#8 transferring context: 2.1MB  # Now requests src/ files</span>
<span class="hljs-comment">#9 [5/4] COPY src/ ./src/                              0.2s</span>
</code></pre>
<h4 id="heading-2-scalability">2. Scalability</h4>
<p>The client-server architecture enables scalability features. Multiple Docker CLI clients can connect to the same BuildKit instance, and BuildKit can run on remote servers instead of your local machine. This means you could execute builds on a cloud server while controlling them from your laptop. Teams can also deploy multiple BuildKit instances for different teams or purposes, scaling from individual developers to large enterprises.</p>
<h4 id="heading-3-security">3. Security</h4>
<p>Security is improved by only requesting sensitive files when explicitly needed. BuildKit never sees files your Dockerfile doesn't reference, reducing the attack surface. It also handles credentials through separate, secure channels rather than mixing them with your build context, preventing secrets from being embedded in image layers or exposed in build logs.</p>
<h3 id="heading-real-world-example">Real-World Example</h3>
<p>Let's trace through a typical build step by step. You can find the full code available here: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/02-python-cache">02-python-cache</a>.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.9</span>-slim
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> requirements.txt .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> pip install -r requirements.txt</span>
<span class="hljs-keyword">COPY</span><span class="bash"> src/ ./src/</span>
<span class="hljs-keyword">COPY</span><span class="bash"> main.py .</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"python"</span>, <span class="hljs-string">"main.py"</span>]</span>
</code></pre>
<p>Let’s see what actually happens here:</p>
<ol>
<li><p>You run <code>docker build .</code></p>
</li>
<li><p>Buildx says to BuildKit:</p>
</li>
</ol>
<pre><code class="lang-bash">   <span class="hljs-string">"Here's a build request with this Dockerfile"</span>
</code></pre>
<ol start="3">
<li><p><strong>BuildKit processes</strong>: <code>FROM python:3.9-slim</code></p>
<ul>
<li>No client files needed, pulls base image</li>
</ul>
</li>
<li><p><strong>BuildKit processes</strong>: <code>COPY requirements.txt .</code></p>
<ul>
<li><p>BuildKit to Buildx: "I need <code>requirements.txt</code>"</p>
</li>
<li><p>Buildx to BuildKit: Sends the file content</p>
</li>
</ul>
</li>
<li><p><strong>BuildKit processes</strong>: <code>RUN pip install -r requirements.txt</code></p>
<ul>
<li>No client files needed, runs inside container</li>
</ul>
</li>
<li><p><strong>BuildKit processes</strong>: <code>COPY src/ ./src/</code></p>
<ul>
<li><p>BuildKit to Buildx: "I need all files in <code>src/</code> directory"</p>
</li>
<li><p>Buildx to BuildKit: Sends all files in src/</p>
</li>
</ul>
</li>
<li><p><strong>BuildKit processes</strong>: <code>COPY main.py .</code></p>
<ul>
<li><p>BuildKit to Buildx: "I need <code>main.py</code>"</p>
</li>
<li><p>Buildx to BuildKit: Sends the file</p>
</li>
</ul>
</li>
<li><p>BuildKit to Buildx: "Build complete, here's your image"</p>
</li>
</ol>
<p>From the illustration, you can see that BuildKit only requests what it needs, when it needs it. Not this entire context:</p>
<pre><code class="lang-bash">
my-app/
├── src/                 <span class="hljs-comment"># ← Only loaded when COPY src/ runs</span>
├── tests/              <span class="hljs-comment"># ← Never requested (not in Dockerfile)</span>
├── docs/               <span class="hljs-comment"># ← Never requested  </span>
├── node_modules/       <span class="hljs-comment"># ← Never requested (in .dockerignore)</span>
├── requirements.txt    <span class="hljs-comment"># ← Loaded early (first COPY)</span>
└── main.py            <span class="hljs-comment"># ← Loaded later (second COPY)</span>
</code></pre>
<h2 id="heading-docker-build-features">Docker Build Features</h2>
<h3 id="heading-named-contexts">Named Contexts</h3>
<p>👉 Demo project: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/07-named-contexts">07-named-contexts</a></p>
<p>Named contexts allow you to include files from multiple sources during a build while keeping them logically separated. This is useful when you need documentation, configuration files, or shared libraries from different directories or repositories in your build.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Build with additional named context</span>
docker build --build-context docs=./documentation .
</code></pre>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use named context in Dockerfile</span>
<span class="hljs-keyword">FROM</span> alpine
<span class="hljs-keyword">COPY</span><span class="bash"> . /app</span>
<span class="hljs-comment"># Mount files from named context</span>
<span class="hljs-keyword">RUN</span><span class="bash"> --mount=from=docs,target=/docs \
    cp /docs/manual.pdf /app/</span>
</code></pre>
<h3 id="heading-build-secrets">Build Secrets</h3>
<p>👉 Demo project: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/06-build-secrets">06-build-secrets</a></p>
<p>Build secrets let you pass sensitive information (like API keys or passwords) to your build without including them in the final image or build history. The secrets are mounted temporarily during specific <code>RUN</code> commands and are never stored in image layers.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Pass secret to build</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"api_key=secret123"</span> | docker build --secret id=apikey,src=- .
</code></pre>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Use secret in Dockerfile</span>
<span class="hljs-keyword">FROM</span> alpine
<span class="hljs-keyword">RUN</span><span class="bash"> --mount=<span class="hljs-built_in">type</span>=secret,id=apikey \
    <span class="hljs-built_in">export</span> API_KEY=$(cat /run/secrets/apikey) &amp;&amp; \
    curl -H <span class="hljs-string">"Authorization: <span class="hljs-variable">$API_KEY</span>"</span> https://api.example.com/data</span>
</code></pre>
<h2 id="heading-docker-build-context">Docker Build Context</h2>
<h3 id="heading-what-is-a-build-context">What is a Build Context?</h3>
<p>The build context is the collection of files and directories that Docker can access during the build process. It's like gathering all your cooking ingredients on the counter before you start cooking.</p>
<pre><code class="lang-bash">docker build [OPTIONS] CONTEXT
                       ^^^^^^^
                       This is your build context
</code></pre>
<h3 id="heading-why-build-contexts-matter">Why Build Contexts Matter</h3>
<ol>
<li><p><strong>Security</strong>: Only files in the context can be accessed during build</p>
</li>
<li><p><strong>Performance</strong>: Large contexts slow down builds</p>
</li>
<li><p><strong>Functionality</strong>: Your Dockerfile can only COPY/ADD files from the context</p>
</li>
<li><p><strong>Efficiency</strong>: Understanding contexts helps you build faster, leaner images</p>
</li>
</ol>
<h2 id="heading-types-of-docker-build-contexts">Types of Docker Build Contexts</h2>
<h3 id="heading-1-local-directory-context-most-common">1. Local Directory Context (Most Common)</h3>
<p>👉 See code here: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/01-node-local-context">01-node-local-context</a></p>
<p>This is what you'll use in 90% of cases – pointing to a folder on your machine:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Use current directory</span>
docker build .

<span class="hljs-comment"># Use specific directory</span>
docker build /path/to/my/project

<span class="hljs-comment"># Use parent directory</span>
docker build ..
</code></pre>
<p><strong>Example Project Structure:</strong></p>
<pre><code class="lang-bash">my-webapp/
├── src/
│   ├── index.js
│   └── utils.js
├── public/
│   ├── index.html
│   └── styles.css
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
└── README.md
</code></pre>
<p><strong>Corresponding Dockerfile:</strong></p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span>-alpine
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>

<span class="hljs-comment"># Copy package files first for better layer caching</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm ci --only=production</span>

<span class="hljs-comment"># Copy application source</span>
<span class="hljs-keyword">COPY</span><span class="bash"> src/ ./src/</span>
<span class="hljs-keyword">COPY</span><span class="bash"> public/ ./public/</span>

<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">3000</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"node"</span>, <span class="hljs-string">"src/index.js"</span>]</span>
</code></pre>
<h3 id="heading-2-remote-git-repository-context">2. Remote Git Repository Context</h3>
<p>You can build directly from Git repositories without cloning locally:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Build from GitHub main branch</span>
docker build https://github.com/&lt;username&gt;/project.git

<span class="hljs-comment"># Build from specific branch</span>
docker build https://github.com/&lt;username&gt;/project.git<span class="hljs-comment">#develop</span>

<span class="hljs-comment"># Build from specific directory in repo</span>
docker build https://github.com/&lt;username&gt;/project.git<span class="hljs-comment">#main:docker</span>

<span class="hljs-comment"># Build with authentication</span>
docker build --ssh default git@github.com:&lt;username&gt;/private-repo.git
</code></pre>
<p>This has various cases like CI/CD pipelines, building open-source projects, ensuring clean builds from source control, automated deployments, and so on.</p>
<h3 id="heading-3-remote-tarball-context">3. Remote Tarball Context</h3>
<p>You can also build from compressed archives hosted on web servers. A remote <strong>tarball</strong> is a <code>.tar.gz</code> or similar compressed archive file accessible via HTTP/HTTPS. This is useful when your source code is packaged and hosted on a web server, artifact repository, or CDN. Docker downloads and extracts the archive automatically, using its contents as the build context.</p>
<p>This approach works well for CI/CD pipelines where build artifacts are stored centrally, or when you want to build images from released versions of your code without cloning entire repositories.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Build from remote tarball</span>
docker build http://server.com/context.tar.gz

<span class="hljs-comment"># BuildKit downloads and extracts automatically</span>
docker build https://example.com/project-v1.2.3.tar.gz
</code></pre>
<h3 id="heading-4-empty-context-advanced">4. Empty Context (Advanced)</h3>
<p>When you don't need any files, you can pipe the Dockerfile directly:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create image without file context</span>
docker build -t hello-world - &lt;&lt;EOF
FROM alpine:latest
RUN <span class="hljs-built_in">echo</span> <span class="hljs-string">"Hello, World!"</span> &gt; /hello.txt
CMD cat /hello.txt
EOF
</code></pre>
<h2 id="heading-common-docker-build-mistakes-and-how-to-fix-them">Common Docker Build Mistakes (And How to Fix Them)</h2>
<h3 id="heading-mistake-1-wrong-context-directory">Mistake 1: Wrong Context Directory</h3>
<p>👉 Reproduced here: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/04-wrong-context">04-wrong-context</a></p>
<p>This mistake occurs when you run <code>docker build</code> from the wrong directory, causing the build context to be different from what your Dockerfile expects.</p>
<p>In the example, running <code>docker build frontend/</code> from the <code>/projects/</code> directory means the context is <code>/projects/frontend/</code>, but the Dockerfile tries to access <code>../shared/utils.js</code>, which is outside this context. Docker can only access files within the build context, so any attempt to reference files outside it will fail.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Project structure</span>
/projects/
├── frontend/
│   ├── Dockerfile
│   ├── src/
│   └── package.json
└── shared/
    └── utils.js

<span class="hljs-comment"># WRONG - Running from projects directory</span>
docker build frontend/
<span class="hljs-comment"># This won't work if Dockerfile tries to COPY ../shared/utils.js</span>
</code></pre>
<h4 id="heading-how-to-fix-wrong-context-directory">How to fix wrong context directory:</h4>
<p>The key is aligning your build context with what your Dockerfile needs.</p>
<ul>
<li><p><strong>Option 1</strong> changes your working directory so the context matches your Dockerfile's expectations. You run the build from inside <code>frontend/</code>, making that directory the context root.</p>
</li>
<li><p><strong>Option 2</strong> keeps you in the parent directory but explicitly sets it as the context (the <code>.</code> argument) while telling Docker where to find the Dockerfile with the <code>-f</code> flag. Now both <code>frontend/</code> and <code>shared/</code> are accessible since they're both within the <code>/projects/</code> context.</p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># Option 1: Run from correct directory</span>
<span class="hljs-built_in">cd</span> frontend
docker build .

<span class="hljs-comment"># Option 2: Use parent directory as context</span>
docker build -f frontend/Dockerfile .
</code></pre>
<h3 id="heading-mistake-2-including-massive-files">Mistake 2: Including Massive Files</h3>
<p>👉 Optimized version with <code>.dockerignore</code>: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/05-dockerignore-optimization">05-dockerignore-optimization</a></p>
<p>This mistake happens when your build context contains large, unnecessary files that slow down the build process.</p>
<p>Docker must transfer the entire context to the build daemon before starting, so including files like <code>node_modules</code> (which can be hundreds of MB), git history, build artifacts, logs, and database dumps makes builds painfully slow. These files are rarely needed in the final image and should be excluded.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># This context includes everything!</span>
my-app/
├── node_modules/        <span class="hljs-comment"># 200MB+ </span>
├── .git/               <span class="hljs-comment"># Version history</span>
├── dist/               <span class="hljs-comment"># Built files</span>
├── logs/               <span class="hljs-comment"># Log files</span>
├── temp/               <span class="hljs-comment"># Temporary files</span>
├── database.dump       <span class="hljs-comment"># 1GB database backup</span>
└── Dockerfile
</code></pre>
<h4 id="heading-how-to-fix-docker-build-massive-files">How to fix Docker build massive files:</h4>
<p>Use <code>.dockerignore</code> to exclude unnecessary files, dramatically reducing context size and build time. We’ll discuss this in more detail below.</p>
<h3 id="heading-mistake-3-inefficient-layer-caching">Mistake 3: Inefficient Layer Caching</h3>
<p>👉 See good practice code here: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/02-python-cache">02-python-cache</a></p>
<p>This mistake wastes Docker's layer caching system by copying frequently-changing files (like source code) before running expensive operations (like <code>npm install</code>). When you modify your source code, Docker invalidates the cache for that layer and all subsequent layers, forcing <code>npm install</code> to run again even though dependencies haven't changed. This can turn a 5-second build into a 5-minute build.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># BAD - Changes to source code rebuild npm install</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . /app</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"npm"</span>, <span class="hljs-string">"start"</span>]</span>
</code></pre>
<h4 id="heading-how-to-fix-docker-build-inefficient-layer-caching">How to fix docker build inefficient layer caching:</h4>
<p>Copy dependency files first, install dependencies, then copy source code. This way, <code>npm install</code> only runs when <code>package.json</code> actually changes:</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># GOOD - npm install only rebuilds when package.json changes</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span>
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm install</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"npm"</span>, <span class="hljs-string">"start"</span>]</span>
</code></pre>
<h2 id="heading-how-to-optimize-and-monitor-build-performance">How to Optimize and Monitor Build Performance</h2>
<p>Understanding build performance metrics helps you identify bottlenecks and measure improvements.</p>
<h3 id="heading-how-to-optimize-docker-builds-with-dockerignore">How to Optimize Docker Builds with .dockerignore</h3>
<p>The <code>.dockerignore</code> file is your secret weapon for faster, more secure builds. It tells Docker which files to exclude from the build context.</p>
<h4 id="heading-creating-dockerignore-patterns">Creating .dockerignore Patterns</h4>
<p>Create a <code>.dockerignore</code> file in your project root. The syntax is similar to <code>.gitignore</code>, and you can use wildcards (<code>*</code>), match specific file extensions (<code>*.log</code>), exclude entire directories (<code>node_modules/</code>), or use negation patterns (<code>!important.txt</code>) to include files that would otherwise be excluded. Each line represents a pattern, and comments start with <code>#</code>.</p>
<p>Example of a .dockerignore file:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Dependencies</span>
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

<span class="hljs-comment"># Build outputs</span>
dist/
build/
*.tgz

<span class="hljs-comment"># Version control</span>
.git/
.gitignore
.svn/

<span class="hljs-comment"># IDE and editor files</span>
.vscode/
.idea/
*.swp
*.swo
*~

<span class="hljs-comment"># OS generated files</span>
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

<span class="hljs-comment"># Logs and databases</span>
*.<span class="hljs-built_in">log</span>
*.sqlite
*.db

<span class="hljs-comment"># Environment and secrets</span>
.env
.env.local
.env.*.<span class="hljs-built_in">local</span>
secrets/
*.key
*.pem

<span class="hljs-comment"># Documentation</span>
README.md
docs/
*.md

<span class="hljs-comment"># Test files</span>
<span class="hljs-built_in">test</span>/
tests/
*.test.js
coverage/

<span class="hljs-comment"># Temporary files</span>
tmp/
temp/
*.tmp
</code></pre>
<h3 id="heading-measuring-build-performance">Measuring Build Performance</h3>
<h4 id="heading-analyzing-build-time">Analyzing Build Time</h4>
<p>Understanding where your build spends time helps identify bottlenecks and optimization opportunities. The detailed progress output shows timing for each build step, cache hits/misses, and resource usage.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Enable BuildKit progress output</span>
DOCKER_BUILDKIT=1 docker build --progress=plain .

<span class="hljs-comment"># Use buildx for detailed timing</span>
docker buildx build --progress=plain .
</code></pre>
<h4 id="heading-profiling-context-transfer">Profiling Context Transfer</h4>
<p>Monitor context transfer time to understand how build context size affects overall performance. Profile which directories contribute most to help target <code>.dockerignore</code> optimizations.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Measure context transfer time</span>
time docker build --no-cache .

<span class="hljs-comment"># Profile context size by directory</span>
du -sh */ | sort -hr
</code></pre>
<h4 id="heading-measuring-dockerignore-impact">Measuring .dockerignore Impact</h4>
<p>Before <code>.dockerignore</code>, you'll notice that the <code>transfering context</code> size is 245.7MB in 15.2s:</p>
<pre><code class="lang-bash">$ docker build .
<span class="hljs-comment">#1 [internal] load build context</span>
<span class="hljs-comment">#1 transferring context: 245.7MB in 15.2s</span>
</code></pre>
<p>After adding the .dockerignore file, the context reduced to 2.1MB in 0.3s:</p>
<pre><code class="lang-bash">$ docker build .
<span class="hljs-comment">#1 [internal] load build context  </span>
<span class="hljs-comment">#1 transferring context: 2.1MB in 0.3s</span>
</code></pre>
<p><strong>Result</strong>: 99% reduction in context size and 50x faster context transfer!</p>
<h2 id="heading-best-practices-for-docker-build-performance">Best Practices for Docker Build Performance</h2>
<p>We've covered several optimization techniques throughout this guide. Here's a quick recap of the key practices, plus some additional strategies:</p>
<ol>
<li><p><strong>Layer Caching</strong> (covered in Mistake 3): Copy dependency files before source code to maximize cache reuse.</p>
</li>
<li><p><strong>Using .dockerignore</strong> (covered in Mistake 2): Exclude unnecessary files to reduce context size and improve build speed.</p>
</li>
<li><p><strong>Choosing the Right Context</strong> (covered earlier): Select appropriate context types (local, Git, tarball) based on your use case.</p>
</li>
</ol>
<p>Now let’s talk about some more ways you can improve performance:</p>
<h3 id="heading-use-multi-stage-builds">Use Multi-Stage Builds</h3>
<p>👉 Demo project: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples/03-multistage-node">03-multistage-node</a></p>
<p>Multi-stage builds let you use one image for building/compiling your application and a different, smaller image for running it. This dramatically reduces your final image size by excluding build tools, source code, and other unnecessary files from the production image.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Build stage</span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span> AS builder
<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package*.json ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm ci</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm run build</span>

<span class="hljs-comment"># Production stage</span>
<span class="hljs-keyword">FROM</span> nginx:alpine
<span class="hljs-keyword">COPY</span><span class="bash"> --from=builder /app/dist /usr/share/nginx/html</span>
<span class="hljs-keyword">EXPOSE</span> <span class="hljs-number">80</span>
<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"nginx"</span>, <span class="hljs-string">"-g"</span>, <span class="hljs-string">"daemon off;"</span>]</span>
</code></pre>
<h3 id="heading-use-specific-base-images">Use Specific Base Images</h3>
<p>Generic base images like <code>ubuntu:latest</code> include many packages you don't need, making your images larger and slower to download. Specific images like <code>node:18-alpine</code> or distroless images contain only what's necessary for your application to run.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Large base image</span>
<span class="hljs-keyword">FROM</span> ubuntu:latest

<span class="hljs-comment"># Smaller, more specific base image  </span>
<span class="hljs-keyword">FROM</span> node:<span class="hljs-number">18</span>-alpine

<span class="hljs-comment"># Even smaller distroless image</span>
<span class="hljs-keyword">FROM</span> gcr.io/distroless/nodejs18-debian11
</code></pre>
<h3 id="heading-combine-run-commands">Combine RUN Commands</h3>
<p>Each <code>RUN</code> command creates a new layer in your image. Multiple <code>RUN</code> commands create multiple layers, increasing image size. Combining commands into a single <code>RUN</code> instruction creates just one layer, and you can clean up temporary files in the same step.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Creates multiple layers</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get install -y curl</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get clean</span>

<span class="hljs-comment"># Single layer</span>
<span class="hljs-keyword">RUN</span><span class="bash"> apt-get update &amp;&amp; \
    apt-get install -y curl &amp;&amp; \
    apt-get clean &amp;&amp; \
    rm -rf /var/lib/apt/lists/*</span>
</code></pre>
<h2 id="heading-troubleshooting-docker-build-issues">Troubleshooting Docker Build Issues</h2>
<h3 id="heading-issue-copy-failed-no-such-file-or-directory">Issue: "COPY failed: no such file or directory"</h3>
<p><strong>Problem</strong>: File not in build context<br><strong>What’s going wrong</strong>: Docker can only access files within the build context (the directory you specify in <code>docker build</code>). If your Dockerfile tries to <code>COPY</code> a file that doesn't exist in the context directory, the build fails. This often happens when running the build command from the wrong directory or when the file path is incorrect relative to the context root.</p>
<p><strong>Solution</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check what's in your context</span>
ls -la

<span class="hljs-comment"># Verify file path relative to context</span>
docker build -t debug . --progress=plain
</code></pre>
<h3 id="heading-issue-docker-build-is-extremely-slow">Issue: "Docker Build is extremely slow"</h3>
<p><strong>Problem</strong>: Large build context<br><strong>What’s going wrong</strong>: Docker must transfer your entire build context to the BuildKit daemon before building starts. If your context contains large files, directories like <code>node_modules</code>, or unnecessary files, this transfer can take minutes instead of seconds. The larger the context, the slower your builds become.</p>
<p><strong>Solution</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check context size</span>
du -sh .

<span class="hljs-comment"># Add more patterns to .dockerignore</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"large-directory/"</span> &gt;&gt; .dockerignore
<span class="hljs-built_in">echo</span> <span class="hljs-string">"*.zip"</span> &gt;&gt; .dockerignore
</code></pre>
<h3 id="heading-issue-cannot-locate-specified-dockerfile">Issue: "Cannot locate specified Dockerfile"</h3>
<p><strong>Problem</strong>: Dockerfile not in context root<br><strong>What’s going wrong</strong>: By default, Docker looks for a file named <code>Dockerfile</code> in the root of your build context. If your Dockerfile is in a subdirectory or has a different name, Docker can't find it. This is common in monorepo setups where Dockerfiles are organized in separate folders.</p>
<p><strong>Solution</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Specify Dockerfile location</span>
docker build -f path/to/Dockerfile .

<span class="hljs-comment"># Or move Dockerfile to context root</span>
mv path/to/Dockerfile .
</code></pre>
<h3 id="heading-issue-cache-misses-on-unchanged-files">Issue: "Cache misses on unchanged files"</h3>
<p><strong>Problem</strong>: File timestamps or permissions changed<br><strong>What’s going wrong</strong>: Docker's layer caching relies on file checksums and metadata. Even if file content is unchanged, different timestamps or permissions can cause cache misses, forcing unnecessary rebuilds. This often happens after git operations, file system operations, or when files are copied between systems.</p>
<p><strong>Solution</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Check file modifications</span>
git status

<span class="hljs-comment"># Reset timestamps</span>
git ls-files -z | xargs -0 touch -r .git/HEAD
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Understanding Docker build contexts and architecture is essential for achieving faster builds. We’ve covered various techniques in this article, like optimized contexts and caching strategies, creating smaller images with efficient layering and multi-stage builds, maintaining better security with proper secret handling and minimal attack surface, and delivering an improved developer experience with faster iteration cycles.</p>
<p>👉 <strong>Full code examples are available on GitHub here:</strong> <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building/tree/main/beginner/docker/docker-build-architecture-examples">Docker build architecture examples</a></p>
<p>As always, I hope you enjoyed the article and learned something new. If you want, you can also follow me on <a target="_blank" href="https://www.linkedin.com/in/destiny-erhabor">LinkedIn</a> or <a target="_blank" href="https://twitter.com/caesar_sage">Twitter</a>.</p>
<p>For more hands-on projects, follow and star this repository: <a target="_blank" href="https://github.com/Caesarsage/Learn-DevOps-by-building">Learn-DevOps-by-building</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Choose the Best Programming Languages, Libraries, and Patterns ]]>
                </title>
                <description>
                    <![CDATA[ In my first few years learning software development and building applications, I was quite interested in finding the best programming language, platform, libraries, frameworks, patterns, and architectures available. I thought that by finding the best... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-choose-the-best-programming-languages-libraries-and-patterns/</link>
                <guid isPermaLink="false">6898d3414c52a26ffefb1693</guid>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ programming languages ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ryan Michael Kay ]]>
                </dc:creator>
                <pubDate>Sun, 10 Aug 2025 17:13:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754846007203/c9db729e-ebed-4726-8e3e-5414c8e2714d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In my first few years learning software development and building applications, I was quite interested in finding the best programming language, platform, libraries, frameworks, patterns, and architectures available. I thought that by finding the <em>best</em> things and focusing on those topics to the exclusion of others, I could avoid wasting precious time.</p>
<p>While I figured out early on that narrowing my focus by using a project-based learning approach (as opposed to the topic-by-topic laundry list approach) was important, finding the best tools for the job was a different matter. </p>
<p>If you happen to be searching for some of those things, then this article is for you. After over a decade of programming products, building client applications, answering thousands of questions from junior and intermediate developers, and wrestling with these questions myself, I will do my best to explain how to find the best <em>things</em>.</p>
<p>This article is intended for junior to intermediate level developers looking to get some practical answers to difficult problems. You will not need extensive programming experience to get through it and you may skip over any technical discussions specifics. Those are meant to be helpful pieces of information, but the core of this article is about how to make these decisions in general using what I call: The Law of Suitability.</p>
<p>The topics I will cover are:</p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-find-the-best-anything">How to Find the Best Anything</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-how-to-find-the-best-water-bottle">How to Find the Best Water Bottle</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-find-the-best-programming-language">How to Find the Best Programming Language</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-navigating-public-and-expert-opinions">Navigating Public and Expert Opinions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-low-level-vs-high-level">Low Level vs High Level</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tightly-structured-static-vs-loosely-structured-dynamic">Tightly Structured (Static) vs Loosely Structured (Dynamic)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-popularity-is-only-one-factor">Popularity Is Only One Factor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-popularity-is-not-a-guarantee-of-employment">Popularity Is Not A Guarantee Of Employment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-hardware-amp-working-with-what-you-have">Hardware &amp; Working With What You Have</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-if-i-want-the-ai-to-code-for-me">What if I Want the AI to Code For Me?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-avoid-the-sunk-cost-fallacy">Avoid the Sunk-Cost Fallacy</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-find-the-best-libraries-and-frameworks">How to Find the Best Libraries and Frameworks</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-libraries-and-frameworks">What Are Libraries and Frameworks?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-choose-libraries-and-frameworks">How to Choose Libraries and Frameworks</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-find-the-best-programming-principles-and-practices">How to Find the Best Programming Principles and Practices</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-dry-dont-repeat-yourself">D.R.Y – Don’t Repeat Yourself</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-about-other-programming-principles">What About Other Programming Principles?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-a-note-about-patterns-and-architectures">A Note About Patterns and Architectures</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-find-the-best-software-architecture">How to Find the Best Software Architecture</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-design-pattern-trap">The Design Pattern Trap</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-how-to-find-the-best-anything">How to Find the Best Anything</h2>
<p>I invite you to follow along with some basic non-technical examples which lay the groundwork for the rest of this article. The examples may sound silly to some, but I have layered in some conceptual patterns which you can apply to programming languages, tools, and concepts – as well as just about anything where terms like good, bad, best, or worst can apply.</p>
<h3 id="heading-how-to-find-the-best-water-bottle">How to Find the Best Water Bottle</h3>
<p>Suppose that you are looking to solve the problem of staying hydrated and wish to purchase a water bottle. </p>
<p>You consider that figuring out the best water bottle might involve looking into:</p>
<ul>
<li><p>Public opinions and reviews</p>
</li>
<li><p>Expert opinions and reviews</p>
</li>
<li><p>The manufacturer’s descriptions and reputation</p>
</li>
<li><p>Buying and testing water bottles (though preferably not all of them, as that costs too much time and money, generally)</p>
</li>
</ul>
<p>All of those things are fair for consideration. Something which is not well-tested in public presents uncertainty. Expert reviews can help inform your decision, but you have to consider the biases and motivations of such experts. You also should consider if the manufacturer has a history of quality, design, and customer support, or whether they’re simply maximizing profit.</p>
<p>After doing some research, you come up with these options:</p>
<ul>
<li><p>A plastic bottle of water from a vending machine</p>
</li>
<li><p>A high-tech metal bottle which you can even boil stuff in!</p>
</li>
<li><p>A simple but ethically sourced, refillable, BPA-free plastic water bottle</p>
</li>
</ul>
<p>However, you noticed that nobody seemed to universally agree about which option was the best. There were usually some common opinions, but it was never the case that every expert had the same evaluation or recommendation.</p>
<p>After reflecting on this, it became obvious that you need to consider how, when, and where you will be using this water bottle. In other words, you need to <strong>consider the context or situation</strong> of its usage.</p>
<p>Suppose three different contexts within which you have to make this decision:</p>
<ul>
<li><p>You are standing in a rest stop in Death Valley California (often thought to be the hottest place on Earth in summer) and there is a vending machine full of micro-plastic filled, old, generic, and cheap plastic bottles of water in front of you. But you have no other options and are very thirsty</p>
</li>
<li><p>You are in a camping store preparing for a camping trip to New Zealand with lots of hiking and not too much access to filtered water</p>
</li>
<li><p>You are looking on your favorite shopping website for something you can bring to work each day to avoid those dehydration headaches from not drinking enough water</p>
</li>
</ul>
<p>In summary, you shouldn’t ignore second-hand knowledge, expert opinions, popularity, ratings, reviews, testimonials, and even first-hand experience. But you will never find the best water bottle for every situation you find yourself in. The best “anything” depends on the problem you are trying to solve and the context (words like requirements or situation also apply here) of that problem. </p>
<p>In other words, none of these things have an absolute or fixed value – their value is always relative. I call that the <strong>Law of Suitability</strong>.</p>
<p>Let’s now discuss some examples which are directly related to software design and development. </p>
<h2 id="heading-how-to-find-the-best-programming-language">How to Find the Best Programming Language</h2>
<p>The Law of Suitability applies just as much to programming languages as it does to water bottles – even though the details and contexts are different. There is no such thing as the best programming language for every person, team, problem, or feature. </p>
<p>But I do have some specific details and contexts to offer which may help you answer that question for yourself. This section will cover concrete details and some general ideas on how to choose a programming language. These patterns also apply to frameworks, libraries, and most other aspects of programming.</p>
<p>If you are not interested in the topic of finding a programming language, feel free to skip to the next sections on topics like libraries, principles, and patterns.</p>
<h3 id="heading-navigating-public-and-expert-opinions">Navigating Public and Expert Opinions</h3>
<p>Firstly, you need to be skeptical of popularity and the opinions of experts and “influencers” here. </p>
<p>My general tip here is to be extremely cautious about anyone who makes one of these claims:</p>
<ul>
<li><p>“X” language is the best (though saying “X” language is my favourite is perfectly acceptable)</p>
</li>
<li><p>“X” language is the worst, is terrible, is dead, is garbage, is useless, and so on.</p>
</li>
</ul>
<p>There are three groups of people who generally make these sorts of statements:</p>
<ul>
<li><p>Actual experts who are voicing their personal preferences but presenting them as immutable facts (how I wish this was not so common in this industry)</p>
</li>
<li><p>Non-experts parroting opinions of the above group or who have not yet understood the Law of Suitability</p>
</li>
<li><p>Engagement farmers</p>
</li>
</ul>
<p>It’s also worth noting that people can be experts in a subset of problems but that doesn’t guarantee their opinions about all problems are expert level. </p>
<p>This doesn’t mean you should reflexively dismiss expert opinions in general. Consider both the track records of the person and the degree to which they pay attention to the context of their statement. </p>
<p>Let us look at two examples:</p>
<ul>
<li><p><em>Expert A</em> says: “Python has the best tooling, support, and ecosystem for ML development”</p>
</li>
<li><p><em>Expert B</em> says: “Python is the best programming language”</p>
</li>
</ul>
<p>While <em>Expert B</em> is obviously displaying a lack of precision (either deliberately or not) if you have followed me so far, <em>Expert A</em> is a different case. Whether or not the statements made by <em>Expert A</em> are true (for the record, I have written a bit of Python but no ML code), you can tell that they are considering details and context. Look for people like <em>Expert A</em>!</p>
<h3 id="heading-low-level-vs-high-level">Low Level vs High Level</h3>
<p>In simple terms, low level programming languages are difficult for humans to read and write. Also, they tend to be faster and have a lower memory footprint than high level languages. Conversely, high level languages are closer to human language which generally makes them easier for humans to work with.</p>
<p>I must confess, though, that I have seen plenty of examples of people writing unintelligible code in high level languages – please don’t do that.</p>
<p>Someone working on an embedded system might want to do so in a language like C or C++ to optimize performance or work around the limitations of memory and processing power.</p>
<p>But in enterprise systems, which need to run on a variety of platforms and which closely intermingle with business requirements, rules, and real world objects (thing products, users, and so on), lower level languages are not so popular. After all, upper and middle management generally tends to care about low level optimizations only to the extent it affects the user experience. </p>
<p>I love optimization in general, but never forget that everything is fast with small datasets (that is, when <em>n</em> is small) or high processing power. In simpler terms, sometimes human concerns like legibility are significantly more important than insignificant optimizations on efficiency.</p>
<h3 id="heading-tightly-structured-static-vs-loosely-structured-dynamic">Tightly Structured (Static) vs Loosely Structured (Dynamic)</h3>
<p>Ironically, the main downsides and benefits of a language like Java (which is very structured and verbose) versus a language like JavaScript (quite the opposite, depending on how you use it) are the same depending on the context. </p>
<p>Speaking of enterprise systems, using structures, types, interfaces, classes, threading, concurrency primitives, and similar programming constructs can have a variety of benefits. It can insulate you from safety while providing some flexibility via type hierarchies, interfaces, protocols, abstractions and so on.</p>
<p>Further, studying design patterns can teach you repeatable solutions to problems which have been encountered since the dawn of the general purpose computer – or shortly thereafter. </p>
<p>But the Law of Suitability still applies here. Maybe you just need to write a quick script to migrate some data from one SQL database to another. Maybe you know how to approach problems using a more functional approach that doesn’t require or discourages the use of objects, classes, or structs. Maybe you realize one day that trying to apply design patterns, architectures, hierarchies and similar constructs in every situation has actually created as many problems as they were solving. More on that later.</p>
<p>It’s also worth mentioning that most modern language designers and maintainers have understood that we developers like flexibility. Many of us want to avoid premature optimization and unnecessary structure but also don’t want our code to blow up because we accidentally told the program to add together 1.23356 + “Rhinoceros”. </p>
<p>The main point is that structure or a lack of structure is both a blessing and a curse, depending on where it is used.</p>
<h3 id="heading-popularity-is-only-one-factor">Popularity Is Only One Factor</h3>
<p>I’m not going to say that popularity is irrelevant and that you should start with the least popular hipster programming language you can find. No shade intended to hipster programming languages, but unpopularity is not generally a good thing in isolation, either.</p>
<p>The key point is that many people weighing in on programming languages (probably most) don’t have abundant experience working on a variety of platforms, languages, and settings. If someone has only ever written Python and enjoys doing so, they will naturally tend to regard it above others.</p>
<p>We humans have a tendency to find the first thing that works for us and then die on a hill defending it. But to take a more anecdotal approach here, I know a couple dozen intermediate to senior level developers who have extensive experience in Java and other programming languages. Despite Java still being ranked as one of the most popular languages globally, only one of those developers I know actually prefers to write in Java if they have the choice. </p>
<p>Don’t make the mistake of assuming that the first thing that works for many people will be the last thing you need to try out. I have from time to time experimented with languages such as Haskell, which taught me many valuable lessons about the benefits of making my code more functional (and functionally pure) in nature. </p>
<p>But I have zero intention of using Haskell as my go to solution for building GUI applications. </p>
<h3 id="heading-popularity-is-not-a-guarantee-of-employment">Popularity Is Not A Guarantee Of Employment</h3>
<p>One of the most common things you ought to consider is whether or not the language you pick will help you get a job – assuming that’s a concern. Influencers absolutely love to tell people to choose one particular language because it has the most public commits on GitHub, or another because it has the largest number of programmers using it (which is in practice something that’s impossible to say for sure). </p>
<p>Let me flip that on its head: suppose that the most common programming language and platform combination is JavaScript and web. Let’s further suppose that we have pretty concrete data on the number of job postings on the web which confirms that the largest volume of jobs available is for that combination. Let’s finally suppose that for whatever reason, you strongly dislike JavaScript and enjoyed building a website using PHP. </p>
<p>You will find voices who will tell you that PHP is a dead language and a dead end for job searching. </p>
<p>But if you go looking, you may notice that there is a good supply of job postings out there looking for PHP developers who can expand and maintain existing codebases. You might also have a much better chance of getting an interview because <em>the ratio of job postings to applications is significantly better for PHP developers</em> than JavaScript developers. In fact, my team recently hired a PHP developer!</p>
<h3 id="heading-hardware-amp-working-with-what-you-have">Hardware &amp; Working With What You Have</h3>
<p>This section is largely irrelevant to web developers, but may be extremely important for those looking to target specific hardware or operating systems. Simply put, if you don’t have a computer with Mac OS and XCode, you will have a very hard time developing an iOS app, for example. In my case, back in 2014, I chose Android development partly because I had studied a bit of Java – though a big consideration was that I had an Android phone. </p>
<p>There are some ways around this by paying to use a remote device (such as a remote Mac via an online service), but my experience with such services years ago is that they weren’t great.</p>
<p>Think about what resources you have and how that fits into what you want to build or whom you want to work for. If you have not much other than a cheap computer with a web browser, and you still want to build GUI applications, web development can be a great choice.</p>
<h3 id="heading-what-if-i-want-the-ai-to-code-for-me">What if I Want the AI to Code For Me?</h3>
<p>While this topic is worthy of a separate article, I don’t believe this is an unreasonable question to ask. A year ago, I would have told you that at best, the AI can write some basic code and help you learn some things which may or may not be wrong. </p>
<p>How things have changed! Though I would be bad at my job if I copy pasted code I didn’t understand or didn’t test, AI has absolutely become a force multiplier for me as a developer. </p>
<p>Back to the topic in question, how does this relate to choosing a programming language? Well, after telling you that popularity is not always a big deal, by the nature of how LLMs work, popularity is a factor. In terms of general use, languages like Python, JavaScript, and Java are likely to have the largest amount of training data. My experience has been that the languages I typically use, such as Kotlin, TypeScript, and Swift also do fine.</p>
<p>But there is a curious side effect of developing Android or iOS applications that I don’t experience so much in web development. The nature of these constantly changing platforms and SDKs, with tens of thousands of third party libraries, dozens of architectures, and endless opinions about best practices and anti-patterns, means that LLMs can have serious troubles with complexity or specificity. </p>
<p>I expect this issue to be fixed as LLM services improve correctness checking or other methods to reduce hallucinations.</p>
<h3 id="heading-avoid-the-sunk-cost-fallacy">Avoid the Sunk-Cost Fallacy</h3>
<p>Perhaps the most important point I can make in choosing a programming language is to avoid the sunk-cost fallacy. For the first several years of my part-time studies, I didn’t imagine learning a second language based on how difficult it was for me to learn Java.</p>
<p>Roughly 12 years later, I have written non-trivial code in Java, Kotlin, Swift, C++, TypeScript, and SQL. Further, I have dabbled with code in C, Python, JavaScript, Racket, Haskell, Objective C, Visual Basic, and C#. </p>
<p>It was not the case that I sought out to learn all of these things artificially – I don’t tend to learn things outside of the problems in front of me. It’s that these learning opportunities naturally unfolded along with my personal and professional interests. </p>
<p>Learning the fundamentals or approaching mastery of any general purpose programming language will have carry over to others. It’s true that someone learning Python or JavaScript without CS fundamentals is not going to have much of a clue how things work at the OS level or lower. </p>
<p>It’s also true that I have met several people who could probably code circles around me in C/C++/Assembly but never made it past building toy programs in University or College. </p>
<p>Just keep learning and try to find a balance between personal interest and professional goals.</p>
<h2 id="heading-how-to-find-the-best-libraries-and-frameworks">How to Find the Best Libraries and Frameworks</h2>
<p>The next few topics revolve around a question which we’ll revisit a couple of times before the end of this article: “<em>Does it solve more problems than it creates?</em>”</p>
<h3 id="heading-what-are-libraries-and-frameworks">What Are Libraries and Frameworks?</h3>
<p>Before we proceed, here’s a useful but not definitive definition about the relationship between libraries and frameworks. You’ll find other definitions, but there’s remarkably little consensus on topics like this in this industry.</p>
<p>For me, a library is code which you can take from somewhere and use it to build things. It could be anything from a single line to a large and complex sub-system – usually something in between. I could give you a long and pedantic definition, but that’s not appropriate for this context (suitability!). </p>
<p>One example could be Java’s Math (java.lang.Math) library, which provides you with the following: “<em>The class Math contains methods for performing basic numeric operations such as the elementary exponential, logarithm, square root, and trigonometric functions.</em>”</p>
<p>Some people use the term framework interchangeably with library, and I don’t have any problems with that. When I think of a framework, I’m thinking about something which you build stuff around and is not necessarily to do with solving a specific problem domain (such as mathematics). </p>
<p>An example of this would be RxJava, which is a rather complex framework you can use to bind together and manage data flows across an entire application. I’ve used this framework in almost a dozen applications which did very different things in principle.</p>
<p>I do consider a framework to be a library, fundamentally – they just have a different set of goals and often a larger footprint.</p>
<h3 id="heading-how-to-choose-libraries-and-frameworks">How to Choose Libraries and Frameworks</h3>
<p>When I think about choosing libraries and frameworks, I ask myself these questions:</p>
<ul>
<li><p>Does it solve more problems than it creates compared to writing my own solution?</p>
</li>
<li><p>Is it well-maintained (regularly worked on, responsive authors, backed by tech companies)?</p>
</li>
<li><p>Does it have good documentation (less of a problem now that we can leverage AI for this purpose)?</p>
</li>
<li><p>What kind of footprint does it have?</p>
</li>
</ul>
<p>Let’s take two examples. I won’t refer to the specific platform or name of these libraries to avoid offending anyone. But they were/are both used in mobile development (though they are solving common GUI problems on any platform).</p>
<p>Firstly, one of my favorite libraries had one job: It loads images into the UI. </p>
<p>Although devices are more powerful than they used to be, it can still be a problem to load large images on smart phones for display. Mobile operating systems can be aggressive about killing programs (that is, processes) which use up too much of the system’s resources.</p>
<p>This library handles all aspects of loading images that I’m concerned about:</p>
<ul>
<li><p>Loading the image into a particular widget</p>
</li>
<li><p>Displaying an appropriate loading indicator </p>
</li>
<li><p>Displaying an optional error or fallback state that tells the user something went wrong</p>
</li>
<li><p>Handling the complexities of asynchronously loading in (via URL/URI), processing, and compressing potentially large streams of bits (that is, image data)</p>
</li>
<li><p>Doesn’t inflate the packaged application’s size unnecessarily</p>
</li>
<li><p>Doesn’t change its public API frequently (think changing function names which cause people’s implementations to break when updating versions)</p>
</li>
<li><p>It solves problems that I’m not interested in solving</p>
</li>
</ul>
<p>Secondly, one of my least favorite libraries also had one job: Pagination. Pagination, or paging, in this case refers to loading data in <em>chunks</em> into an application. This is an extremely common pattern in shopping cart or social media applications. </p>
<p>The library I am thinking of approaches that problem like so:</p>
<ul>
<li><p>Tightly couples every layer of your client application (from front end to back end) to its dependencies</p>
</li>
<li><p>This tight coupling makes testing difficult without jumping through some hoops</p>
</li>
<li><p>Handles the core problem of pagination well unless you need customization or specialized cases</p>
</li>
<li><p>Frequently changed its public facing API</p>
</li>
<li><p>Solved (in general) a problem which I am quite happy to write my own solution for</p>
</li>
<li><p>Did not play well with other frameworks due to a restrictive set of types and lack of flexibility</p>
</li>
<li><p>Was constantly updated for a couple of years then ditched and marked deprecated</p>
</li>
<li><p>Did not inflate the packaged applications size too much but certainly more than my own solution would</p>
</li>
</ul>
<p>As you can see, even something which solves a core problem reasonably well, can still fail this simple test of: Does it solve more problems than it creates? Having written pagination code by myself on a couple of occasions now, I would have to be pretty strongly convinced not to. </p>
<h2 id="heading-how-to-find-the-best-programming-principles-and-practices">How to Find the Best Programming Principles and Practices</h2>
<p>There are more best practices and principles than I care to describe in detail. What I will do is explain why I treat programming principles as being distinct from immutable/unbreakable laws. Similarly to my goal of finding the best programming language, I wanted to find the best principles in order to write the best code. </p>
<p>The problem is that any programming principle I’ve come across has also been subject to the Law of Suitability. I’ll discuss one example from personal experience and point out that the question we asked above, “does it solve more problems than creates,” also applies here.</p>
<h3 id="heading-dry-dont-repeat-yourself">D.R.Y – Don’t Repeat Yourself</h3>
<p>This principle can be summarized with the idea that if you find duplicated code, you should pull it into a separate module (file, function, class, library, and so on). Without getting into the weeds, the act of pulling the duplicated code into a separate module can be thought of as a process of abstraction. </p>
<p>To be fair to the creators and proponents of this idea, it’s more nuanced than that. But many developers never bother to dig that deep into nuances – nor should they have to. I ran into the nuances simply by applying this idea more than I should have.</p>
<p>There a couple of cases where code duplication is sometimes preferable:</p>
<ul>
<li><p>You have a set of similar modules (say similar widgets or business rules) but they get used in different places for different reasons</p>
</li>
<li><p>You have a set of similar modules which might change for different reasons (for example, rapidly changing demands from product teams and clients with different priorities) </p>
</li>
<li><p>You’re deliberately grouping certain modules that work together in distinct packages, files, or directories to insulate modules/groupings from affecting each other</p>
</li>
<li><p>You find that you need to add details about one particular implementation into your abstraction, but those details don’t apply to other implementations (that is, it’s a bad abstraction)</p>
</li>
</ul>
<p>All of the things I listed above are summaries of things I have run into in the past. The key take away is that I broadly agree with avoiding code duplication. I also know of some cases where I prefer it. Suitability!</p>
<h3 id="heading-what-about-other-programming-principles">What About Other Programming Principles?</h3>
<p>In general, you can think of all programming principles like YAGNI, DRY, SRP (and other aspects of SOLID), and even software development methodologies like AGILE and Waterfall in the same way. Contextually, you can use them as guidelines to help avoid some common problems. But a person of sufficient creativity and experience can come up with a situation where following any of these principles creates more problems than it solves.</p>
<p>In many cases, you need to apply these things too much in order to understand what too much means in practical terms. Just be careful not to swing too far in the other direction when one of these principles really breaks down in front of you. I’ve also made that mistake and had to re-adjust.</p>
<p>To date, I haven’t come across a programming principle which is universally true. There are some which come close to that, but I can always imagine a situation where they’re not the best approach. Take a good one like: Always write the simplest code you can write. In other words, don’t add extra complexity without a reason. </p>
<p>Well, suppose you have a not great value system or incentive structure which encourages inflating your work artificially. Need I say more?</p>
<h2 id="heading-a-note-about-patterns-and-architectures">A Note About Patterns and Architectures</h2>
<p>I’ll now discuss the topic of patterns and architectures in software systems with respect to the Law of Suitability. Software architecture is the only thing I consider myself an expert in, and I have read multiple books on design patterns. I always try to provide some useful info on these topics when I get the chance.</p>
<h3 id="heading-how-to-find-the-best-software-architecture">How to Find the Best Software Architecture</h3>
<p>To summarize entire articles, courses, and public talks I have given on this topic: The best software architecture depends on project and personal requirements. </p>
<p>One way to grasp the main idea is to ask yourself whether the best architecture for a hospital is also a good fit for a 2-bedroom apartment. The obvious answer is that we might expect a few commonalities (doors, windows, bathrooms of some kind, and so on) among these different sets of requirements. But the ideal, or even just a good architecture for a 2 bedroom apartment cannot possibly be the same for a hospital. </p>
<p>In short, you will never find an architecture which works well for all projects and requirements.</p>
<p>Here is a list of architectures I have some familiarity with:</p>
<ul>
<li><p>Model-View-Controller</p>
</li>
<li><p>Model-View-Presenter</p>
</li>
<li><p>Model-View-ViewModel</p>
</li>
<li><p>VIPER</p>
</li>
<li><p>Clean Architecture (Robert C. Martin style)</p>
</li>
<li><p>Model-View-Intent</p>
</li>
</ul>
<p>To make matters more confusing, there are multiple different ways to implement these architectures – almost as many ways as developers implementing them! M-V-VM is one of the more common architectures in mobile development, and I can think of at least five different variations on how to achieve what some people think of as a single architecture.</p>
<p>Here are my general suggestions for working with these architectures:</p>
<ul>
<li><p>Be wary of adding unnecessary complexity with the more complex architectures (particularly Clean Architecture, as many people get this horribly wrong)</p>
</li>
<li><p>Don’t try to make the project requirements fit the architecture – work the other way around (the best indicator for this is noticing that something you’re trying to implement is made unnecessarily difficult because of the architecture you’re using)</p>
</li>
<li><p>Don’t be afraid of applying different approaches in different features of the same application instead of blindly applying the same pattern just for consistency’s sake</p>
</li>
</ul>
<h3 id="heading-the-design-pattern-trap">The Design Pattern Trap</h3>
<p>One of the most common engagement farming tactics I see on social media is to post lists of design patterns “that you must know” in order to get a job or to scare junior programmers into buying your low-quality, copy-pasted content of each pattern.</p>
<p>Don’t get me wrong, I loved studying design patterns and I use a couple key patterns in most GUI applications I build. The Observer (a.k.a. Publisher-Subscriber or Pub-Sub) Pattern really shines when you need to glue together a bunch of asynchronous data sources. I love seeing library developers give me a nice Builder Pattern to work with their APIs. I think understanding the basics of patterns like the Bridge or Facade can teach you how to hide details behind abstractions, which is actually simpler than the big scary words describing these things make it sound.</p>
<p>But I spend very little time in my day to day work thinking about or in design patterns. Instead, I’m always thinking about the kinds of attitudes and principles that give rise to these patterns: </p>
<ul>
<li><p>Promoting loosely coupled code (separating the creation and usage of dependencies and parameters, reasonable usage of abstraction)</p>
</li>
<li><p>Writing classes, interfaces, protocols, and functions which do one thing (though this “one thing” might be a macroscopic goal instead of a microscopic operation)</p>
</li>
<li><p>Avoiding complexity wherever possible (a common source of this complexity is over-use of abstractions)</p>
</li>
<li><p>Not pretending that every complex problem has a simple solution (that is, as simple as it can be but no simpler)</p>
</li>
<li><p>Avoiding pre-mature optimization</p>
</li>
</ul>
<p>Again, I will only apply these principles and attitudes to the extent that I find they solve more problems than they create. Design patterns, when applied too rigorously, can break many of those principles – particularly when it comes to avoiding complexity and pre-mature optimization. </p>
<p>Don’t try to make your project requirements fit your patterns. Instead, think about which patterns might suit your project requirements and deviate as necessary.</p>
<h2 id="heading-summary">Summary</h2>
<p>My goal with this piece was to provide three things:</p>
<ul>
<li><p>A practical overview of choosing a programming language and avoiding the traps the people can fall into when navigating these sorts of topics</p>
</li>
<li><p>A philosophical but pragmatic framework which you can use to evaluate the suitability of anything – with emphasis on learning and developing software</p>
</li>
<li><p>A breakdown of how I approach other topics like tools, architectures, and patterns</p>
</li>
</ul>
<p>While it can be important to consider things like job opportunities and your current hardware, don’t discount personal interest as a driving factor. From what little I remember about studying cognition (learning how to learn), interest is tightly coupled to motivation and memory. We can’t always exclusively do what interests us, but I suggest you look for intersections between personal and practical concerns as often as you can. </p>
<p>In closing, I encourage you to think about other areas where you might be able to explore the principles of suitability and the problems of tribalistic thinking. Change is constant and value is relative.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Zero-Trust Authentication in Your Web Apps ]]>
                </title>
                <description>
                    <![CDATA[ Your biggest security problem might be inside your own network. Hackers don't break in anymore - they just log in with stolen passwords. Old security systems trusted anyone who got inside the network. But now there's no clear "inside" or "outside." P... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-implement-zero-trust-authentication-in-your-web-apps/</link>
                <guid isPermaLink="false">6893afcc4ff769448b46934a</guid>
                
                    <category>
                        <![CDATA[ zerotrust ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #cybersecurity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tope Fasasi ]]>
                </dc:creator>
                <pubDate>Wed, 06 Aug 2025 19:41:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754503273007/1b04e262-05de-4fac-be47-56c01eb44446.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Your biggest security problem might be inside your own network. Hackers don't break in anymore - they just log in with stolen passwords. Old security systems trusted anyone who got inside the network. But now there's no clear "inside" or "outside." People work from home, use cloud services, and fall for fake emails. Attackers can pretend to be real users for weeks without being caught.</p>
<p>Zero-Trust Authentication fixes this. Instead of trusting people once they log in, it checks every person, every device, and every request, every single time. The rule is simple: "Trust no one, verify everything."</p>
<p>This isn't just theory – it works. Companies using zero-trust security have smaller breaches, meet compliance rules easier, and control who sees what data. This matters because <a target="_blank" href="https://www.securityweek.com/cost-of-data-breach-in-2024-4-88-million-says-latest-ibm-study/">95% of data breaches happen due to human mistakes, and the average breach now costs $4.88 million</a>.</p>
<p>In this article, you will learn how to build a complete Zero-Trust Authentication system into your web app step by step. From multi-factor authentication (MFA) to behavioral anomaly detection, we will discuss the architecture decisions, code examples, and some real-world approaches you are likely able to implement right away.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-zero-trust-authentication">What Is Zero-Trust Authentication?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-architecture-overview">Architecture Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-multi-factor-authentication-mfa">Multi-factor Authentication (MFA)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-jwt-token-management">JWT Token Management</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-session-security">Session Security</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-role-based-access-control-rbac">Role-Based Access Control (RBAC)</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-using-middleware-to-enforce-rbac">Using Middleware to Enforce RBAC</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-access-control-logic">Testing Access Control Logic</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-continuous-verification">Continuous Verification</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-behavioral-analysis">Behavioral Analysis</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-up-authentication">Step-Up Authentication</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-security-monitoring">Security Monitoring</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-automating-threat-response">Automating Threat Response</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before implementing zero-trust, make sure your stack aligns with frequent calls for token checks, volumes of logging, and the additional auth step, all without impairing system performance on the users' end.</p>
<p>You should at least have knowledge of:</p>
<ul>
<li><p>JWT and secure session handling</p>
</li>
<li><p>MFA, specifically understanding TOTP</p>
</li>
<li><p>Basic understanding of middleware design</p>
</li>
</ul>
<p>Audit your system: examine login flows, token handling, protected routes, session termination, and identify weak spots like long sessions or unprotected routes.</p>
<h2 id="heading-what-is-zero-trust-authentication">What Is Zero-Trust Authentication?</h2>
<p><a target="_blank" href="https://www.civilsdaily.com/news/what-is-zero-trust-authentication-zta/">Zero-Trust Authentication</a> (ZTA) redefines how access is granted in contemporary applications. It doesn't take network location or a single login event into account – it demands the continuous validation of an identity, context, and intent.</p>
<p>Whereas perimeter-based models consider anyone inside a network "safe," zero-trust presumes every request can be compromised. This means that access decisions are made in real time over verified identity, device posture, and behavioral signals. In short, it’s a "security-first" approach designed for a cloud-native, threat-aware world.</p>
<h2 id="heading-architecture-overview">Architecture Overview</h2>
<p>Building a ZTA system means checking everyone and everything, all the time. The architecture you can see below demonstrates this "never trust, always verify" approach in action:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752183554393/4cfda450-14d8-49e3-944b-a0e4654a3dcc.png" alt="Zero Trust Security architecture diagram showing trust boundary encompassing internal network components, with external cloud services and internet connections, illustrating key zero trust principles" class="image--center mx-auto" width="779" height="401" loading="lazy"></p>
<p>Image source: <a target="_blank" href="https://www.civilsdaily.com/news/what-is-zero-trust-authentication-zta/">civilsdaily</a></p>
<p>Here's how it works:</p>
<ul>
<li><p>Every request gets checked: When anyone tries to access your network (from office, home, or mobile), they hit the authentication layer first. No exceptions.</p>
</li>
<li><p>Identity + context verification: The system doesn't just check passwords. It looks at who you are, what device you're using, where you're connecting from, and what you're trying to access.</p>
</li>
<li><p>Continuous protection: Once inside, the system keeps watching. It protects your data, devices, networks, people, and workloads through constant monitoring and access controls.</p>
</li>
<li><p>The big change: Traditional security created a "trusted inside" and "untrusted outside." Zero-trust eliminates this boundary. Whether you're connecting to cloud services (AWS, Office 365) or internal systems, every request goes through the same verification process.</p>
</li>
</ul>
<h2 id="heading-multi-factor-authentication-mfa">Multi-factor Authentication (MFA)</h2>
<p><a target="_blank" href="https://support.microsoft.com/en-gb/topic/what-is-multifactor-authentication-e5e39437-121c-be60-d123-eda06bddf661">MFA</a> is the foundation of zero-trust security. It requires users to prove who they are with multiple pieces of evidence before getting access. In ZTA, even the strongest password isn't enough on its own.</p>
<p>To begin, start with a strong password, then add a second factor. For example, <a target="_blank" href="https://en.wikipedia.org/wiki/Time-based_one-time_password">Time-based One-Time Password (TOTP)</a> is the most secure. TOTP is the best second factor because it works offline and doesn't rely on SMS or email (which can be intercepted). Apps like Google Authenticator generate a new code every 30 seconds.</p>
<p>Here’s an example of what that would look like:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> speakeasy = <span class="hljs-built_in">require</span>(<span class="hljs-string">'speakeasy'</span>);
<span class="hljs-keyword">const</span> QRCode = <span class="hljs-built_in">require</span>(<span class="hljs-string">'qrcode'</span>);

<span class="hljs-comment">// Generate TOTP secret for new user</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateTOTPSecret</span>(<span class="hljs-params">userEmail</span>) </span>{
  <span class="hljs-keyword">const</span> secret = speakeasy.generateSecret({
    <span class="hljs-attr">name</span>: userEmail,
    <span class="hljs-attr">issuer</span>: <span class="hljs-string">'YourApp'</span>,
    <span class="hljs-attr">length</span>: <span class="hljs-number">32</span>
  });

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">secret</span>: secret.base32,
    <span class="hljs-attr">qrCodeUrl</span>: secret.otpauth_url
  };
}
</code></pre>
<p>When a new user signs up, this function creates a unique secret key just for them. The <code>name</code> is their email, <code>issuer</code> is your app name, and <code>length: 32</code> makes it extra secure. It returns two things: the secret key (in base32 format) and a special URL that creates a QR code for easy setup.</p>
<p>To verify the code from their app, you check it against the stored secret:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Verify TOTP token</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyTOTP</span>(<span class="hljs-params">token, secret</span>) </span>{
  <span class="hljs-keyword">return</span> speakeasy.totp.verify({
    <span class="hljs-attr">secret</span>: secret,
    <span class="hljs-attr">token</span>: token,
    <span class="hljs-attr">window</span>: <span class="hljs-number">2</span>,
    <span class="hljs-attr">encoding</span>: <span class="hljs-string">'base32'</span>
  });
}
</code></pre>
<p>When the user enters their 6-digit code, this function checks if it's correct. The <code>window: 2</code> is smart – it allows for timing differences (like if their phone clock is slightly off). It returns true if the code is valid, false if not.</p>
<p>SMS verification can serve as a backup option. It’s less secure than TOTP but can work as a backup. Always limit how many SMS codes someone can request to prevent abuse:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// SMS verification with rate limiting</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendSMSVerification</span>(<span class="hljs-params">phoneNumber, userId</span>) </span>{
  <span class="hljs-keyword">const</span> attempts = <span class="hljs-keyword">await</span> getRecentSMSAttempts(userId);
  <span class="hljs-keyword">if</span> (attempts &gt;= <span class="hljs-number">3</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Too many SMS attempts. Please try again later.'</span>);
  }

  <span class="hljs-keyword">const</span> code = generateRandomCode(<span class="hljs-number">6</span>);
  <span class="hljs-keyword">await</span> storeSMSCode(userId, code, <span class="hljs-number">300</span>); <span class="hljs-comment">// 5-minute expiry</span>

  <span class="hljs-keyword">await</span> smsProvider.send(phoneNumber, <span class="hljs-string">`Your verification code: <span class="hljs-subst">${code}</span>`</span>);
}
</code></pre>
<p>Before sending an SMS, it checks how many times this user has already requested codes. If they've tried 3 times, it blocks them (prevents spam/abuse). If they're under the limit, it creates a random 6-digit code, saves it for 5 minutes (300 seconds), then sends it via SMS.</p>
<p>But what happens if a user loses their phone or authenticator app? Backup codes provide emergency access:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Generate backup codes</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateBackupCodes</span>(<span class="hljs-params">userId</span>) </span>{
  <span class="hljs-keyword">const</span> codes = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) {
    codes.push(generateRandomCode(<span class="hljs-number">8</span>));
  }

  <span class="hljs-keyword">const</span> hashedCodes = codes.map(<span class="hljs-function"><span class="hljs-params">code</span> =&gt;</span> hashCode(code));
  storeBackupCodes(userId, hashedCodes);

  <span class="hljs-keyword">return</span> codes; <span class="hljs-comment">// Only show to user once</span>
}
</code></pre>
<p>This creates 10 emergency backup codes (each 8 characters long). The <code>for</code> loop runs 10 times, creating a new random code each time. Before storing them in the database, it "hashes" them (scrambles them for security). Then it returns the original codes to show the user once, but stores the scrambled versions so even if someone hacks your database, they can't see the real codes.</p>
<h2 id="heading-jwt-token-management">JWT Token Management</h2>
<p>JSON Web Tokens (JWTs) are stateless authentication in a zero-trust system. Using them safely is critical because you need to carefully think through payload design, implement short expiration policies, and implement token rotation and blocklisting that could prevent token theft, token reuse, or privilege escalation.</p>
<p>Let's walk through how to securely implement and manage JWTs in your web application.</p>
<p>First, define a minimal and secure structure for your access tokens. Only add information that’s necessary for making authorization decisions, and never put anything sensitive even if it is encrypted.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// JWT payload structure</span>
<span class="hljs-keyword">const</span> tokenPayload = {
  <span class="hljs-attr">sub</span>: userId,           <span class="hljs-comment">// Subject (user ID)</span>
  <span class="hljs-attr">email</span>: userEmail,      <span class="hljs-comment">// User identifier</span>
  <span class="hljs-attr">roles</span>: userRoles,      <span class="hljs-comment">// User roles array</span>
  <span class="hljs-attr">permissions</span>: userPermissions, <span class="hljs-comment">// Specific permissions</span>
  <span class="hljs-attr">iat</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>), <span class="hljs-comment">// Issued at</span>
  <span class="hljs-attr">exp</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>) + <span class="hljs-number">900</span>, <span class="hljs-comment">// Expires in 15 minutes</span>
  <span class="hljs-attr">jti</span>: generateUniqueId(), <span class="hljs-comment">// JWT ID for blocklisting</span>
  <span class="hljs-attr">aud</span>: <span class="hljs-string">'your-app'</span>,       <span class="hljs-comment">// Audience</span>
  <span class="hljs-attr">iss</span>: <span class="hljs-string">'your-auth-service'</span> <span class="hljs-comment">// Issuer</span>
};
</code></pre>
<p>In the code above, the payload consists of the user identity, roles, permissions, and metadata such as the issued time (<code>iat</code>), expiration (<code>exp</code>), and unique token ID (<code>jti</code>). While <code>aud</code> and <code>iss</code> describe the token's origin and audience for validation, <code>jti</code> is used for revocation. Thus, it keeps the payload as lean as possible to minimize exposure and overhead.</p>
<p>For security and usability, it’s better to use access tokens with a short lifespan and refresh tokens with a considerably longer duration, which minimizes the window for potential utilization of compromised tokens while providing a smooth user session.</p>
<p>Let's take this example:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Token generation service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TokenService</span> </span>{
  generateTokenPair(user) {
    <span class="hljs-keyword">const</span> accessToken = jwt.sign(
      <span class="hljs-built_in">this</span>.createAccessTokenPayload(user),
      process.env.JWT_SECRET,
      { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'15m'</span>, <span class="hljs-attr">algorithm</span>: <span class="hljs-string">'HS256'</span> }
    );

    <span class="hljs-keyword">const</span> refreshToken = jwt.sign(
      { <span class="hljs-attr">sub</span>: user.id, <span class="hljs-attr">type</span>: <span class="hljs-string">'refresh'</span> },
      process.env.REFRESH_SECRET,
      { <span class="hljs-attr">expiresIn</span>: <span class="hljs-string">'7d'</span>, <span class="hljs-attr">algorithm</span>: <span class="hljs-string">'HS256'</span> }
    );

    <span class="hljs-keyword">return</span> { accessToken, refreshToken };
  }

  <span class="hljs-keyword">async</span> refreshAccessToken(refreshToken) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

      <span class="hljs-comment">// Check if refresh token is blocklisted</span>
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.isTokenBlocklisted(decoded.jti)) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Token has been revoked'</span>);
      }

      <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(decoded.sub);
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.generateTokenPair(user);
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid refresh token'</span>);
    }
  }
}
</code></pre>
<p><code>generateTokenPair</code> will generate two signed JWTs – that is, an access token with a 15-minute expiration and a refresh token with a validity of 7 days. The refresh tokens are verified to grant new ones and are checked against a blocklist. This ensures that revoked tokens can’t be reused, even if they’re still technically valid.</p>
<p>If you choose, a sliding session can be implemented to reduce friction by renewing tokens for an active user without violating your expiration strategy.</p>
<p>Now, let's implement a <a target="_blank" href="https://stackoverflow.com/questions/48189866/sliding-session-on-web-api-request">sliding session</a> that automatically refreshes JWTs when they're close to expiring and the user is still active.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Sliding session implementation</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extendSessionIfActive</span>(<span class="hljs-params">token</span>) </span>{
  <span class="hljs-keyword">const</span> decoded = jwt.decode(token);
  <span class="hljs-keyword">const</span> timeUntilExpiry = decoded.exp - <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>);

  <span class="hljs-comment">// If token expires within 5 minutes and user is active, refresh</span>
  <span class="hljs-keyword">if</span> (timeUntilExpiry &lt; <span class="hljs-number">300</span> &amp;&amp; <span class="hljs-keyword">await</span> isUserActive(decoded.sub)) {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(decoded.sub);
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.generateTokenPair(user);
  }

  <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
}
</code></pre>
<p>The above function checks for token expiration. If the token expires within 5 minutes and the user continues to interact, a new access token pair is issued. This way, the session is kept alive during real activity but still forces expiration for idle users.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Token blocklist service</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TokenBlocklistService</span> </span>{
  <span class="hljs-keyword">async</span> blocklistToken(token) {
    <span class="hljs-keyword">const</span> decoded = jwt.decode(token);
    <span class="hljs-keyword">const</span> expiresAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(decoded.exp * <span class="hljs-number">1000</span>);

    <span class="hljs-comment">// Store in Redis with automatic expiry</span>
    <span class="hljs-keyword">await</span> redis.setex(
      <span class="hljs-string">`blocklist:<span class="hljs-subst">${decoded.jti}</span>`</span>,
      <span class="hljs-built_in">Math</span>.max(<span class="hljs-number">0</span>, <span class="hljs-built_in">Math</span>.floor((expiresAt - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>)),
      <span class="hljs-string">'revoked'</span>
    );
  }

  <span class="hljs-keyword">async</span> isTokenBlocklisted(jti) {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> redis.get(<span class="hljs-string">`blocklist:<span class="hljs-subst">${jti}</span>`</span>);
    <span class="hljs-keyword">return</span> result !== <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<p>In the above code, when users log out or tokens are compromised, the <code>jti</code> is stored in <a target="_blank" href="https://redis.io/docs/latest/">Redis</a> with an expiration time of the remaining life of the token. You can block future uses of a token by checking if its ID exists on the blocklist. This allows for instant invalidation, even though JWTs are stateless.</p>
<h2 id="heading-session-security">Session Security</h2>
<p>In zero-trust environments, <a target="_blank" href="https://www.descope.com/learn/post/session-management">session management</a> goes far beyond keeping users logged in. A session must be treated as a constantly evaluated contract between the user, their device, and the system – and should be revoked the moment trust breaks down.</p>
<p>Here, we’ll build a session system that incorporates adaptive <a target="_blank" href="https://www.prove.com/blog/trust-score">trust scoring</a>, dynamic timeouts, real-time visibility, and <a target="_blank" href="https://www.researchgate.net/publication/354720916_Revocation_Mechanisms_for_Blockchain_Applications_A_Review">revocation mechanisms</a> – all aligned with zero-trust principles.</p>
<p>For example, when a user successfully authenticates, you don’t just store a session ID. Instead, you collect contextual metadata to evaluate ongoing risk. The function below demonstrates how to initialize a session that’s both secure and context-aware.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Comprehensive session creation</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createSecureSession</span>(<span class="hljs-params">userId, deviceInfo, clientInfo</span>) </span>{
  <span class="hljs-keyword">const</span> sessionId = generateSecureSessionId();

  <span class="hljs-keyword">const</span> session = {
    <span class="hljs-attr">id</span>: sessionId,
    <span class="hljs-attr">userId</span>: userId,
    <span class="hljs-attr">deviceFingerprint</span>: generateDeviceFingerprint(deviceInfo),
    <span class="hljs-attr">ipAddress</span>: clientInfo.ipAddress,
    <span class="hljs-attr">userAgent</span>: clientInfo.userAgent,
    <span class="hljs-attr">location</span>: <span class="hljs-keyword">await</span> resolveLocation(clientInfo.ipAddress),
    <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-attr">lastActivity</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
    <span class="hljs-attr">trustScore</span>: calculateInitialTrustScore(deviceInfo, clientInfo),
    <span class="hljs-attr">securityLevel</span>: determineSecurityLevel(userId, deviceInfo)
  };

  <span class="hljs-keyword">await</span> storeSession(session);
  <span class="hljs-keyword">return</span> session;
}
</code></pre>
<p>Many other tools are tracking concerning details during session creation. The device fingerprint, IP address, geolocation, and browser agent data are collected. These metadata are used to compute a trust score, and finally, a security level is assigned to the session to be used for dynamically adjusting policies later.</p>
<p>With this contextual information captured during session creation, the system can spot suspicious behavior during the sessions and, in turn, adapt policies like re-authentication of users or termination of the session.</p>
<p>Not all sessions should be treated equally. If a user logs in via an unfamiliar device or risky location, they should have less time for their session lifespan compared to a trusted setup's time. The following implementation changes timeout periods on the basis of trust and risk factors:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Adaptive session timeout</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionTimeoutManager</span> </span>{
  calculateTimeoutPeriod(session) {
    <span class="hljs-keyword">const</span> baseTimeout = <span class="hljs-number">30</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>; <span class="hljs-comment">// 30 minutes</span>
    <span class="hljs-keyword">const</span> trustMultiplier = session.trustScore / <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> securityMultiplier = <span class="hljs-built_in">this</span>.getSecurityMultiplier(session.securityLevel);

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.max(
      <span class="hljs-number">5</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>, <span class="hljs-comment">// Minimum 5 minutes</span>
      baseTimeout * trustMultiplier * securityMultiplier
    );
  }

  <span class="hljs-keyword">async</span> checkSessionValidity(sessionId) {
    <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getSession(sessionId);
    <span class="hljs-keyword">if</span> (!session) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now();
    <span class="hljs-keyword">const</span> timeout = <span class="hljs-built_in">this</span>.calculateTimeoutPeriod(session);

    <span class="hljs-comment">// Check both idle timeout and absolute timeout</span>
    <span class="hljs-keyword">const</span> idleExpired = (now - session.lastActivity) &gt; timeout;
    <span class="hljs-keyword">const</span> absoluteExpired = (now - session.createdAt) &gt; <span class="hljs-number">8</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>; <span class="hljs-comment">// 8 hours max</span>

    <span class="hljs-keyword">return</span> !idleExpired &amp;&amp; !absoluteExpired;
  }
}
</code></pre>
<p>The above code keeps session duration adaptable to the risk context at hand. The timeout is calculated by adjusting the base value according to trust and security level, while imposing minimum and maximum bounds.</p>
<p>The system then periodically intervenes to see if the session has become invalid due to inactivity (idle timeout) or simply outlives its initial duration (absolute timeout). This provides a more flexible yet enforceable way of mitigating the risk behind stale or hijacked sessions.</p>
<p>Zero-trust should also mean visibility across all access points. The user should be able to view all active sessions associated with their account, and security systems should also allow them to control these sessions in fine-grained detail. The following code lets you manage those active sessions across devices.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Cross-device session management</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionManager</span> </span>{
  <span class="hljs-keyword">async</span> getUserSessions(userId) {
    <span class="hljs-keyword">const</span> sessions = <span class="hljs-keyword">await</span> getActiveSessionsForUser(userId);

    <span class="hljs-keyword">return</span> sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> ({
      <span class="hljs-attr">id</span>: session.id,
      <span class="hljs-attr">deviceType</span>: <span class="hljs-built_in">this</span>.identifyDeviceType(session.userAgent),
      <span class="hljs-attr">location</span>: session.location,
      <span class="hljs-attr">lastActivity</span>: session.lastActivity,
      <span class="hljs-attr">current</span>: session.id === currentSessionId
    }));
  }

  <span class="hljs-keyword">async</span> revokeSession(sessionId, requestingSessionId) {
    <span class="hljs-keyword">const</span> session = <span class="hljs-keyword">await</span> getSession(sessionId);
    <span class="hljs-keyword">if</span> (!session) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Session not found'</span>);

    <span class="hljs-comment">// Verify requesting session has permission</span>
    <span class="hljs-keyword">const</span> requestingSession = <span class="hljs-keyword">await</span> getSession(requestingSessionId);
    <span class="hljs-keyword">if</span> (requestingSession.userId !== session.userId) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Unauthorized'</span>);
    }

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.terminateSession(sessionId);
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'session_revoked'</span>, session);
  }
}
</code></pre>
<p>Here, users fetch a list of their active sessions along with identifying information such as device type and location. Any session can be securely revoked by the user who owns it, preventing unauthorized access if the session ID is compromised.</p>
<p>This also allows the user to detect suspicious activities in time. All revocations are logged for auditing purposes to enable post-incident investigations as well as compliance reports.</p>
<p>When a trust breaks due to credential theft, suspicious activity, or user-level actions such as password reset, all sessions have to be immediately revoked. This example guarantees a full revocation, promptly applied to all devices:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Real-time session revocation</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionRevocationService</span> </span>{
  <span class="hljs-keyword">async</span> revokeAllUserSessions(userId, reason) {
    <span class="hljs-keyword">const</span> sessions = <span class="hljs-keyword">await</span> getActiveSessionsForUser(userId);

    <span class="hljs-comment">// Blocklist all tokens for this user</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> 
      <span class="hljs-built_in">this</span>.blocklistSessionTokens(session.id)
    ));

    <span class="hljs-comment">// Notify all active clients</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(sessions.map(<span class="hljs-function"><span class="hljs-params">session</span> =&gt;</span> 
      <span class="hljs-built_in">this</span>.notifySessionTermination(session.id, reason)
    ));

    <span class="hljs-comment">// Clear session data</span>
    <span class="hljs-keyword">await</span> clearUserSessions(userId);

    <span class="hljs-comment">// Log security event</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'all_sessions_revoked'</span>, {
      userId,
      reason,
      <span class="hljs-attr">sessionCount</span>: sessions.length
    });
  }
}
</code></pre>
<p>The above code permits full-scale revocation. It blocklists all session tokens, sends out termination notices to active clients (for example, through WebSockets), clears the session records on the server-side, and logs the event for auditing. It is an instantaneous and complete response to compromised accounts or states where user risk is very high. It is the foremost component of real-time zero-trust enforcement in any serious authentication system.</p>
<h2 id="heading-role-based-access-control-rbac">Role-Based Access Control (RBAC)</h2>
<p>Identity verification determines what users can access once they’re logged in. As the basis for any system that is aware of permissions and follows least privilege, <a target="_blank" href="https://en.wikipedia.org/wiki/Role-based_access_control">RBAC</a> doesn’t grant access on an individual basis – it groups users into roles that define the operations they are permitted to perform.</p>
<p>Before assigning roles to users, you need a structured system to define what each role can do. A set of granular permissions is first identified and then aggregated under these roles, optionally allowing inheritance and hierarchy. The code below shows how to build a basic permission system:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// RBAC permission system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PermissionSystem</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
    <span class="hljs-built_in">this</span>.roles = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
    <span class="hljs-built_in">this</span>.roleHierarchy = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
  }

  <span class="hljs-comment">// Define granular permissions</span>
  definePermission(name, description, resource, action) {
    <span class="hljs-built_in">this</span>.permissions.set(name, {
      name,
      description,
      resource,
      action,
      <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    });
  }

  <span class="hljs-comment">// Create role with inherited permissions</span>
  createRole(name, description, parentRole = <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">const</span> role = {
      name,
      description,
      <span class="hljs-attr">permissions</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(),
      <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    };

    <span class="hljs-comment">// Inherit permissions from parent role</span>
    <span class="hljs-keyword">if</span> (parentRole &amp;&amp; <span class="hljs-built_in">this</span>.roles.has(parentRole)) {
      <span class="hljs-keyword">const</span> parent = <span class="hljs-built_in">this</span>.roles.get(parentRole);
      role.permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(parent.permissions);
      <span class="hljs-built_in">this</span>.roleHierarchy.set(name, parentRole);
    }

    <span class="hljs-built_in">this</span>.roles.set(name, role);
    <span class="hljs-keyword">return</span> role;
  }

  <span class="hljs-comment">// Add permission to role</span>
  addPermissionToRole(roleName, permissionName) {
    <span class="hljs-keyword">const</span> role = <span class="hljs-built_in">this</span>.roles.get(roleName);
    <span class="hljs-keyword">if</span> (!role) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Role not found'</span>);

    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.permissions.has(permissionName)) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Permission not found'</span>);
    }

    role.permissions.add(permissionName);
  }
}
</code></pre>
<p>The code above lets you specify fine-grained permissions like <code>documents.read.own</code> and organizes them into roles such as <code>employee</code> or <code>manager</code> that you can independently reuse. You can define roles to inherit from other roles, which avoids redundancy and promotes a consistent, scalable access control logic.</p>
<p>As a general rule to avoid privilege creep, permissions should always be as fine-grained as possible. This lets the application refine access decisions to specific actions or scopes: for example, allowing users to read only their documents versus reading all documents for their team.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Fine-grained permission definitions</span>
<span class="hljs-keyword">const</span> permissions = {
  <span class="hljs-comment">// User management</span>
  <span class="hljs-string">'users.read'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span> },
  <span class="hljs-string">'users.create'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'create'</span> },
  <span class="hljs-string">'users.update'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span> },
  <span class="hljs-string">'users.delete'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'users'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'delete'</span> },

  <span class="hljs-comment">// Document management</span>
  <span class="hljs-string">'documents.read.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },
  <span class="hljs-string">'documents.read.team'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'team'</span> },
  <span class="hljs-string">'documents.read.all'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'all'</span> },
  <span class="hljs-string">'documents.create'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'create'</span> },
  <span class="hljs-string">'documents.update.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },
  <span class="hljs-string">'documents.delete.own'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'documents'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'delete'</span>, <span class="hljs-attr">scope</span>: <span class="hljs-string">'own'</span> },

  <span class="hljs-comment">// System administration</span>
  <span class="hljs-string">'system.logs.read'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'system'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'read'</span>, <span class="hljs-attr">subresource</span>: <span class="hljs-string">'logs'</span> },
  <span class="hljs-string">'system.config.update'</span>: { <span class="hljs-attr">resource</span>: <span class="hljs-string">'system'</span>, <span class="hljs-attr">action</span>: <span class="hljs-string">'update'</span>, <span class="hljs-attr">subresource</span>: <span class="hljs-string">'config'</span> }
};
</code></pre>
<p>With an array of permissions at its disposal, the app can undertake very precise access control decisions. Instead of merely addressing the binary "is admin" question, this capability enables the system to answer questions such as "can this user delete their own document but not others?"</p>
<p>Static roles are often insufficient. You may want to give people temporary or conditional access, for example, when the team lead takes over for a manager or when a user approves a higher access level for the sake of incident response.</p>
<p>To support these cases, the RBAC system must allow dynamic role assignment – that is, the ability to assign roles on the basis of time, context, or an external trigger such as a security workflow.</p>
<p>The code below assigns a temporary role to a user, notes the exact time at which the role was assigned to the user, and periodically revokes the right after some fixed amount of time. Also, it has a method to calculate a user's complete set of active rights, depending on their permanent rights, temporary rights, and role-based contextual rights.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Dynamic role assignment system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DynamicRoleAssignment</span> </span>{
  <span class="hljs-keyword">async</span> assignTemporaryRole(userId, roleName, duration, reason) {
    <span class="hljs-keyword">const</span> assignment = {
      userId,
      roleName,
      <span class="hljs-attr">assignedAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
      <span class="hljs-attr">expiresAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(<span class="hljs-built_in">Date</span>.now() + duration * <span class="hljs-number">1000</span>),
      reason,
      <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>
    };

    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.storeRoleAssignment(assignment);
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logRoleAssignment(assignment);

    <span class="hljs-comment">// Schedule automatic revocation</span>
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">this</span>.revokeExpiredAssignment(assignment.id);
    }, duration * <span class="hljs-number">1000</span>);

    <span class="hljs-keyword">return</span> assignment;
  }

  <span class="hljs-keyword">async</span> getUserEffectivePermissions(userId, context = {}) {
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(userId);
    <span class="hljs-keyword">const</span> permanentRoles = user.roles || [];
    <span class="hljs-keyword">const</span> temporaryRoles = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getActiveTemporaryRoles(userId);
    <span class="hljs-keyword">const</span> contextualRoles = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getContextualRoles(userId, context);

    <span class="hljs-keyword">const</span> allRoles = [...permanentRoles, ...temporaryRoles, ...contextualRoles];
    <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>();

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> roleName <span class="hljs-keyword">of</span> allRoles) {
      <span class="hljs-keyword">const</span> rolePermissions = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getRolePermissions(roleName);
      rolePermissions.forEach(<span class="hljs-function"><span class="hljs-params">permission</span> =&gt;</span> permissions.add(permission));
    }

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Array</span>.from(permissions);
  }
}
</code></pre>
<p>This allows for more flexible security configurations. Temporary roles that are granted have an automatic expiration. The context roles may be added dynamically depending on contextual factors such as location or type of device. Permanent roles are combined with temporary and context roles to compute the aggregate permission set for the user on a per-request basis, which maintains flexibility without compromising control.</p>
<h3 id="heading-using-middleware-to-enforce-rbac">Using Middleware to Enforce RBAC</h3>
<p>The RBAC policies have to be enforced before any request reaches a protected route or protected data. <a target="_blank" href="https://aws.amazon.com/what-is/middleware/">Middleware</a> is a good place to run such checks in the scope of a web application. We’ll now look into how the reusable middleware function for authorization works.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Authorization middleware</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createAuthorizationMiddleware</span>(<span class="hljs-params">requiredPermission</span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">async</span> (req, res, next) =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Extract user from validated JWT</span>
      <span class="hljs-keyword">const</span> user = req.user;
      <span class="hljs-keyword">if</span> (!user) {
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">401</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Authentication required'</span> });
      }

      <span class="hljs-comment">// Get user's effective permissions</span>
      <span class="hljs-keyword">const</span> context = {
        <span class="hljs-attr">ipAddress</span>: req.ip,
        <span class="hljs-attr">userAgent</span>: req.get(<span class="hljs-string">'User-Agent'</span>),
        <span class="hljs-attr">resourceId</span>: req.params.id,
        <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
      };

      <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">await</span> roleSystem.getUserEffectivePermissions(
        user.id,
        context
      );

      <span class="hljs-comment">// Check if user has required permission</span>
      <span class="hljs-keyword">if</span> (!permissions.includes(requiredPermission)) {
        <span class="hljs-keyword">await</span> logUnauthorizedAccess(user.id, requiredPermission, context);
        <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">403</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Insufficient permissions'</span> });
      }

      <span class="hljs-comment">// Add permissions to request for downstream use</span>
      req.userPermissions = permissions;
      next();
    } <span class="hljs-keyword">catch</span> (error) {
      res.status(<span class="hljs-number">500</span>).json({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Authorization check failed'</span> });
    }
  };
}

<span class="hljs-comment">// Usage in routes</span>
app.get(<span class="hljs-string">'/api/users'</span>, 
  authenticateToken,
  createAuthorizationMiddleware(<span class="hljs-string">'users.read'</span>),
  getUsersController
);
</code></pre>
<p>In the code above, the middleware will validate user identities in real-time, check if adequate permissions are granted, and allow or deny access accordingly. It’s a central mechanism for enforcing access rules in a uniform way across your routes, and it even records unauthorized attempts for auditing.</p>
<h3 id="heading-testing-access-control-logic">Testing Access Control Logic</h3>
<p>Once you’ve implemented the RBAC system, testing becomes a must. You want to guarantee that permissions are inherited properly, that access is actually denied when a user isn’t authorized, and that your roles behave as designed in the real world as well as in edge-case scenarios.</p>
<p>The following example uses a testing framework to demonstrate the verification of two fundamental behaviors: inheritance of permissions from parent roles and rejection of unauthorized access.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// RBAC testing suite</span>
describe(<span class="hljs-string">'RBAC System'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should inherit permissions from parent roles'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> manager = <span class="hljs-keyword">await</span> roleSystem.createRole(<span class="hljs-string">'manager'</span>, <span class="hljs-string">'Team Manager'</span>, <span class="hljs-string">'employee'</span>);
    <span class="hljs-keyword">await</span> roleSystem.addPermissionToRole(<span class="hljs-string">'manager'</span>, <span class="hljs-string">'team.manage'</span>);

    <span class="hljs-keyword">const</span> permissions = <span class="hljs-keyword">await</span> roleSystem.getRolePermissions(<span class="hljs-string">'manager'</span>);
    expect(permissions).toContain(<span class="hljs-string">'documents.read.own'</span>); <span class="hljs-comment">// From employee</span>
    expect(permissions).toContain(<span class="hljs-string">'team.manage'</span>); <span class="hljs-comment">// Manager-specific</span>
  });

  test(<span class="hljs-string">'should deny access without proper permissions'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = { <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">roles</span>: [<span class="hljs-string">'employee'</span>] };
    <span class="hljs-keyword">const</span> req = { user, <span class="hljs-attr">params</span>: { <span class="hljs-attr">id</span>: <span class="hljs-string">'doc123'</span> } };
    <span class="hljs-keyword">const</span> res = { <span class="hljs-attr">status</span>: jest.fn().mockReturnThis(), <span class="hljs-attr">json</span>: jest.fn() };

    <span class="hljs-keyword">const</span> middleware = createAuthorizationMiddleware(<span class="hljs-string">'documents.delete.all'</span>);
    <span class="hljs-keyword">await</span> middleware(req, res, <span class="hljs-function">() =&gt;</span> {}); <span class="hljs-comment">// Middleware call simulating request</span>

    expect(res.status).toHaveBeenCalledWith(<span class="hljs-number">403</span>);
  });
});
</code></pre>
<p>The tests represent the positive and negative validations of the access rules. The first test determines whether inherited permissions flow freely from the parent to child roles. The second test blocks any user without the required permission, returning a status code appropriately.</p>
<p>Over time, you can enrich test coverage to include temporary role assignments, contextual conditions, and session-aware behavior to alert you to any regressions before they start affecting production access.</p>
<h2 id="heading-continuous-verification">Continuous Verification</h2>
<p>Modern access security is not a one-shot check but an ongoing process. A strong system must continuously verify user identity and context throughout the ongoing session while adapting to newly emerging risk signals.</p>
<p>In <a target="_blank" href="https://spot.io/resources/gitops/continuous-verification/">continuous verification</a>, it’s an assurance that access stays appropriate while the user behavior, device posture, or environment changes mid-session.</p>
<p>To uniquely identify a device, you can combine subtle traits like browser settings, hardware specs, and plugin data. This forms a device “fingerprint,” which helps flag new or suspicious devices attempting access.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Advanced device fingerprinting</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DeviceFingerprintService</span> </span>{
  generateFingerprint(deviceInfo) {
    <span class="hljs-keyword">const</span> components = [
      deviceInfo.userAgent,
      deviceInfo.screenResolution,
      deviceInfo.timezone,
      deviceInfo.language,
      deviceInfo.platform,
      deviceInfo.hardwareConcurrency,
      deviceInfo.memorySize,
      deviceInfo.availableFonts?.join(<span class="hljs-string">','</span>),
      deviceInfo.plugins?.map(<span class="hljs-function"><span class="hljs-params">p</span> =&gt;</span> p.name).join(<span class="hljs-string">','</span>),
      deviceInfo.webglRenderer,
      deviceInfo.audioContext
    ];

    <span class="hljs-keyword">return</span> <span class="hljs-built_in">this</span>.hashComponents(components);
  }

  calculateTrustScore(currentFingerprint, knownFingerprints) {
    <span class="hljs-keyword">if</span> (knownFingerprints.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">50</span>; <span class="hljs-comment">// Neutral for new device</span>
    <span class="hljs-keyword">const</span> similarities = knownFingerprints.map(<span class="hljs-function"><span class="hljs-params">known</span> =&gt;</span>
      <span class="hljs-built_in">this</span>.calculateSimilarity(currentFingerprint, known)
    );
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.min(<span class="hljs-number">100</span>, <span class="hljs-built_in">Math</span>.max(...similarities) * <span class="hljs-number">100</span>);
  }

  <span class="hljs-keyword">async</span> updateDeviceTrust(userId, deviceFingerprint, securityEvents) {
    <span class="hljs-keyword">const</span> device = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getOrCreateDevice(userId, deviceFingerprint);
    <span class="hljs-keyword">let</span> trustAdjustment = <span class="hljs-number">0</span>;

    securityEvents.forEach(<span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
      <span class="hljs-keyword">switch</span> (event.type) {
        <span class="hljs-keyword">case</span> <span class="hljs-string">'successful_login'</span>: trustAdjustment += <span class="hljs-number">5</span>; <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">'failed_login'</span>: trustAdjustment -= <span class="hljs-number">10</span>; <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">'suspicious_activity'</span>: trustAdjustment -= <span class="hljs-number">25</span>; <span class="hljs-keyword">break</span>;
      }
    });

    device.trustScore = <span class="hljs-built_in">Math</span>.max(<span class="hljs-number">0</span>, <span class="hljs-built_in">Math</span>.min(<span class="hljs-number">100</span>, device.trustScore + trustAdjustment));
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.updateDevice(device);
    <span class="hljs-keyword">return</span> device.trustScore;
  }
}
</code></pre>
<p>Generating a fingerprint hash from device traits, this service uses historical events to dynamically adjust the device's trust score. Step-up authentication may be prompted by low scores, or access may be denied altogether.</p>
<h3 id="heading-behavioral-analysis">Behavioral Analysis</h3>
<p>People tend to use apps rather consistently – they type a certain way, move the mouse in a particular manner, or browse varied content. <a target="_blank" href="https://zimperium.com/glossary/behavioral-analysis">Behavioral analysis</a> tries to detect that anomaly by comparing ongoing activities to known ones.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Behavioral analysis system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BehaviorAnalysisService</span> </span>{
  <span class="hljs-keyword">async</span> analyzeUserBehavior(userId, currentSession) {
    <span class="hljs-keyword">const</span> historicalBehavior = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getUserBehaviorProfile(userId);
    <span class="hljs-keyword">const</span> anomalies = [];

    <span class="hljs-keyword">const</span> typingAnomaly = <span class="hljs-built_in">this</span>.analyzeTypingPatterns(
      currentSession.typingData,
      historicalBehavior.typingProfile
    );
    <span class="hljs-keyword">if</span> (typingAnomaly.score &gt; <span class="hljs-number">0.7</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'typing_pattern'</span>, <span class="hljs-attr">score</span>: typingAnomaly.score, <span class="hljs-attr">details</span>: typingAnomaly.details });
    }

    <span class="hljs-keyword">const</span> navigationAnomaly = <span class="hljs-built_in">this</span>.analyzeNavigationPatterns(
      currentSession.navigationData,
      historicalBehavior.navigationProfile
    );
    <span class="hljs-keyword">if</span> (navigationAnomaly.score &gt; <span class="hljs-number">0.6</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'navigation_pattern'</span>, <span class="hljs-attr">score</span>: navigationAnomaly.score, <span class="hljs-attr">details</span>: navigationAnomaly.details });
    }

    <span class="hljs-keyword">const</span> timeAnomaly = <span class="hljs-built_in">this</span>.analyzeTimePatterns(
      currentSession.timestamp,
      historicalBehavior.timeProfile
    );
    <span class="hljs-keyword">if</span> (timeAnomaly.score &gt; <span class="hljs-number">0.5</span>) {
      anomalies.push({ <span class="hljs-attr">type</span>: <span class="hljs-string">'time_pattern'</span>, <span class="hljs-attr">score</span>: timeAnomaly.score, <span class="hljs-attr">details</span>: timeAnomaly.details });
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">overallRiskScore</span>: <span class="hljs-built_in">this</span>.calculateOverallRisk(anomalies),
      anomalies,
      <span class="hljs-attr">recommendations</span>: <span class="hljs-built_in">this</span>.generateRecommendations(anomalies)
    };
  }

  analyzeTypingPatterns(currentData, historicalProfile) {
    <span class="hljs-keyword">if</span> (!currentData || !historicalProfile) <span class="hljs-keyword">return</span> { <span class="hljs-attr">score</span>: <span class="hljs-number">0</span> };
    <span class="hljs-keyword">const</span> dwellTimeVariance = <span class="hljs-built_in">this</span>.calculateVariance(currentData.dwellTimes, historicalProfile.averageDwellTime);
    <span class="hljs-keyword">const</span> flightTimeVariance = <span class="hljs-built_in">this</span>.calculateVariance(currentData.flightTimes, historicalProfile.averageFlightTime);
    <span class="hljs-keyword">const</span> score = <span class="hljs-built_in">Math</span>.max(dwellTimeVariance, flightTimeVariance);
    <span class="hljs-keyword">return</span> { score, <span class="hljs-attr">details</span>: { dwellTimeVariance, flightTimeVariance, <span class="hljs-attr">sampleSize</span>: currentData.keystrokes.length } };
  }
}
</code></pre>
<p>This will detect suspicious changes in user behavior and typing characteristics as early warning indicators of session hijacking or insider threat.</p>
<p>Access from a new country or city can either be harmless or highly suspicious. Comparing login geography against historical patterns helps flag impossible travel or access from banned regions.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Location-based access control</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LocationAccessControl</span> </span>{
  <span class="hljs-keyword">async</span> validateLocationAccess(userId, ipAddress, session) {
    <span class="hljs-keyword">const</span> location = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.resolveLocation(ipAddress);
    <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> getUserById(userId);
    <span class="hljs-keyword">const</span> historicalLocations = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getUserLocations(userId);
    <span class="hljs-keyword">const</span> locationRisk = <span class="hljs-built_in">this</span>.assessLocationRisk(location, historicalLocations);

    <span class="hljs-keyword">const</span> lastLocation = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getLastKnownLocation(userId);
    <span class="hljs-keyword">if</span> (lastLocation) {
      <span class="hljs-keyword">const</span> impossibleTravel = <span class="hljs-built_in">this</span>.checkImpossibleTravel(lastLocation, location, session.lastActivity);
      <span class="hljs-keyword">if</span> (impossibleTravel.detected) {
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.logSecurityEvent(<span class="hljs-string">'impossible_travel'</span>, {
          userId, <span class="hljs-attr">fromLocation</span>: lastLocation, <span class="hljs-attr">toLocation</span>: location,
          <span class="hljs-attr">timeWindow</span>: impossibleTravel.timeWindow,
          <span class="hljs-attr">minimumTravelTime</span>: impossibleTravel.minimumTravelTime
        });
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'impossible_travel'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span> };
      }
    }

    <span class="hljs-keyword">if</span> (user.allowedCountries &amp;&amp; !user.allowedCountries.includes(location.country)) {
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'country_restriction'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span> };
    }

    <span class="hljs-keyword">const</span> highRiskCountries = [<span class="hljs-string">'XX'</span>, <span class="hljs-string">'YY'</span>, <span class="hljs-string">'ZZ'</span>];
    <span class="hljs-keyword">if</span> (highRiskCountries.includes(location.country)) {
      <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'high_risk_location'</span>, <span class="hljs-attr">requiresStepUp</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">additionalVerification</span>: [<span class="hljs-string">'sms'</span>, <span class="hljs-string">'email'</span>] };
    }

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">allowed</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">riskScore</span>: locationRisk, location };
  }

  checkImpossibleTravel(fromLocation, toLocation, lastActivity) {
    <span class="hljs-keyword">const</span> distance = <span class="hljs-built_in">this</span>.calculateDistance(fromLocation, toLocation);
    <span class="hljs-keyword">const</span> timeElapsed = <span class="hljs-built_in">Date</span>.now() - lastActivity;
    <span class="hljs-keyword">const</span> maximumSpeed = <span class="hljs-number">900</span>; <span class="hljs-comment">// km/h</span>
    <span class="hljs-keyword">const</span> minimumTravelTime = (distance / maximumSpeed) * <span class="hljs-number">3600000</span>;
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">detected</span>: timeElapsed &lt; minimumTravelTime, <span class="hljs-attr">timeWindow</span>: timeElapsed, minimumTravelTime, distance };
  }
}
</code></pre>
<p>This logic prevents abuse via VPNs or stolen credentials by requiring step-up verification when impossible travel or unusual locations are detected.</p>
<h3 id="heading-step-up-authentication">Step-Up Authentication</h3>
<p><a target="_blank" href="https://doubleoctopus.com/security-wiki/authentication/step-up-authentication/">Step-up security</a> introduces friction only when truly needed. With lower risk considered, users move freely. When risk levels rises, they're asked for stronger proofs, such as biometrics or hardware tokens.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Step-up authentication system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StepUpAuthenticationService</span> </span>{
  <span class="hljs-keyword">async</span> evaluateStepUpRequirement(userId, requestContext, resourceSensitivity) {
    <span class="hljs-keyword">const</span> riskFactors = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.calculateRiskFactors(userId, requestContext);
    <span class="hljs-keyword">const</span> stepUpRequired = <span class="hljs-built_in">this</span>.shouldRequireStepUp(riskFactors, resourceSensitivity);

    <span class="hljs-keyword">if</span> (stepUpRequired.required) {
      <span class="hljs-keyword">return</span> {
        <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">methods</span>: <span class="hljs-built_in">this</span>.selectAuthenticationMethods(riskFactors, stepUpRequired.level),
        <span class="hljs-attr">expiresIn</span>: <span class="hljs-built_in">this</span>.calculateStepUpDuration(stepUpRequired.level),
        <span class="hljs-attr">reason</span>: stepUpRequired.reason
      };
    }

    <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span> };
  }

  <span class="hljs-keyword">async</span> calculateRiskFactors(userId, context) {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">deviceTrust</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getDeviceTrustScore(userId, context.deviceFingerprint),
      <span class="hljs-attr">locationRisk</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getLocationRiskScore(userId, context.ipAddress),
      <span class="hljs-attr">behaviorAnomaly</span>: <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getBehaviorAnomalyScore(userId, context.sessionData),
      <span class="hljs-attr">timeSinceLastAuth</span>: <span class="hljs-built_in">Date</span>.now() - context.lastAuthTime,
      <span class="hljs-attr">resourceSensitivity</span>: context.resourceSensitivity || <span class="hljs-string">'medium'</span>
    };
  }

  shouldRequireStepUp(riskFactors, sensitivity) {
    <span class="hljs-keyword">let</span> score = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">70</span>) score += <span class="hljs-number">30</span>;
    <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">40</span>) score += <span class="hljs-number">20</span>;
    <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.6</span>) score += <span class="hljs-number">25</span>;
    <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.8</span>) score += <span class="hljs-number">15</span>;
    <span class="hljs-keyword">if</span> (riskFactors.behaviorAnomaly &gt; <span class="hljs-number">0.5</span>) score += <span class="hljs-number">20</span>;
    <span class="hljs-keyword">if</span> (riskFactors.behaviorAnomaly &gt; <span class="hljs-number">0.7</span>) score += <span class="hljs-number">10</span>;
    <span class="hljs-keyword">const</span> hours = riskFactors.timeSinceLastAuth / (<span class="hljs-number">1000</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span>);
    <span class="hljs-keyword">if</span> (hours &gt; <span class="hljs-number">8</span>) score += <span class="hljs-number">10</span>;
    <span class="hljs-keyword">if</span> (hours &gt; <span class="hljs-number">24</span>) score += <span class="hljs-number">15</span>;

    score *= { <span class="hljs-attr">low</span>: <span class="hljs-number">0.7</span>, <span class="hljs-attr">medium</span>: <span class="hljs-number">1.0</span>, <span class="hljs-attr">high</span>: <span class="hljs-number">1.3</span>, <span class="hljs-attr">critical</span>: <span class="hljs-number">1.6</span> }[sensitivity] || <span class="hljs-number">1.0</span>;

    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">80</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'high'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'high_risk_detected'</span> };
    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">50</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'medium'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'moderate_risk_detected'</span> };
    <span class="hljs-keyword">if</span> (score &gt;= <span class="hljs-number">25</span>) <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'low'</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'low_risk_detected'</span> };
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">required</span>: <span class="hljs-literal">false</span> };
  }

  selectAuthenticationMethods(riskFactors, level) {
    <span class="hljs-keyword">const</span> methods = [];
    <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'high'</span>) {
      methods.push(<span class="hljs-string">'hardware_token'</span>, <span class="hljs-string">'biometric'</span>);
      <span class="hljs-keyword">if</span> (riskFactors.deviceTrust &lt; <span class="hljs-number">30</span>) methods.push(<span class="hljs-string">'admin_approval'</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'medium'</span>) {
      methods.push(<span class="hljs-string">'totp'</span>, <span class="hljs-string">'sms'</span>);
      <span class="hljs-keyword">if</span> (riskFactors.locationRisk &gt; <span class="hljs-number">0.7</span>) methods.push(<span class="hljs-string">'email_verification'</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (level === <span class="hljs-string">'low'</span>) {
      methods.push(<span class="hljs-string">'totp'</span>);
    }
    <span class="hljs-keyword">return</span> methods;
  }
}
</code></pre>
<p>The service uses this balancing technique between critical resources and risks while keeping normal workflows intact when things look safe.</p>
<h2 id="heading-security-monitoring">Security Monitoring</h2>
<p>Security monitoring provides the observability layer that’s essential for detecting, analyzing, and responding to threats in real time. A strong system must log every authentication event, highlight anomalies, and allow for rapid and automated response to threats. This phase further builds trust by constantly evaluating access patterns and acting on them when signals of risk emerge.</p>
<p>Logging is visibility at its base. These days, every authentication attempt, be it successful, failed, or suspicious, needs to be logged with exhaustive context. This very information helps forensic analysis, alerting, and compliance reporting.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Comprehensive authentication event logging</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthenticationLogger</span> </span>{
  <span class="hljs-keyword">async</span> logAuthenticationEvent(eventType, userId, context, result) {
    <span class="hljs-keyword">const</span> logEntry = {
      <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(),
      eventType,
      userId,
      <span class="hljs-attr">sessionId</span>: context.sessionId,
      <span class="hljs-attr">ipAddress</span>: context.ipAddress,
      <span class="hljs-attr">userAgent</span>: context.userAgent,
      <span class="hljs-attr">deviceFingerprint</span>: context.deviceFingerprint,
      <span class="hljs-attr">location</span>: context.location,
      <span class="hljs-attr">authenticationMethod</span>: context.authMethod,
      <span class="hljs-attr">result</span>: result.success ? <span class="hljs-string">'success'</span> : <span class="hljs-string">'failure'</span>,
      <span class="hljs-attr">failureReason</span>: result.failureReason,
      <span class="hljs-attr">riskScore</span>: result.riskScore,
      <span class="hljs-attr">additionalFactorsRequired</span>: result.stepUpRequired,
      <span class="hljs-attr">processingTime</span>: result.processingTime,
      <span class="hljs-attr">correlationId</span>: context.correlationId
    };

    <span class="hljs-comment">// Store in multiple destinations for redundancy</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
      <span class="hljs-built_in">this</span>.writeToDatabase(logEntry),
      <span class="hljs-built_in">this</span>.sendToLogAggregator(logEntry),
      <span class="hljs-built_in">this</span>.updateRealTimeMetrics(logEntry)
    ]);

    <span class="hljs-comment">// Trigger real-time alerts for critical events</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.isCriticalEvent(logEntry)) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.triggerSecurityAlert(logEntry);
    }
  }

  isCriticalEvent(logEntry) {
    <span class="hljs-keyword">const</span> criticalConditions = [
      logEntry.result === <span class="hljs-string">'failure'</span> &amp;&amp; logEntry.failureReason === <span class="hljs-string">'brute_force_detected'</span>,
      logEntry.riskScore &gt; <span class="hljs-number">80</span>,
      logEntry.eventType === <span class="hljs-string">'impossible_travel_detected'</span>,
      logEntry.eventType === <span class="hljs-string">'account_takeover_suspected'</span>
    ];

    <span class="hljs-keyword">return</span> criticalConditions.some(<span class="hljs-function"><span class="hljs-params">condition</span> =&gt;</span> condition);
  }

  <span class="hljs-keyword">async</span> generateSecurityReport(userId, timeRange) {
    <span class="hljs-keyword">const</span> events = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.getAuthenticationEvents(userId, timeRange);

    <span class="hljs-keyword">const</span> analysis = {
      <span class="hljs-attr">totalEvents</span>: events.length,
      <span class="hljs-attr">successfulLogins</span>: events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.result === <span class="hljs-string">'success'</span>).length,
      <span class="hljs-attr">failedAttempts</span>: events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.result === <span class="hljs-string">'failure'</span>).length,
      <span class="hljs-attr">uniqueDevices</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.deviceFingerprint)).size,
      <span class="hljs-attr">uniqueLocations</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.location?.country)).size,
      <span class="hljs-attr">averageRiskScore</span>: events.reduce(<span class="hljs-function">(<span class="hljs-params">sum, e</span>) =&gt;</span> sum + e.riskScore, <span class="hljs-number">0</span>) / events.length,
      <span class="hljs-attr">timePatterns</span>: <span class="hljs-built_in">this</span>.analyzeTimePatterns(events),
      <span class="hljs-attr">locationPatterns</span>: <span class="hljs-built_in">this</span>.analyzeLocationPatterns(events),
      <span class="hljs-attr">devicePatterns</span>: <span class="hljs-built_in">this</span>.analyzeDevicePatterns(events)
    };

    <span class="hljs-keyword">return</span> analysis;
  }
}
</code></pre>
<p>In the above code, the class logs detailed authentication events such as the approximate device and location from which it was initiated, the authentication methods used, and the risk score.</p>
<p>From a security perspective, it’s envisaged to generate security reports with the advantage of flagging critical events such as brute-force attempts or logins from suspicious geographies that can send real-time alerts.</p>
<p>Monitoring authentication events isn’t enough – the system must be able to interpret patterns and flag suspicious behavior. This detection system combines static rule-based checks with dynamic anomaly detection powered by machine learning. It identifies threats like brute-force attacks, credential stuffing, and unusual geographic access, then escalates them automatically for further action.</p>
<p>The following code performs real-time threat detection by analyzing recent authentication events and contextual data. Here's what it does, broken down clearly:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Suspicious activity detection system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SuspiciousActivityDetector</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.detectionRules = <span class="hljs-built_in">this</span>.initializeDetectionRules();
    <span class="hljs-built_in">this</span>.mlModel = <span class="hljs-built_in">this</span>.loadAnomalyDetectionModel();
  }

  <span class="hljs-keyword">async</span> analyzeActivity(userId, recentEvents, context) {
    <span class="hljs-keyword">const</span> suspiciousPatterns = [];

    <span class="hljs-comment">// Rule-based detection</span>
    <span class="hljs-keyword">const</span> ruleViolations = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.checkDetectionRules(userId, recentEvents);
    suspiciousPatterns.push(...ruleViolations);

    <span class="hljs-comment">// ML-based anomaly detection</span>
    <span class="hljs-keyword">const</span> anomalies = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.detectAnomalies(userId, recentEvents, context);
    suspiciousPatterns.push(...anomalies);

    <span class="hljs-comment">// Threat intelligence correlation</span>
    <span class="hljs-keyword">const</span> threatMatches = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.correlateThreatIntelligence(context);
    suspiciousPatterns.push(...threatMatches);

    <span class="hljs-keyword">if</span> (suspiciousPatterns.length &gt; <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.escalateSuspiciousActivity(userId, suspiciousPatterns);
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">suspicious</span>: suspiciousPatterns.length &gt; <span class="hljs-number">0</span>,
      <span class="hljs-attr">patterns</span>: suspiciousPatterns,
      <span class="hljs-attr">riskScore</span>: <span class="hljs-built_in">this</span>.calculateSuspiciousActivityRisk(suspiciousPatterns)
    };
  }

  initializeDetectionRules() {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'brute_force_detection'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> failedAttempts = events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span>
            e.result === <span class="hljs-string">'failure'</span> &amp;&amp;
            <span class="hljs-built_in">Date</span>.now() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.timestamp).getTime() &lt; <span class="hljs-number">300000</span> <span class="hljs-comment">// 5 minutes</span>
          );
          <span class="hljs-keyword">return</span> failedAttempts.length &gt;= <span class="hljs-number">5</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'high'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'temporary_lockout'</span>
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'credential_stuffing'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> recentFailures = events.filter(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span>
            e.result === <span class="hljs-string">'failure'</span> &amp;&amp;
            <span class="hljs-built_in">Date</span>.now() - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(e.timestamp).getTime() &lt; <span class="hljs-number">3600000</span> <span class="hljs-comment">// 1 hour</span>
          );
          <span class="hljs-keyword">const</span> uniqueUsernames = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(recentFailures.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.username));
          <span class="hljs-keyword">return</span> uniqueUsernames.size &gt;= <span class="hljs-number">10</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'medium'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'rate_limiting'</span>
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'suspicious_location_pattern'</span>,
        <span class="hljs-attr">condition</span>: <span class="hljs-function">(<span class="hljs-params">events</span>) =&gt;</span> {
          <span class="hljs-keyword">const</span> locations = events.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.location?.country).filter(<span class="hljs-built_in">Boolean</span>);
          <span class="hljs-keyword">const</span> uniqueCountries = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Set</span>(locations);
          <span class="hljs-keyword">return</span> uniqueCountries.size &gt;= <span class="hljs-number">3</span> &amp;&amp; events.length &gt;= <span class="hljs-number">5</span>;
        },
        <span class="hljs-attr">severity</span>: <span class="hljs-string">'medium'</span>,
        <span class="hljs-attr">action</span>: <span class="hljs-string">'enhanced_verification'</span>
      }
    ];
  }

  <span class="hljs-keyword">async</span> detectAnomalies(userId, events, context) {
    <span class="hljs-keyword">const</span> features = <span class="hljs-built_in">this</span>.extractFeatures(events, context);
    <span class="hljs-keyword">const</span> anomalyScore = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.mlModel.predict(features);

    <span class="hljs-keyword">if</span> (anomalyScore &gt; <span class="hljs-number">0.7</span>) {
      <span class="hljs-keyword">return</span> [{
        <span class="hljs-attr">type</span>: <span class="hljs-string">'ml_anomaly'</span>,
        <span class="hljs-attr">score</span>: anomalyScore,
        <span class="hljs-attr">features</span>: features,
        <span class="hljs-attr">description</span>: <span class="hljs-string">'Machine learning model detected anomalous behavior pattern'</span>
      }];
    }

    <span class="hljs-keyword">return</span> [];
  }
}
</code></pre>
<p>This class applies multiple techniques to detect threats. It first evaluates authentication history using static rules for brute-force attempts, large-scale credential reuse, or location anomalies. It then passes <a target="_blank" href="https://www.fullstory.com/blog/behavioral-data/">behavioral data</a> through a trained ML model to spot subtle patterns missed by rules. If any suspicious pattern is detected, it returns a structured risk report and initiates escalation.</p>
<h3 id="heading-automating-threat-response">Automating Threat Response</h3>
<p>Most times, systems respond in real-time. Automated threat response follows predefined actions and includes locking an account, alerting users, or blocking an IP, among others, when a high-risk event occurs.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Automated threat response system</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AutomatedThreatResponse</span> </span>{
  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">this</span>.responsePlaybooks = <span class="hljs-built_in">this</span>.initializeResponsePlaybooks();
    <span class="hljs-built_in">this</span>.escalationPolicies = <span class="hljs-built_in">this</span>.loadEscalationPolicies();
  }

  <span class="hljs-keyword">async</span> processSecurityEvent(event) {
    <span class="hljs-keyword">const</span> threatLevel = <span class="hljs-built_in">this</span>.assessThreatLevel(event);
    <span class="hljs-keyword">const</span> applicablePlaybooks = <span class="hljs-built_in">this</span>.selectPlaybooks(event, threatLevel);

    <span class="hljs-keyword">const</span> responses = [];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> playbook <span class="hljs-keyword">of</span> applicablePlaybooks) {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.executePlaybook(playbook, event);
      responses.push(response);
    }

    <span class="hljs-keyword">if</span> (threatLevel === <span class="hljs-string">'critical'</span> || responses.some(<span class="hljs-function"><span class="hljs-params">r</span> =&gt;</span> !r.success)) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.escalateToHuman(event, responses);
    }

    <span class="hljs-keyword">return</span> {
      event,
      threatLevel,
      responses,
      <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>()
    };
  }

  initializeResponsePlaybooks() {
    <span class="hljs-keyword">return</span> [
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'brute_force_response'</span>,
        <span class="hljs-attr">triggers</span>: [<span class="hljs-string">'brute_force_detected'</span>],
        <span class="hljs-attr">actions</span>: [
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'temporary_lockout'</span>, <span class="hljs-attr">duration</span>: <span class="hljs-number">900</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'rate_limiting'</span>, <span class="hljs-attr">factor</span>: <span class="hljs-number">10</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'notify_user'</span>, <span class="hljs-attr">method</span>: <span class="hljs-string">'email'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'log_security_event'</span>, <span class="hljs-attr">level</span>: <span class="hljs-string">'high'</span> }
        ]
      },
      {
        <span class="hljs-attr">name</span>: <span class="hljs-string">'account_takeover_response'</span>,
        <span class="hljs-attr">triggers</span>: [<span class="hljs-string">'impossible_travel'</span>, <span class="hljs-string">'behavior_anomaly_high'</span>],
        <span class="hljs-attr">actions</span>: [
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'terminate_all_sessions'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'require_password_reset'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'notify_user'</span>, <span class="hljs-attr">method</span>: <span class="hljs-string">'multiple'</span> },
          { <span class="hljs-attr">type</span>: <span class="hljs-string">'freeze_account'</span>, <span class="hljs-attr">duration</span>: <span class="hljs-number">7200</span> }
        ]
      }
    ];
  }

  <span class="hljs-keyword">async</span> executePlaybook(playbook, event) {
    <span class="hljs-keyword">const</span> execution = {
      <span class="hljs-attr">playbookName</span>: playbook.name,
      <span class="hljs-attr">eventId</span>: event.id,
      <span class="hljs-attr">actions</span>: [],
      <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>
    };

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> action <span class="hljs-keyword">of</span> playbook.actions) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.executeAction(action, event);
        execution.actions.push(result);
        <span class="hljs-keyword">if</span> (!result.success) {
          execution.success = <span class="hljs-literal">false</span>;
          <span class="hljs-keyword">break</span>;
        }
      } <span class="hljs-keyword">catch</span> (err) {
        execution.success = <span class="hljs-literal">false</span>;
        execution.error = err.message;
      }
    }

    <span class="hljs-keyword">return</span> execution;
  }

  <span class="hljs-keyword">async</span> executeAction(action, event) {
    <span class="hljs-keyword">switch</span> (action.type) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">'temporary_lockout'</span>:
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.lockoutUser(event.userId, action.duration);
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">type</span>: action.type };
      <span class="hljs-keyword">case</span> <span class="hljs-string">'notify_user'</span>:
        <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.notifyUser(event.userId, action.method, event);
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">type</span>: action.type };
      <span class="hljs-keyword">default</span>:
        <span class="hljs-keyword">return</span> { <span class="hljs-attr">success</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">type</span>: action.type, <span class="hljs-attr">error</span>: <span class="hljs-string">'Unknown action'</span> };
    }
  }
}
</code></pre>
<p>Here, the system uses playbooks – predefined actions to be taken in response to threats. For example, locks user from further brute-force attempts for some time and sends them an email notification. Freezing the account and ending all sessions are some reactive measures you can take if suspicious behavior indicates a takeover. These measures ensure fast and consistent action to mitigate damage even before humans can get involved.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Zero-trust authentication creates a strong line of distinction going against classic perimeter-based security. It must be painstakingly planned, implemented in layers, and constantly improved. This article offers a structured path, from basic MFA to intelligent behavioral monitoring and automated threat response.</p>
<p>Complementing the improvement of security, zero-trust promises better user experience, compliance readiness, and decreased incident risk. When organizations maintain a perpetual position of zero trust, we can see an actual positive impact on their ability to detect, prevent, and respond to threats in real time.</p>
<p>To have long-term success with this approach, you’ll need to continuously monitor your setup, perform periodic assessments, and be responsive to evolving attack patterns. Feedback loops and performance data are essential to keep the system secure yet user-friendly.</p>
<p>As threats grow more sophisticated, so must our defenses. ZTA provides a durable foundation – ready to evolve with emerging technologies like adaptive biometrics and AI-driven risk engines. Organizations investing in it today will be better equipped to meet tomorrow’s security and usability demands.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
