<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>System Design on Shane&apos;s Personal Blog</title><description>Recent content in System Design on Shane&apos;s Personal Blog</description><link>https://shanechang.com/tags/system-design/</link><language>en-us</language><lastBuildDate>Sat, 22 Nov 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/tags/system-design/index.xml" rel="self" type="application/rss+xml"/><item><title>Where Stateless APIs Meet Stateful Databases</title><link>https://shanechang.com/p/backend-boundaries/</link><guid isPermaLink="true">https://shanechang.com/p/backend-boundaries/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.BLbO8Wh-_Z1JfbR6.webp&quot; alt=&quot;Featured image of post Where Stateless APIs Meet Stateful Databases&quot; /&gt;&lt;p&gt;I used to treat databases like magic.&lt;/p&gt;
&lt;p&gt;The tutorials always showed the same pattern: create an engine, make a session factory, write a dependency that yields sessions. It worked. I copied it everywhere. But I never really understood &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Then I started building background workers and suddenly those copy-paste patterns stopped working. I’d stare at my code, confused:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why do we keep the database engine around forever?&lt;/li&gt;
&lt;li&gt;Why do we recreate sessions for every request?&lt;/li&gt;
&lt;li&gt;If APIs should be “stateless,” why are we persisting &lt;em&gt;anything&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Is S3 stateless or stateful? What about Redis?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Something wasn’t adding up.&lt;/p&gt;
&lt;h2 id=&quot;the-tension-that-wouldnt-go-away&quot;&gt;The Tension That Wouldn’t Go Away&lt;/h2&gt;
&lt;p&gt;Everyone says: “Build stateless APIs.”&lt;/p&gt;
&lt;p&gt;But in practice, every backend I’ve built keeps things around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database engines that live for the entire process&lt;/li&gt;
&lt;li&gt;Session factories stored in app state&lt;/li&gt;
&lt;li&gt;S3 clients tucked into context objects&lt;/li&gt;
&lt;li&gt;Redis connections shared across requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So which is it? Are we stateless or not?&lt;/p&gt;
&lt;p&gt;For a long time, my brain filed this under: “This is just what you do.” Which was really code for: “I don’t understand this, but it seems to work, so let’s move on.”&lt;/p&gt;
&lt;h2 id=&quot;the-mental-model-that-changed-everything&quot;&gt;The Mental Model That Changed Everything&lt;/h2&gt;
&lt;p&gt;Here’s the picture that finally made sense.&lt;/p&gt;
&lt;p&gt;Imagine a horizontal line cutting through your entire system:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Above the line:&lt;/strong&gt; Your request handlers and background tasks.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They behave like they’re stateless&lt;/li&gt;
&lt;li&gt;Each request is its own clean unit of work&lt;/li&gt;
&lt;li&gt;No hidden state carried between requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Below the line:&lt;/strong&gt; The persistence world.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Postgres with its tables and rows&lt;/li&gt;
&lt;li&gt;S3 with its buckets and objects&lt;/li&gt;
&lt;li&gt;Redis with its keys and values&lt;/li&gt;
&lt;li&gt;This is where &lt;em&gt;real state&lt;/em&gt; lives across everything&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;On the line:&lt;/strong&gt; Client objects that connect the two worlds.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Database engines with their connection pools&lt;/li&gt;
&lt;li&gt;S3 clients with their configuration&lt;/li&gt;
&lt;li&gt;Redis clients with their connection management&lt;/li&gt;
&lt;li&gt;These are the &lt;em&gt;bridges&lt;/em&gt; between your stateless code and the stateful world&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once I saw it this way, everything started making sense.&lt;/p&gt;
&lt;p&gt;Your code isn’t &lt;em&gt;cheating&lt;/em&gt; when it keeps an engine around. You’re not breaking the “stateless” rule. You’re just maintaining the infrastructure to &lt;em&gt;access&lt;/em&gt; the stateful world cleanly.&lt;/p&gt;
&lt;h2 id=&quot;why-databases-feel-so-special&quot;&gt;Why Databases Feel So Special&lt;/h2&gt;
&lt;p&gt;Let’s be honest: databases get way more ceremony than S3 or most other services. There’s a reason for that.&lt;/p&gt;
&lt;h3 id=&quot;connection-pools-are-serious-business&quot;&gt;Connection Pools Are Serious Business&lt;/h3&gt;
&lt;p&gt;Imagine your database is a restaurant with exactly 100 tables.&lt;/p&gt;
&lt;p&gt;If every customer (request) brought their own table and left it there, the restaurant would fill up instantly. Even worse, if you have 20 servers (workers) and each brings 10 tables per second… you see the problem.&lt;/p&gt;
&lt;p&gt;So instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The restaurant has a &lt;strong&gt;pool&lt;/strong&gt; of tables it manages (the engine)&lt;/li&gt;
&lt;li&gt;Customers sit down, eat (do their work), and leave&lt;/li&gt;
&lt;li&gt;The same table gets reused for the next customer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In code terms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One engine per process manages a small pool of connections&lt;/li&gt;
&lt;li&gt;Each request grabs a connection from the pool&lt;/li&gt;
&lt;li&gt;When the request finishes, the connection goes back&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t style. This is survival.&lt;/p&gt;
&lt;h3 id=&quot;transactions-need-their-own-space&quot;&gt;Transactions Need Their Own Space&lt;/h3&gt;
&lt;p&gt;A database session isn’t just a connection—it’s a &lt;strong&gt;transaction boundary&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of it like this: a session is a private notepad where you write down everything you want to change. At the end, you either tear out the page and hand it in (commit), or crumple it up (rollback).&lt;/p&gt;
&lt;p&gt;If you share that notepad across multiple requests, suddenly you’re mixing everyone’s changes together. One request thinks it saved an order, another thinks it cancelled it, and the database has no idea what you actually want.&lt;/p&gt;
&lt;p&gt;That’s why sessions are &lt;strong&gt;strictly per-request&lt;/strong&gt;. One request, one notepad, clean start to finish.&lt;/p&gt;
&lt;h2 id=&quot;what-about-s3&quot;&gt;What About S3?&lt;/h2&gt;
&lt;p&gt;S3 is interesting because it sits on the boundary differently.&lt;/p&gt;
&lt;p&gt;Yes, S3 stores files that persist. Upload something today, download it tomorrow. It’s absolutely stateful.&lt;/p&gt;
&lt;p&gt;But—and this is the key difference—you talk to S3 over HTTP. Every upload, every download is its own independent request. There’s no “transaction” that spans multiple operations. No notepad that tracks changes before you commit them.&lt;/p&gt;
&lt;p&gt;Think of it like mail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; You’re in a room working on documents. You can see everything you’ve written, make changes, and decide later whether to file them or throw them out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S3:&lt;/strong&gt; You’re sending letters. Each letter is complete and independent. The post office doesn’t remember your previous letters or hold them in some draft state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both are stateful systems below the line. But databases need more careful handling because of connections and transactions.&lt;/p&gt;
&lt;h2 id=&quot;the-real-meaning-of-stateless&quot;&gt;The Real Meaning of “Stateless”&lt;/h2&gt;
&lt;p&gt;Here’s where my confusion cleared up.&lt;/p&gt;
&lt;p&gt;When people say “build stateless APIs,” they don’t mean “never keep anything in memory.”&lt;/p&gt;
&lt;p&gt;They mean: &lt;strong&gt;each request should not depend on hidden state from previous requests.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ Don’t keep a database session alive between requests&lt;/li&gt;
&lt;li&gt;❌ Don’t store “current user” in a global variable&lt;/li&gt;
&lt;li&gt;❌ Don’t rely on some mutable object that changes across requests&lt;/li&gt;
&lt;li&gt;✅ Do keep shared infrastructure (engines, clients, config)&lt;/li&gt;
&lt;li&gt;✅ Do create fresh sessions/contexts for each request&lt;/li&gt;
&lt;li&gt;✅ Do read and write actual state from databases, caches, and storage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shared objects are &lt;em&gt;tools&lt;/em&gt;, not business state. Your real state lives below the boundary, where it belongs.&lt;/p&gt;
&lt;h2 id=&quot;when-this-really-mattered-background-workers&quot;&gt;When This Really Mattered: Background Workers&lt;/h2&gt;
&lt;p&gt;All of this became concrete when I started building background workers.&lt;/p&gt;
&lt;p&gt;In a web framework, you get this for free. The framework handles creating sessions per request and cleaning them up. You mostly just write your business logic.&lt;/p&gt;
&lt;p&gt;But in a background worker? Suddenly you have to decide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What goes in the worker’s context (shared across all tasks)?&lt;/li&gt;
&lt;li&gt;What gets created fresh for each task?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s the boundary decision, made explicit.&lt;/p&gt;
&lt;p&gt;A clean pattern:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# In worker setup (long-lived)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;db_session_factory&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; SessionFactory(engine)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;s3_client&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; S3Client(config)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# In each task (short-lived)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; process_document&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(context, document_id):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    session &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;db_session_factory&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;].create()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    s3 &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; context[&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;s3_client&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # Do work with fresh session&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # Session tracks its own transaction&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;    # Then it&apos;s done and cleaned up&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The factories and clients live on the boundary. The session and your business logic live above it, clean and independent.&lt;/p&gt;
&lt;h2 id=&quot;why-this-mental-model-is-worth-having&quot;&gt;Why This Mental Model Is Worth Having&lt;/h2&gt;
&lt;p&gt;I didn’t realize I was on autopilot until I had to wire things myself.&lt;/p&gt;
&lt;p&gt;The FastAPI tutorial says: “Do this.” So I did it. The Litestar docs say: “Put this here.” So I put it there.&lt;/p&gt;
&lt;p&gt;And that works… until you step off the happy path:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple workers&lt;/li&gt;
&lt;li&gt;Background tasks&lt;/li&gt;
&lt;li&gt;Mixing databases, S3, Redis, message queues&lt;/li&gt;
&lt;li&gt;Debugging weird connection issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that point, copy-paste stops being enough. You need to understand &lt;em&gt;why&lt;/em&gt; things are shaped the way they are.&lt;/p&gt;
&lt;p&gt;For me, the breakthrough was seeing the boundary:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Below:&lt;/strong&gt; Persistent systems (Postgres, S3, Redis) where real state lives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On:&lt;/strong&gt; Client objects (engines, session factories, S3 clients) that bridge the gap&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Above:&lt;/strong&gt; Stateless-ish request handlers that do work and move on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you see it, a lot of “magic patterns” become obvious design decisions.&lt;/p&gt;
&lt;p&gt;You’re not keeping a database engine around because of some weird Python quirk. You’re keeping it around because creating connection pools is expensive and they’re meant to be shared.&lt;/p&gt;
&lt;p&gt;You’re not creating sessions per request because the tutorial said so. You’re doing it because transactions need clean boundaries and connection pools need to reuse connections.&lt;/p&gt;
&lt;p&gt;You’re not being inconsistent when you share an S3 client but create fresh database sessions. You’re recognizing that different systems have different interaction models.&lt;/p&gt;
&lt;h2 id=&quot;tldr&quot;&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;If you’re coming back to this later and just need the core ideas:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stateless doesn’t mean no shared objects.&lt;/strong&gt; It means each request doesn’t rely on hidden mutable state from previous requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The boundary model:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Below:&lt;/strong&gt; Persistent systems (databases, S3, Redis)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On:&lt;/strong&gt; Client infrastructure (engines, session factories, clients)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Above:&lt;/strong&gt; Per-request logic (sessions, services, handlers)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Databases feel special because:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection pools have hard limits&lt;/li&gt;
&lt;li&gt;Transactions need clean boundaries&lt;/li&gt;
&lt;li&gt;Sessions track changes before commit/rollback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;In practice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Share: engines, clients, configuration, factories&lt;/li&gt;
&lt;li&gt;Create fresh: sessions, request contexts, transaction boundaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The next time you see &lt;code&gt;engine&lt;/code&gt;, &lt;code&gt;sessionmaker&lt;/code&gt;, &lt;code&gt;Session&lt;/code&gt;, or &lt;code&gt;s3_client&lt;/code&gt;, you’ll know &lt;em&gt;where&lt;/em&gt; they live in your architecture and &lt;em&gt;why&lt;/em&gt; they’re shaped that way.&lt;/p&gt;
&lt;p&gt;Not just magic patterns. Conscious design decisions about where the line is between your code and the stateful world below it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;(Written by Human, improved using AI where applicable.)&lt;/p&gt;</description><pubDate>Sat, 22 Nov 2025 00:00:00 GMT</pubDate></item><item><title>Why AI Cannot Engineer</title><link>https://shanechang.com/p/why-ai-cannot-engineer/</link><guid isPermaLink="true">https://shanechang.com/p/why-ai-cannot-engineer/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.CTct8cc3_1GfOkr.webp&quot; alt=&quot;Featured image of post Why AI Cannot Engineer&quot; /&gt;&lt;p&gt;I asked Claude to build me a customer dashboard last month. Ten minutes later, I had gorgeous React components, a clean API structure, and authentication that worked flawlessly on my laptop. I felt like a wizard.&lt;/p&gt;
&lt;p&gt;Two days later, when we put it in front of actual users, everything fell apart. The API crumbled under concurrent requests. The authentication tokens expired in the middle of user sessions. The database queries that felt snappy with my 10 test records took 30 seconds with real data.&lt;/p&gt;
&lt;p&gt;The code looked beautiful. It even worked—sort of. But it was a disaster waiting to happen, and I had no one to blame but myself. I’d asked for a construction worker when I needed an architect.&lt;/p&gt;
&lt;h2 id=&quot;the-skyscraper-nobody-can-live-in&quot;&gt;The Skyscraper Nobody Can Live In&lt;/h2&gt;
&lt;p&gt;Think about building a skyscraper for a moment. You need two completely different types of expertise:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Architects and Engineers&lt;/strong&gt; design the building. They figure out where the load-bearing walls go, how to route all the electrical wiring and plumbing, what materials can handle the wind shear at that height, how the building responds to earthquakes. They think about fire escapes and elevator shafts and how people will actually move through the space. They create detailed specifications for everything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Construction Workers&lt;/strong&gt; build it. They know how to mix concrete, how to weld steel beams, how to install windows. Give them a good blueprint with all the specifications—“use this gauge of rebar, space them 6 inches apart, let the concrete cure for 48 hours”—and they’ll build you something solid and safe.&lt;/p&gt;
&lt;p&gt;Here’s the interesting part: construction workers understand building principles. They know that rebar makes concrete stronger, that you can’t just drill holes wherever you want in a load-bearing wall, that certain materials expand in heat. They might not be able to calculate the exact compression loads or explain the physics, but they know &lt;em&gt;how&lt;/em&gt; to build.&lt;/p&gt;
&lt;p&gt;So in theory, if you asked construction workers to build a skyscraper without giving them architectural plans, they could do it. They’d use their knowledge to put up walls and install wiring and plumbing. The building would stand.&lt;/p&gt;
&lt;p&gt;For a while.&lt;/p&gt;
&lt;p&gt;Then someone tries to actually &lt;em&gt;live&lt;/em&gt; in it. Or an earthquake hits. Or there’s a fire. And you realize the electrical system is a tangled mess that can’t handle the load. The plumbing creates massive pressure problems on the upper floors. There are no proper fire exits. The structure can’t flex with the wind. It looks like a building, but it’s not safe, not livable, not reliable. It’s a expensive disaster wrapped in a pretty facade.&lt;/p&gt;
&lt;p&gt;You really needed those architects and engineers.&lt;/p&gt;
&lt;h2 id=&quot;we-accidentally-merged-two-jobs&quot;&gt;We Accidentally Merged Two Jobs&lt;/h2&gt;
&lt;p&gt;Now let’s talk about software. In our industry, we have the same two roles—but we’ve gotten confused about which is which.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Software Engineers&lt;/strong&gt; are supposed to design systems. They think about scalability, reliability, security, maintainability. They establish patterns and guardrails. They consider edge cases: What happens when 10,000 users hit this endpoint simultaneously? How do we handle partial failures? What’s our data consistency model? How do we make this debuggable when something goes wrong at 3am? They create the “blueprints” that make the difference between software that works on localhost and software that works in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Programmers&lt;/strong&gt; write code. They understand syntax, algorithms, design patterns. They know how to implement features, how to make things work. Given good specifications and architectural guidelines, they write solid, maintainable code that does exactly what it needs to do.&lt;/p&gt;
&lt;p&gt;But here’s where we got confused: engineers also write code. They have to—you can’t design a system well if you don’t understand implementation realities. An architect who doesn’t understand how concrete and steel actually behave can’t design a building that stands. Similarly, a software engineer needs to understand how code actually executes.&lt;/p&gt;
&lt;p&gt;Because of this overlap, we started treating them as the same job. Most companies just have “software engineers” who do everything—system design &lt;em&gt;and&lt;/em&gt; implementation. We stopped distinguishing between the person who decides “we need a caching layer with these invalidation rules” and the person who implements Redis with that specification.&lt;/p&gt;
&lt;p&gt;For years, this worked okay. The engineer-programmer would design the system in their head, then implement it. Both roles were happening, just inside one person.&lt;/p&gt;
&lt;h2 id=&quot;enter-the-ai-programmer&quot;&gt;Enter the AI Programmer&lt;/h2&gt;
&lt;p&gt;Now we have large language models that can write code. And here’s what I’ve realized: &lt;strong&gt;LLMs are incredible programmers, but terrible engineers.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;An LLM is like that construction worker who can build you a skyscraper without blueprints. Ask it to “build me a todo list app,” and it will. The code will be clean. It’ll have proper separation of concerns. The variable names will make sense. It’ll work beautifully on your laptop.&lt;/p&gt;
&lt;p&gt;But it’s missing all the engineering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No load considerations:&lt;/strong&gt; It’ll use an in-memory array for the todo list, which works great for testing but fails when you have 10,000 users&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No failure handling:&lt;/strong&gt; What happens when the database connection drops mid-request? The LLM’s code probably doesn’t handle that gracefully&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No security thinking:&lt;/strong&gt; Authentication might be present, but is it actually secure against common attacks? Are there SQL injection vulnerabilities?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No performance engineering:&lt;/strong&gt; That nested loop through every user’s todos on every request? Didn’t consider that case&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No operational thinking:&lt;/strong&gt; Good luck debugging when something goes wrong—there’s minimal logging and no monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code &lt;em&gt;looks&lt;/em&gt; professional. It follows good practices. It even works, in the ideal case. But the moment reality hits—concurrent users, network failures, edge cases, malicious input—it falls apart.&lt;/p&gt;
&lt;p&gt;Just like that skyscraper built without architects.&lt;/p&gt;
&lt;h2 id=&quot;what-this-actually-means&quot;&gt;What This Actually Means&lt;/h2&gt;
&lt;p&gt;I used to think the distinction between “engineer” and “programmer” was just semantic nitpicking. Now I realize it’s crucial, and we’re about to learn this lesson the hard way.&lt;/p&gt;
&lt;p&gt;When you tell an LLM “build me an authentication system,” you’re giving it a task without constraints. It’s like telling a construction worker “build me a building” without specifying:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How many people it needs to support&lt;/li&gt;
&lt;li&gt;What kind of disasters it needs to withstand&lt;/li&gt;
&lt;li&gt;What the fire safety requirements are&lt;/li&gt;
&lt;li&gt;How the utilities should be routed&lt;/li&gt;
&lt;li&gt;What the budget constraints are&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The LLM will build you &lt;em&gt;something&lt;/em&gt;. That something might even impress you. But it won’t be robust, because nobody did the engineering.&lt;/p&gt;
&lt;p&gt;Here’s a real example from my startup, Empath Legal. We’re building jury selection tools for attorneys. I could ask an LLM: “Create an API endpoint that analyzes jury questionnaires and returns insights.”&lt;/p&gt;
&lt;p&gt;The LLM would build me something functional:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It would parse the questionnaires&lt;/li&gt;
&lt;li&gt;It would call an LLM to analyze the text&lt;/li&gt;
&lt;li&gt;It would return formatted results&lt;/li&gt;
&lt;li&gt;The code would be clean and well-structured&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here’s what it wouldn’t consider without engineering input:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Business requirements I need to specify:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These questionnaires can be 50+ pages and attorneys need results in under 30 seconds&lt;/li&gt;
&lt;li&gt;Our customers are lawyers who need citations for everything—we can’t just hallucinate insights&lt;/li&gt;
&lt;li&gt;The analysis must be deterministic enough that an attorney can rely on it in court&lt;/li&gt;
&lt;li&gt;We have a credit-based pricing model, so we need to track and limit usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Technical guardrails I need to establish:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We need to chunk large questionnaires intelligently (by section, not arbitrary token counts)&lt;/li&gt;
&lt;li&gt;We must implement streaming so attorneys see progressive results, not a 30-second blank screen&lt;/li&gt;
&lt;li&gt;We need a caching layer so re-analyzing the same questionnaire doesn’t burn credits&lt;/li&gt;
&lt;li&gt;We need robust error handling because LLM API calls fail sometimes&lt;/li&gt;
&lt;li&gt;We need audit trails for compliance reasons&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;System design decisions I need to make:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Should this be synchronous or async? (Async, because of the 30-second requirement)&lt;/li&gt;
&lt;li&gt;What happens if the LLM call fails midway? (We need idempotent retries)&lt;/li&gt;
&lt;li&gt;How do we handle rate limits from the LLM provider? (We need a queue with backpressure)&lt;/li&gt;
&lt;li&gt;What’s our data consistency model? (We need to decide what happens if analysis partially completes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The LLM could write beautiful code for the happy path. But without me doing the engineering work—understanding the requirements, establishing the constraints, designing the system architecture—that code would be worthless in production.&lt;/p&gt;
&lt;h2 id=&quot;the-new-division-of-labor&quot;&gt;The New Division of Labor&lt;/h2&gt;
&lt;p&gt;This isn’t about LLMs being bad. They’re phenomenal at what they do. This is about understanding what they do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLMs are expert programmers:&lt;/strong&gt; Give them a well-specified task with clear constraints, and they’ll implement it beautifully. They know patterns, they write clean code, they can even debug and optimize within a defined scope.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Humans need to be the engineers:&lt;/strong&gt; We need to understand the business requirements, design the system architecture, establish the guardrails and principles, consider the edge cases and failure modes. We need to ask: What are we really trying to achieve? What could go wrong? How does this need to scale?&lt;/p&gt;
&lt;p&gt;This is actually liberating, if you think about it. We can offload the tedious parts—writing boilerplate, implementing standard patterns, translating specifications into code—and focus on the interesting parts: understanding user needs, designing robust systems, making architectural tradeoffs.&lt;/p&gt;
&lt;p&gt;But only if we actually &lt;em&gt;do&lt;/em&gt; the engineering work. If we just prompt “build me X” without providing context, constraints, and architectural guidance, we get that beautiful skyscraper that nobody can live in.&lt;/p&gt;
&lt;h2 id=&quot;how-to-actually-work-with-ai-coding-tools&quot;&gt;How to Actually Work with AI Coding Tools&lt;/h2&gt;
&lt;p&gt;Here’s what I’ve learned about working effectively with LLMs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bad prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Build me a payment system for my SaaS app”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Build me a payment system with these requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Support credit-based pricing (users buy credits, features consume credits)&lt;/li&gt;
&lt;li&gt;Must handle concurrent credit deductions safely (two API calls shouldn’t both succeed if there’s only enough credits for one)&lt;/li&gt;
&lt;li&gt;Need audit trail for billing disputes&lt;/li&gt;
&lt;li&gt;Should fail gracefully if payment processor is down (degrade to cached credit counts, log for reconciliation)&lt;/li&gt;
&lt;li&gt;Performance requirement: credit check must add &amp;#x3C; 50ms to request latency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use PostgreSQL with row-level locking for credit deductions. Implement idempotent credit operations so retries don’t double-charge. Include comprehensive logging for debugging billing issues.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second prompt does the engineering work. It specifies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The business model (credit-based pricing)&lt;/li&gt;
&lt;li&gt;The critical constraint (safe concurrent access)&lt;/li&gt;
&lt;li&gt;The compliance requirement (audit trail)&lt;/li&gt;
&lt;li&gt;The reliability requirement (graceful degradation)&lt;/li&gt;
&lt;li&gt;The performance requirement (latency budget)&lt;/li&gt;
&lt;li&gt;The technical approach (PostgreSQL with row locks)&lt;/li&gt;
&lt;li&gt;The operational requirement (debuggable)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the LLM can write code that actually works in production, because I’ve given it the architectural blueprint.&lt;/p&gt;
&lt;h2 id=&quot;the-uncomfortable-truth&quot;&gt;The Uncomfortable Truth&lt;/h2&gt;
&lt;p&gt;Most of us who’ve been writing code for years have been doing both roles simultaneously for so long that we don’t even notice when we’re switching hats. When we sit down to “code,” we’re actually doing engineering work (thinking about requirements and constraints) &lt;em&gt;and then&lt;/em&gt; programming work (implementing the solution).&lt;/p&gt;
&lt;p&gt;With LLMs, we can’t do that anymore. The programming part gets delegated to the AI. But if we don’t explicitly do the engineering part ourselves—if we just treat the LLM as a magic “build me X” box—we get code that works in the demo but fails in reality.&lt;/p&gt;
&lt;p&gt;And here’s the uncomfortable part: &lt;strong&gt;many people who call themselves engineers are actually just programmers&lt;/strong&gt;. They’re really good at writing code, but they’ve never had to think deeply about system design because they were implementing someone else’s architecture, or working on small enough systems that the complexity didn’t matter.&lt;/p&gt;
&lt;p&gt;With LLMs, this distinction now matters. The code-writing skills that used to be valuable are being commoditized. What’s becoming valuable is the engineering thinking: understanding requirements, designing robust systems, making good architectural tradeoffs, considering failure modes.&lt;/p&gt;
&lt;h2 id=&quot;what-this-means-for-you&quot;&gt;What This Means For You&lt;/h2&gt;
&lt;p&gt;If you’re learning to code right now, don’t just learn syntax and frameworks. Learn to think like an engineer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When you build something, ask: “What happens when 1000 people use this simultaneously?”&lt;/li&gt;
&lt;li&gt;Before writing code, ask: “What are all the ways this could fail, and how should it handle each one?”&lt;/li&gt;
&lt;li&gt;Study real-world system design, not just toy examples&lt;/li&gt;
&lt;li&gt;Learn to write specifications that would let someone else implement your vision correctly&lt;/li&gt;
&lt;li&gt;Understand the &lt;em&gt;why&lt;/em&gt; behind patterns, not just the &lt;em&gt;how&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re already working as a developer, pay attention to which role you’re actually performing. Are you mostly implementing features based on specs? You’re in the programmer role, and LLMs are coming for that job. Start developing engineering skills: system design, requirements analysis, architectural thinking.&lt;/p&gt;
&lt;p&gt;If you’re hiring, stop asking “can you code fizzbuzz?” and start asking “design a URL shortener that handles 1000 requests/second and explain your failure modes.” The code-writing part is increasingly automated. The thinking part isn’t.&lt;/p&gt;
&lt;h2 id=&quot;the-punchline&quot;&gt;The Punchline&lt;/h2&gt;
&lt;p&gt;Here’s the irony: I wrote this post with Claude’s help. Not because I couldn’t write it myself, but because Claude is a better &lt;em&gt;programmer&lt;/em&gt; than I am. I gave it my core idea, my architectural guidelines (the blog writing guide), my requirements (make it engaging and human), and my constraints (use the construction analogy, include real examples).&lt;/p&gt;
&lt;p&gt;Claude wrote beautiful prose. But I did the engineering: I decided what to communicate, how to structure the argument, what examples would land, what the reader needs to understand.&lt;/p&gt;
&lt;p&gt;The future of software development isn’t “no human involved.” It’s humans doing what humans are uniquely good at—understanding messy real-world requirements, making tradeoffs with incomplete information, designing robust systems for complex environments—while AI does what it’s uniquely good at: writing clean, correct code that implements those specifications.&lt;/p&gt;
&lt;p&gt;But only if we remember to do the engineering work. Otherwise, we’re just building skyscrapers without architects.&lt;/p&gt;
&lt;p&gt;And we all know how that story ends.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;What’s your experience with AI coding tools? Are you finding yourself doing more engineering thinking, or just prompting and hoping? I’d love to hear your stories—the spectacular failures are often more educational than the successes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Written by Human, improved using AI where applicable.)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate></item><item><title>The 12-Factor App: Building Software Like LEGO in the Cloud Era</title><link>https://shanechang.com/p/twelve-factor-app-lego-cloud-era/</link><guid isPermaLink="true">https://shanechang.com/p/twelve-factor-app-lego-cloud-era/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.B0hq-9yw_Z1Q0puh.webp&quot; alt=&quot;Featured image of post The 12-Factor App: Building Software Like LEGO in the Cloud Era&quot; /&gt;&lt;h2 id=&quot;intro&quot;&gt;Intro&lt;/h2&gt;
&lt;p&gt;Picture this: It’s 3 AM, and your app just went viral. Traffic spikes from 100 users to 100,000 in minutes. Half your team is asleep, the other half is panicking. Will your application gracefully scale to handle the load, or will it crumble under pressure?&lt;/p&gt;
&lt;p&gt;As a startup, this scenario haunted me as I build our application—until I read something that fundamentally changed how I think about building applications. It’s called the 12-factor app methodology (knowledge and wisdoms discovered like this always humbles me—we are, after all, standing on the shoulders of giants, and hoping to see further into the future)&lt;/p&gt;
&lt;p&gt;Here’s how I think about this: &lt;strong&gt;A 12-factor app is like a LEGO brick for infrastructure&lt;/strong&gt;. You build it with zero assumptions about the outside world, purely implementing internal logic while letting the platform decide everything els—what inputs to feed it, where outputs go, when to start, stop, duplicate, or destroy it. The app becomes not just a one-off creation but an infinitely scalable building block (like lego for the cloud).&lt;/p&gt;
&lt;h2 id=&quot;the-three-pillars-understanding-the-architecture&quot;&gt;The Three Pillars: Understanding the Architecture&lt;/h2&gt;
&lt;p&gt;Before diving into the twelve factors, let me share the mental model that finally made everything click. Every interaction in a 12-factor app falls into one of three categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The App Itself&lt;/strong&gt; - The immutable, stateless core of your logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Interface&lt;/strong&gt; - How your app communicates with the outside world&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Platform&lt;/strong&gt; - The environment that orchestrates everything&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think of it like this: Your app is a chef in a kitchen. The chef (the app) has specific skills and recipes. The kitchen setup—where ingredients come from, where dishes go, what equipment is available—that’s the interface. The restaurant management deciding when the chef works, how many chefs to hire, which orders to prioritize—that’s the platform.&lt;/p&gt;
&lt;p&gt;This separation is what makes the magic happen. Let’s explore how each factor reinforces this architecture.&lt;/p&gt;
&lt;h2 id=&quot;part-1-the-foundation---managing-your-code-and-dependencies&quot;&gt;Part 1: The Foundation - Managing Your Code and Dependencies&lt;/h2&gt;
&lt;h3 id=&quot;factor-i-codebase---your-single-source-of-truth&quot;&gt;Factor I: Codebase - Your Single Source of Truth&lt;/h3&gt;
&lt;p&gt;Remember the last time you heard “it works on my machine” or spent hours figuring out which version of the code was actually running in production? The codebase principle eliminates these nightmares.&lt;/p&gt;
&lt;p&gt;Imagine your codebase as a map where every piece of code has exact coordinates—the X-axis represents different versions, the Y-axis represents different deployments. Any snapshot of your code can be pinpointed precisely on this map. When something breaks at 3 AM, you know &lt;em&gt;exactly&lt;/em&gt; which code is running where.&lt;/p&gt;
&lt;p&gt;This has not be a problem personally—since I adopted github blindly (shame on me) during college, I always put my code there (I naively thought of it as a cloud for my code. I can just pull code from anywhere—fun!) but stupid me: its way more complex than that, which I learned as I use github with teammates. The only way you truly understand how a version control navigates landmines is to actually use it wrong (which i did lol) and get hit on the head with a brick. Then you learn.&lt;/p&gt;
&lt;h3 id=&quot;factors-ii--iii-dependencies-and-config---the-input-controls&quot;&gt;Factors II &amp;#x26; III: Dependencies and Config - The Input Controls&lt;/h3&gt;
&lt;p&gt;These two factors are cousins—they both control how the outside world shapes your app’s behavior, but in subtly different ways.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependencies&lt;/strong&gt; are like the ingredients list for a recipe. You declare exactly what you need (Python packages, Node modules, system libraries) in a manifest file. No assumptions, no “it should probably have this installed.” Every dependency is explicit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configuration&lt;/strong&gt;, on the other hand, is like the knobs on a mixing board. Same equipment, different settings for different venues. Your database credentials, API keys, feature flags—these change between environments, but your code doesn’t.&lt;/p&gt;
&lt;p&gt;Here’s the crucial insight: Your app’s config is everything that varies between deploys. If you can’t open-source your code right now without leaking credentials, you haven’t separated config from code properly.&lt;/p&gt;
&lt;p&gt;for python, what I like to do with settings is just use dotenv + dataclass. Reliable, barebone yet powerful.&lt;/p&gt;
&lt;h3 id=&quot;factor-iv-backing-services---your-apps-supporting-cast&quot;&gt;Factor IV: Backing Services - Your App’s Supporting Cast&lt;/h3&gt;
&lt;p&gt;Backing services are where input and output blur together. Your database, message queue, email service—they’re both sources of input (what data do I read?) and destinations for output (where do I write?).&lt;/p&gt;
&lt;p&gt;The breakthrough principle here: &lt;strong&gt;treat every backing service as an attached resource&lt;/strong&gt;. Your app shouldn’t care whether PostgreSQL is running on the same machine, in a different datacenter, or managed by a third party. It’s just a URL and credentials in your config.&lt;/p&gt;
&lt;p&gt;For example, this really shines when you need to migrate from a self-hosted PostgreSQL to Amazon RDS—if you treat the database as an attached resource, the migration is simply config change. (the problem is never about coding, the problem is always about the bug that comes with new codes. LOL)&lt;/p&gt;
&lt;h2 id=&quot;part-2-the-build-pipeline---from-code-to-running-process&quot;&gt;Part 2: The Build Pipeline - From Code to Running Process&lt;/h2&gt;
&lt;h3 id=&quot;factor-v-build-release-run---the-three-stage-rocket&quot;&gt;Factor V: Build, Release, Run - The Three-Stage Rocket&lt;/h3&gt;
&lt;p&gt;Lets talk about bread. 4AM in the morning, You bake it (Build), then you got a bunch of breads at 9am when you open (releases), then you sell the breads during the day (run). Sure if you are a small bakery you can live with baking fresh breads and sell them right after (mix build and run) but if you bake the bread wrong, your customer would need to wait an extra hour for a new batch. It would be so much better if you bake early and have a bunch of ready bread and satisified customers!&lt;/p&gt;
&lt;p&gt;Now that you have a rough understanding what the three stages are, lets dive deeper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build Stage&lt;/strong&gt;: Your code + dependencies become an executable (compile, bundle, package)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release Stage&lt;/strong&gt;: That executable + configuration becomes a numbered release&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run Stage&lt;/strong&gt;: That release executes in the production environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each release is immutable—a frozen snapshot combining a specific build with specific configuration. When release v427 starts acting weird, you can instantly roll back to v426. You’re not scrambling to figure out what changed; you’re switching between known, tested states.&lt;/p&gt;
&lt;p&gt;The beauty? Problems in the build stage happen when developers are awake and watching. Problems in the run stage can be solved by rolling back to a known-good release. No more debugging in production at 3 AM.&lt;/p&gt;
&lt;h3 id=&quot;factor-vi-processes---the-stateless-mandate&quot;&gt;Factor VI: Processes - The Stateless Mandate&lt;/h3&gt;
&lt;p&gt;Your app processes should be like workers on an assembly line—they process what comes to them but don’t store anything locally. Need to save data? Use a database. Need to cache something? Use Redis. Need to store files? Use S3.&lt;/p&gt;
&lt;p&gt;When your processes are stateless, they become disposable. You can start them, stop them, crash them, multiply them—and your users never notice. It’s liberating.&lt;/p&gt;
&lt;h3 id=&quot;factor-vii-port-binding---self-contained-services&quot;&gt;Factor VII: Port Binding - Self-Contained Services&lt;/h3&gt;
&lt;p&gt;Your app should be completely self-contained, exporting its services by binding to a port. No runtime injection of web servers, no complex application containers—just your app listening on a port.&lt;/p&gt;
&lt;p&gt;This means your Python app includes its own web server (like Gunicorn), your Ruby app brings Thin or Puma, your Java app packages Jetty. The app becomes a standalone service that says, “I’m listening on port 5000. Send me requests.” (This is why RESTful is so elegant—it is a standard that unify the world!)&lt;/p&gt;
&lt;p&gt;Why does this matter? Because it makes your app composable. Today’s web app becomes tomorrow’s backing service for another app. Just point to its URL and port. It’s LEGO bricks all the way down.&lt;/p&gt;
&lt;h2 id=&quot;part-3-operations---running-in-the-wild&quot;&gt;Part 3: Operations - Running in the Wild&lt;/h2&gt;
&lt;h3 id=&quot;factor-viii-concurrency---scale-out-not-up&quot;&gt;Factor VIII: Concurrency - Scale Out, Not Up&lt;/h3&gt;
&lt;p&gt;Typically when building local apps, the instinct is to make your app handle more concurrent request—add threads, increase the connection pool, optimize the event loop. They make things faster. But for web applications that needs to scale, the 12-factor way says: don’t.&lt;/p&gt;
&lt;p&gt;Instead, keep your app simple—handle requests one at a time if that’s natural. Let the platform handle concurrency by running multiple copies of your process. Need to handle 10x traffic? Run 10x processes. It’s crude, but it works brilliantly.&lt;/p&gt;
&lt;p&gt;Think of it like a restaurant. The traditional approach is training your one chef to cook faster, juggle more pans, multitask frantically. The 12-factor approach? Hire more chefs. Each chef works at a sustainable pace, and you scale by adding chefs, not by working them harder.&lt;/p&gt;
&lt;h3 id=&quot;factor-ix-disposability---fast-startup-graceful-shutdown&quot;&gt;Factor IX: Disposability - Fast Startup, Graceful Shutdown&lt;/h3&gt;
&lt;p&gt;Your processes should be like Phoenix—ready to die and be reborn at a moment’s notice. Fast startup—meaning lazy approach, and graceful shutdown (finish current work, then exit cleanly).&lt;/p&gt;
&lt;p&gt;This disposability enables the magic of modern deployment. Rolling updates, automatic scaling, self-healing systems—they all depend on processes that can be created and destroyed without drama.&lt;/p&gt;
&lt;h3 id=&quot;factor-x-devprod-parity---closing-the-gap&quot;&gt;Factor X: Dev/Prod Parity - Closing the Gap&lt;/h3&gt;
&lt;p&gt;The traditional gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time gap&lt;/strong&gt;: Code written Monday, deployed next month&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personnel gap&lt;/strong&gt;: Developers write, ops team deploys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools gap&lt;/strong&gt;: SQLite in dev, PostgreSQL in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The 12-factor approach obliterates these gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deploy hours or minutes after writing code&lt;/li&gt;
&lt;li&gt;Developers are involved in deployment&lt;/li&gt;
&lt;li&gt;Use the same backing services everywhere&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We all know why mutex in multi-threading is important: it prevents race conditions where unpredictable things could happen when state is volatile. The same logic applies here: you want the gap to be as small and controlled as possible when you are between volatile / sensitive states so that you always have a preditive result, rather than chaos.&lt;/p&gt;
&lt;h2 id=&quot;part-4-observability---logs-and-admin-tasks&quot;&gt;Part 4: Observability - Logs and Admin Tasks&lt;/h2&gt;
&lt;h3 id=&quot;factor-xi-logs---the-event-stream&quot;&gt;Factor XI: Logs - The Event Stream&lt;/h3&gt;
&lt;p&gt;Your app should write logs like a diary writer who doesn’t care who reads it—just stream consciousness to stdout. Don’t manage log files, don’t rotate logs, don’t even think about where they go. Just write to stdout and let the platform handle the rest.&lt;/p&gt;
&lt;p&gt;This seemed weird until I saw its power. In development, logs appear in your terminal. In production, the platform can route them anywhere—to files, to Elasticsearch, to DataDog, to multiple destinations simultaneously. Your app doesn’t know or care.&lt;/p&gt;
&lt;p&gt;It’s the same philosophy: the app does one thing (emit events), the platform handles the complexity (routing, storage, analysis).&lt;/p&gt;
&lt;h3 id=&quot;factor-xii-admin-processes---one-off-tasks-in-familiar-territory&quot;&gt;Factor XII: Admin Processes - One-Off Tasks in Familiar Territory&lt;/h3&gt;
&lt;p&gt;Database migrations, console sessions, data fix scripts—these admin processes should run in the same environment as your regular app processes. Same code, same config, same dependencies.&lt;/p&gt;
&lt;p&gt;Why? Because nothing is worse than a migration script that works perfectly in staging but fails in production because of some subtle environmental difference. When admin processes run in identical environments, you eliminate an entire category of “works on my machine” problems.&lt;/p&gt;
&lt;h2 id=&quot;the-revelation-its-all-about-contracts&quot;&gt;The Revelation: It’s All About Contracts&lt;/h2&gt;
&lt;p&gt;The 12-factor methodology is never about the twelve specific factors. It’s about defining a clean contract between your application and the platform it runs on.&lt;/p&gt;
&lt;p&gt;Your app promises:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To be stateless and disposable&lt;/li&gt;
&lt;li&gt;To declare its needs explicitly&lt;/li&gt;
&lt;li&gt;To communicate through standard interfaces&lt;/li&gt;
&lt;li&gt;To log to stdout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The platform promises:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To provide configuration&lt;/li&gt;
&lt;li&gt;To manage processes&lt;/li&gt;
&lt;li&gt;To route requests&lt;/li&gt;
&lt;li&gt;To handle logs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this contract in place, something magical happens. Your app becomes truly portable. It can run on Heroku today, Kubernetes tomorrow, and whatever comes next. It can scale from one user to millions without code changes. It becomes a true building block—a LEGO brick that clicks perfectly into any modern infrastructure.&lt;/p&gt;</description><pubDate>Tue, 19 Aug 2025 00:00:00 GMT</pubDate></item></channel></rss>