<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Antonio B De Castro | Expert Technologist</title><link>https://decastro.work/</link><description>Recent content in Home on Antonio B De Castro | Expert Technologist</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>abdecastro@protonmail.com (Antonio B De Castro)</managingEditor><webMaster>abdecastro@protonmail.com (Antonio B De Castro)</webMaster><copyright>Antonio B De Castro</copyright><lastBuildDate>Tue, 25 Nov 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://decastro.work/index.xml" rel="self" type="application/rss+xml"/><item><title>The 2025 Developer Stack: What I'd Choose Starting from Scratch</title><link>https://decastro.work/blog/2025-developer-stack-starting-from-scratch/</link><pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/2025-developer-stack-starting-from-scratch/</guid><description>&lt;p&gt;Every few months, a “perfect stack” shows up on Twitter—complete with new acronyms and a promise that your next release will be effortless. It won’t. What actually makes developers faster &lt;em&gt;and&lt;/em&gt; keeps production calm is boring consistency: one language everywhere, one runtime that feels good day-to-day, predictable interfaces, and tooling that doesn’t collapse under real traffic. If I were building a new product today, this is the stack I’d start with—and the logic behind it.&lt;/p&gt;</description><content>&lt;p&gt;Every few months, a “perfect stack” shows up on Twitter—complete with new acronyms and a promise that your next release will be effortless. It won’t. What actually makes developers faster &lt;em&gt;and&lt;/em&gt; keeps production calm is boring consistency: one language everywhere, one runtime that feels good day-to-day, predictable interfaces, and tooling that doesn’t collapse under real traffic. If I were building a new product today, this is the stack I’d start with—and the logic behind it.&lt;/p&gt;
&lt;h2 id="typescript-everywhere-one-language-fewer-surprises"&gt;TypeScript Everywhere: One Language, Fewer Surprises&lt;/h2&gt;
&lt;p&gt;If there’s a single foundation I’d refuse to compromise on, it’s TypeScript. Not because “types are cool,” but because they eliminate entire classes of bugs created by uncertainty. Your future self doesn’t just want fewer runtime errors—they want clearer intent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Where I’d use it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backend services: TypeScript with strict settings&lt;/li&gt;
&lt;li&gt;Frontend: Svelte 5 + TypeScript&lt;/li&gt;
&lt;li&gt;Shared libraries: types, domain models, validation schemas&lt;/li&gt;
&lt;li&gt;Tooling scripts: build, migrations, CI helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Opinionated defaults I’d set immediately:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;strict: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Avoid &lt;code&gt;any&lt;/code&gt; like it’s technical debt with a pulse&lt;/li&gt;
&lt;li&gt;Prefer explicit return types for exported functions in shared packages&lt;/li&gt;
&lt;li&gt;Use project references if your repo grows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; Suppose you have a “Checkout” flow used by both web and an internal admin tool. With TypeScript, the request/response types can live in a shared &lt;code&gt;@types/checkout&lt;/code&gt; package. When the API changes, the compiler becomes your reviewer.&lt;/p&gt;
&lt;p&gt;That’s the real win: TypeScript doesn’t just catch mistakes—it shapes how your team talks about code.&lt;/p&gt;
&lt;h2 id="bun-as-the-runtime-fast-feedback-practical-ops"&gt;Bun as the Runtime: Fast Feedback, Practical Ops&lt;/h2&gt;
&lt;p&gt;Next up: &lt;strong&gt;Bun&lt;/strong&gt;. I’m not interested in declaring a winner in a runtime war; I’m interested in the developer loop. Bun’s advantage is tangible: fast installs, quick startup, and a toolchain that feels responsive. If you’re shipping frequently, the time saved compounds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I’d adopt Bun without being reckless:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Bun for local development and CI build/test steps&lt;/li&gt;
&lt;li&gt;For production, keep deployment predictable (containerize like you always do)&lt;/li&gt;
&lt;li&gt;Pin versions (both runtime and dependencies) to avoid “works on my machine” drift&lt;/li&gt;
&lt;li&gt;Benchmark only if you have evidence of a bottleneck—otherwise, trust the productivity gain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; In a monorepo, “install + build + test” can become the slowest part of your day. Bun’s speed makes that loop less painful, which encourages smaller PRs and faster iteration. Smaller PRs lead to fewer regressions. That’s how DX translates into reliability.&lt;/p&gt;
&lt;p&gt;I’ll be blunt: if a runtime makes your team’s daily workflow slower, it’s not “simpler.” It’s compounding friction.&lt;/p&gt;
&lt;h2 id="svelte-5-for-ui-a-ui-framework-that-doesnt-fight-you"&gt;Svelte 5 for UI: A UI Framework That Doesn’t Fight You&lt;/h2&gt;
&lt;p&gt;Svelte has always appealed to me because it’s pragmatic. With &lt;strong&gt;Svelte 5&lt;/strong&gt;, you get a modern component model with a strong focus on performance and developer ergonomics. The best part is that you can build product UIs without turning your app into a configuration labyrinth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I’d standardize:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A design system approach early (even if it’s lightweight)&lt;/li&gt;
&lt;li&gt;Shared UI primitives: buttons, inputs, modals, form components&lt;/li&gt;
&lt;li&gt;Strong typing for props and event payloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; Let’s say you’re building a “user preferences” screen with multiple toggles, validation, and server persistence. With Svelte + TypeScript, you can keep the entire flow coherent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Typed form state on the client&lt;/li&gt;
&lt;li&gt;Typed payload for the API call&lt;/li&gt;
&lt;li&gt;Typed response for optimistic UI updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You end up with fewer “stringly-typed” edge cases, which is where UI bugs hide.&lt;/p&gt;
&lt;p&gt;And yes—UI performance matters. But more than that, UI maintainability matters. Svelte’s mental model tends to stay readable as components grow, which is exactly what you want in teams that will change over time.&lt;/p&gt;
&lt;h2 id="apis-with-hono-or-fastapi-choose-the-one-you-can-ship-with"&gt;APIs with Hono or FastAPI: Choose the One You Can Ship With&lt;/h2&gt;
&lt;p&gt;For APIs, I’d pick between &lt;strong&gt;Hono&lt;/strong&gt; and &lt;strong&gt;FastAPI&lt;/strong&gt; based on team strengths—but I’d insist on a consistent philosophy: fast request handling, clear route structure, and strong validation at boundaries.&lt;/p&gt;
&lt;h3 id="if-your-team-leans-typescript-first-hono"&gt;If your team leans TypeScript-first: Hono&lt;/h3&gt;
&lt;p&gt;Hono fits naturally in the “TypeScript everywhere” world. You can keep request/response types aligned with your frontend and shared libraries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Use schema validation at the edges (request parsing) and keep handlers thin. Put business logic in separate modules so route files don’t become accidental architecture.&lt;/p&gt;
&lt;h3 id="if-your-team-wants-pythons-ecosystem-gravity-fastapi"&gt;If your team wants Python’s ecosystem gravity: FastAPI&lt;/h3&gt;
&lt;p&gt;FastAPI is excellent when you want rapid development with excellent validation ergonomics, and you plan to lean on Python libraries for specific tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Keep your OpenAPI contract stable and treat it like a public interface. If you version endpoints, do it deliberately—not via accidental behavior changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete example (either way):&lt;/strong&gt; Your API should have typed contracts for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Authenticated user identity&lt;/li&gt;
&lt;li&gt;Pagination/filtering semantics&lt;/li&gt;
&lt;li&gt;Error shapes (consistent error codes/messages)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That one decision reduces front-end guesswork and backend churn.&lt;/p&gt;
&lt;h2 id="postgresql--pgvector--redis-data-you-can-trust-cache-you-can-justify"&gt;PostgreSQL + pgvector + Redis: Data You Can Trust, Cache You Can Justify&lt;/h2&gt;
&lt;p&gt;When people talk about “data stacks,” they often list five systems and call it “modern.” I prefer a smaller set of systems you actually understand under pressure.&lt;/p&gt;
&lt;h3 id="postgresql-as-the-source-of-truth"&gt;PostgreSQL as the source of truth&lt;/h3&gt;
&lt;p&gt;Start with &lt;strong&gt;PostgreSQL&lt;/strong&gt;. It’s the backbone that survives scale, migrations, and reorganizations. Then add &lt;strong&gt;pgvector&lt;/strong&gt; when you need vector search features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I’d structure it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Postgres for all canonical entities&lt;/li&gt;
&lt;li&gt;Store vectors in Postgres via pgvector&lt;/li&gt;
&lt;li&gt;Build retrieval logic so that vector search is an internal detail, not something your app code is tangled with&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; A support assistant might retrieve relevant articles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Store article embeddings in a &lt;code&gt;documents&lt;/code&gt; table with a &lt;code&gt;vector&lt;/code&gt; column&lt;/li&gt;
&lt;li&gt;On query, compute the embedding and run similarity search in Postgres&lt;/li&gt;
&lt;li&gt;Retrieve top N matches, then apply application-level ranking/filters&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You get one system that supports both relational data and vector similarity, reducing operational sprawl.&lt;/p&gt;
&lt;h3 id="redis-for-caching-not-for-persistence"&gt;Redis for caching (not for persistence)&lt;/h3&gt;
&lt;p&gt;Use &lt;strong&gt;Redis&lt;/strong&gt; to speed things up—caches, rate limiting, ephemeral session data where appropriate, and background job coordination if it fits your design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; If losing the cache would damage correctness, don’t store correctness-critical state in Redis. Store it in Postgres and let Redis be the acceleration layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Namespaces and TTLs matter. Add prefixes like &lt;code&gt;cache:user:&lt;/code&gt; and set sensible expirations so stale data doesn’t haunt you.&lt;/p&gt;
&lt;h2 id="docker-for-deployment--ai-agents-in-cicd-reliable-shipping-automated-assistance"&gt;Docker for Deployment + AI Agents in CI/CD: Reliable Shipping, Automated Assistance&lt;/h2&gt;
&lt;p&gt;Here’s where modern stacks either become disciplined—or become a science project.&lt;/p&gt;
&lt;h3 id="docker-for-deployment"&gt;Docker for deployment&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt; keeps environments consistent and makes onboarding cheaper. Even if you later move to a more specialized platform, Docker-first habits (clear build steps, reproducible images) pay off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build optimized images (multi-stage builds)&lt;/li&gt;
&lt;li&gt;Run as non-root&lt;/li&gt;
&lt;li&gt;Keep secrets out of images (use environment variables or your secret manager)&lt;/li&gt;
&lt;li&gt;Add health checks and timeouts so your system fails in a controlled way&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ai-agents-in-cicd-use-them-like-junior-devs"&gt;AI agents in CI/CD (use them like junior devs)&lt;/h3&gt;
&lt;p&gt;I’m supportive of &lt;strong&gt;AI agents&lt;/strong&gt; in the pipeline, but only with guardrails. The best use is automation around repetitive tasks: linting improvements, test generation for edge cases, changelog drafting, migration plan review, and code review assistance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I’d deploy them safely:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limit scope: “Suggest” and “annotate,” not “rewrite everything”&lt;/li&gt;
&lt;li&gt;Require human approval for high-impact changes&lt;/li&gt;
&lt;li&gt;Make them explain their modifications and link to failing logs or test output&lt;/li&gt;
&lt;li&gt;Treat them as workflow accelerators, not sources of truth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; If a PR fails integration tests, an agent can read the logs and propose a likely fix: missing environment variable, incorrect request schema, or flaky timing. The agent reduces investigation time; it doesn’t “decide reality.”&lt;/p&gt;
&lt;h2 id="conclusion-a-stack-that-optimizes-for-shipping-not-swagger"&gt;Conclusion: A Stack That Optimizes for Shipping, Not Swagger&lt;/h2&gt;
&lt;p&gt;The 2025 stack I’d choose isn’t the flashiest lineup. It’s the one that respects your time: TypeScript for coherence, Bun for a fast feedback loop, Svelte 5 for maintainable UI, Hono or FastAPI for APIs, PostgreSQL + pgvector for trustworthy data plus retrieval, Redis for speed without correctness risk, Docker for repeatable deployment, and AI agents in CI/CD for automation with supervision.&lt;/p&gt;
&lt;p&gt;If you want a stack that survives the hype cycle, optimize for the daily loop that your team lives in—edit, build, test, deploy, repeat. Everything else is noise until it proves it helps you ship.&lt;/p&gt;</content></item><item><title>The Technologies That Survived the Hype: A Four-Year Retrospective</title><link>https://decastro.work/blog/technologies-survived-hype-four-year-retrospective/</link><pubDate>Sat, 15 Nov 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/technologies-survived-hype-four-year-retrospective/</guid><description>&lt;p&gt;Hype cycles are loud, but they’re not decisive. Over four years—2022 through 2025—what survived wasn’t what sounded impressive in keynote decks. It was what made developers’ lives easier, reduced operational pain, and scaled with real-world requirements. If you’ve ever watched a promising tool peak, fade, and leave your team stuck with migration work, this retrospective will feel uncomfortably familiar—and useful.&lt;/p&gt;
&lt;h2 id="the-survival-rule-solve-problems-that-compound"&gt;The survival rule: solve problems that compound&lt;/h2&gt;
&lt;p&gt;The technologies that endured share a blunt trait: they compound value. Every successful project teaches the team something, and that knowledge becomes reusable. That’s how adoption accelerates—documentation grows, community answers become sharper, tooling tightens, and integrations multiply.&lt;/p&gt;</description><content>&lt;p&gt;Hype cycles are loud, but they’re not decisive. Over four years—2022 through 2025—what survived wasn’t what sounded impressive in keynote decks. It was what made developers’ lives easier, reduced operational pain, and scaled with real-world requirements. If you’ve ever watched a promising tool peak, fade, and leave your team stuck with migration work, this retrospective will feel uncomfortably familiar—and useful.&lt;/p&gt;
&lt;h2 id="the-survival-rule-solve-problems-that-compound"&gt;The survival rule: solve problems that compound&lt;/h2&gt;
&lt;p&gt;The technologies that endured share a blunt trait: they compound value. Every successful project teaches the team something, and that knowledge becomes reusable. That’s how adoption accelerates—documentation grows, community answers become sharper, tooling tightens, and integrations multiply.&lt;/p&gt;
&lt;p&gt;By contrast, hype-driven tools often solve a problem that mostly exists in slides. When the novelty wears off, teams don’t have a sustained reason to keep switching. The result is a revolving door: new features, unstable ecosystems, and migration fatigue without enough operational or developer experience payoff to justify the churn.&lt;/p&gt;
&lt;p&gt;You can see this in how “winners” behave across the stack: languages and frameworks that reduce day-to-day friction, databases that perform predictably, and deployment tools that make reliability boring.&lt;/p&gt;
&lt;h2 id="typescript-and-the-death-of-clever-javascript"&gt;TypeScript and the death of “clever JavaScript”&lt;/h2&gt;
&lt;p&gt;JavaScript frameworks come and go, but TypeScript keeps converting skeptics into believers. The key isn’t that TypeScript is trendy—it’s that it makes large codebases survivable.&lt;/p&gt;
&lt;p&gt;In practical terms, TypeScript doesn’t just “add types.” It gives teams:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Safer refactors&lt;/strong&gt;: rename a function, and the compiler tells you what breaks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better navigation&lt;/strong&gt;: IDEs can infer intent, not just syntax.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More maintainable APIs&lt;/strong&gt;: teams can publish contracts instead of tribal knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why TypeScript moved from “nice-to-have” to default behavior. It meshes with how real engineering teams work: multiple services, multiple contributors, and a constant need to reduce regressions. When JavaScript gets messy, TypeScript offers a steady, incremental escape route.&lt;/p&gt;
&lt;p&gt;Here’s a concrete way to adopt it without burning time: enable &lt;code&gt;checkJs&lt;/code&gt; in the parts of your codebase with the highest churn, then ratchet strictness module-by-module. Your goal isn’t to go fully strict on day one—it’s to build confidence quickly, so the habit sticks.&lt;/p&gt;
&lt;h2 id="postgresql-the-boring-choice-that-keeps-winning"&gt;PostgreSQL: the boring choice that keeps winning&lt;/h2&gt;
&lt;p&gt;Databases don’t win on marketing. They win on reliability, performance you can reason about, and operational transparency.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s rise in this period reflects a familiar pattern: once teams hit the limits of “simple enough,” they look for a database that can grow with them. PostgreSQL delivers that growth path through robust indexing options, predictable transactions, solid query planning, and a mature ecosystem around tooling and migrations.&lt;/p&gt;
&lt;p&gt;The important part is that PostgreSQL rewards competence. If you write queries thoughtfully and design schemas carefully, you get stable performance and fewer surprises. That creates a loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Better defaults reduce incidents.&lt;/li&gt;
&lt;li&gt;Fewer incidents reduce rework.&lt;/li&gt;
&lt;li&gt;Rework reduction makes teams more willing to invest in deeper optimization.&lt;/li&gt;
&lt;li&gt;Investment leads to stronger institutional knowledge.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;MySQL didn’t “lose” so much as PostgreSQL pulled ahead in scenarios where teams needed more flexibility and fewer compromises. If you’re choosing today, treat this as a process: match the database to your workload and operational tolerance, not your preference for one vendor’s branding.&lt;/p&gt;
&lt;h2 id="docker-17-points-packaging-reality-into-something-teams-can-trust"&gt;Docker (+17 points): packaging reality into something teams can trust&lt;/h2&gt;
&lt;p&gt;Some tools survive hype because they turn invisible complexity into a repeatable artifact. Docker is a masterclass in this: it packaged “works on my machine” into something testable and deployable.&lt;/p&gt;
&lt;p&gt;The real win wasn’t containers as a concept—it was &lt;em&gt;containers as a workflow&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consistent runtime environments across dev, CI, and production&lt;/li&gt;
&lt;li&gt;faster onboarding (“here’s how to run the service”)&lt;/li&gt;
&lt;li&gt;safer experimentation (spin up a new version without wrecking the baseline)&lt;/li&gt;
&lt;li&gt;pragmatic isolation for dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Docker has a credibility advantage, it’s because it’s not asking teams to believe in an ideology. It’s asking them to stop arguing about environment drift. That’s why adoption tends to stay sticky once teams integrate it into CI pipelines and release processes.&lt;/p&gt;
&lt;p&gt;A practical tip: treat your Dockerfile like part of your production surface area. Use multi-stage builds, keep images small enough to scan and reason about, and pin versions where it matters. The hype ends; the build discipline remains.&lt;/p&gt;
&lt;h2 id="rust-and-fastapi-when-ergonomics-and-performance-meet"&gt;Rust and FastAPI: when ergonomics and performance meet&lt;/h2&gt;
&lt;p&gt;Rust and FastAPI are often discussed separately, but they rhyme: both provide immediate developer value while enabling serious performance and correctness goals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rust&lt;/strong&gt; survived because it takes safety seriously without pretending trade-offs don’t exist. Teams that adopt Rust typically do it for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stronger guarantees that reduce production failures&lt;/li&gt;
&lt;li&gt;performance that doesn’t require contortions&lt;/li&gt;
&lt;li&gt;a tooling ecosystem (cargo, formatting, linting) that encourages consistency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Admittedly, Rust can feel demanding at first. That’s why “most admired four years running” matters: admiration follows adoption that sticks. People don’t love Rust merely because it’s fashionable—they love it because, once the team clears the learning curve, the toolchain makes quality easier.&lt;/p&gt;
&lt;p&gt;FastAPI, meanwhile, is a case study in aligning a framework with how developers actually build APIs: concise code, strong validation, and automatic documentation that saves hours. It also integrates smoothly into modern Python service patterns, which lowers the cost of switching frameworks mid-stream.&lt;/p&gt;
&lt;p&gt;If you’re evaluating FastAPI, a good strategy is to start with a single service boundary—one that already has a clear request/response contract. Let the framework prove itself by improving maintainability (docs, validation, typed models) instead of asking you to rewrite everything at once.&lt;/p&gt;
&lt;h2 id="the-losers-where-hype-tried-to-solve-imaginary-problems"&gt;The losers: where hype tried to solve imaginary problems&lt;/h2&gt;
&lt;p&gt;The most instructive failures aren’t subtle—they’re painfully obvious in hindsight.&lt;/p&gt;
&lt;h3 id="web3-billions-burned-nothing-shipped-at-the-level-teams-needed"&gt;Web3: billions burned, nothing shipped (at the level teams needed)&lt;/h3&gt;
&lt;p&gt;The Web3 story, at least as it played out in many engineering orgs, reads like a marketing-driven detour. Teams chased novelty without securing the practical foundations required to ship stable systems for ordinary users—reliability, governance that doesn’t collapse under edge cases, developer tooling that doesn’t punish iteration, and clear economic incentives that hold up under real usage.&lt;/p&gt;
&lt;p&gt;The lesson for developers is uncomfortable but straightforward: if you can’t explain your system’s operational model in plain engineering terms, you’re probably not building a production platform—you’re building a belief system.&lt;/p&gt;
&lt;h3 id="the-nosql-for-everything-movement-abstraction-cant-replace-design"&gt;The NoSQL-for-everything movement: abstraction can’t replace design&lt;/h3&gt;
&lt;p&gt;NoSQL didn’t fail because it’s inherently bad. It failed when it became a reflex. The “NoSQL-for-everything” mindset treated the database as a branding decision rather than a workload-specific engineering choice.&lt;/p&gt;
&lt;p&gt;The durable takeaway: databases are tools with trade-offs. If your access patterns are known and your consistency requirements are clear, relational databases can be an excellent fit. When teams insisted on schemaless flexibility for every problem, they often paid later in complexity: harder migrations, inconsistent data models, and debugging that became a team sport.&lt;/p&gt;
&lt;h3 id="javascript-frameworks-built-for-cleverness-over-experience"&gt;JavaScript frameworks built for cleverness over experience&lt;/h3&gt;
&lt;p&gt;Every cycle brings a new front-end framework that promises to “change everything.” Many of them prioritize novelty or marketing narratives over the day-to-day developer experience: predictable ergonomics, excellent error messages, stable APIs, and tooling that integrates cleanly into real projects.&lt;/p&gt;
&lt;p&gt;A good framework doesn’t just make demos impressive. It makes maintenance cheaper. If switching tools improves productivity for both new and existing developers, adoption follows. If it mainly improves press coverage, the ecosystem becomes a treadmill.&lt;/p&gt;
&lt;h2 id="what-to-do-in-2026-pick-tools-by-evidence-not-vibes"&gt;What to do in 2026: pick tools by evidence, not vibes&lt;/h2&gt;
&lt;p&gt;So how do you avoid repeating the same hype-cycle mistakes? Use a short checklist that forces signal over style:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ask what problem it removes.&lt;/strong&gt; If the answer is “faster” or “revolutionary,” demand specifics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Look for compounding assets.&lt;/strong&gt; Mature documentation, examples that resemble production, and tooling that doesn’t require heroics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate migration cost early.&lt;/strong&gt; If adoption requires a rewrite, run a pilot with a clear exit plan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure developer experience, not excitement.&lt;/strong&gt; Time-to-debug, refactor safety, clarity of error messages, and how quickly new teammates become effective.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check whether the ecosystem supports operational reality.&lt;/strong&gt; Monitoring, deployment patterns, and predictable runtime behavior.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In other words: reward technologies that help you build and run software with less friction, not ones that help you win arguments on Twitter.&lt;/p&gt;
&lt;h2 id="conclusion-the-winners-werent-magicalthey-were-useful"&gt;Conclusion: the winners weren’t magical—they were useful&lt;/h2&gt;
&lt;p&gt;Across 2022–2025, the technologies that survived hype did so because they solved real engineering problems in ways that compound over time. TypeScript made large JavaScript systems manageable. PostgreSQL offered dependable growth without constant trade-off drama. Docker turned environments into repeatable artifacts. Rust and FastAPI earned admiration by improving quality and developer speed. The losers—Web3 hype, NoSQL-for-everything impulses, and frameworks driven more by clever marketing than real ergonomics—didn’t fail because they were interesting. They failed because they didn’t reliably reduce friction in production.&lt;/p&gt;
&lt;p&gt;The pattern is the point: technologies that make real work better tend to endure. Technologies that mostly satisfy anticipation tend to evaporate.&lt;/p&gt;</content></item><item><title>Deno 2.0: The Second Act That Might Actually Work</title><link>https://decastro.work/blog/deno-2-0-second-act-might-work/</link><pubDate>Mon, 03 Nov 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/deno-2-0-second-act-might-work/</guid><description>&lt;p&gt;Deno 1.0 arrived with a bold thesis: stop trusting the web’s “it works on my machine” past, and build a safer, cleaner runtime for modern JavaScript and TypeScript. But the first act also came with friction—especially around npm compatibility and the ecosystem gravity of Node. Deno 2.0 flips the story. It doesn’t ask you to abandon npm. It meets you where you already are, then layers Deno’s security and developer experience on top. That shift is small in wording and massive in impact.&lt;/p&gt;</description><content>&lt;p&gt;Deno 1.0 arrived with a bold thesis: stop trusting the web’s “it works on my machine” past, and build a safer, cleaner runtime for modern JavaScript and TypeScript. But the first act also came with friction—especially around npm compatibility and the ecosystem gravity of Node. Deno 2.0 flips the story. It doesn’t ask you to abandon npm. It meets you where you already are, then layers Deno’s security and developer experience on top. That shift is small in wording and massive in impact.&lt;/p&gt;
&lt;h2 id="from-purity-to-pragmatism"&gt;From “purity” to pragmatism&lt;/h2&gt;
&lt;p&gt;Deno 1.0’s original proposition was understandable: eliminate global package state, make permissions explicit, and simplify TypeScript by treating it as first-class rather than an afterthought. But “simplify” turned into “retrain,” and “rethink the ecosystem” turned into “pay the switching tax.”&lt;/p&gt;
&lt;p&gt;Deno 2.0 changes the tone. The goal is no longer to win a philosophical debate about package management. The goal is to make Deno the default choice for teams building real systems—where dependencies already exist, documentation already assumes Node, and libraries aren’t going to rewrite themselves just because a runtime asks nicely.&lt;/p&gt;
&lt;p&gt;You can feel this in the design: Node compatibility isn’t treated as a concession. It’s treated as a product feature.&lt;/p&gt;
&lt;h2 id="node-compatibility-the-ecosystem-door-swings-both-ways"&gt;Node compatibility: the ecosystem door swings both ways&lt;/h2&gt;
&lt;p&gt;The easiest way to understand why this matters is to picture how Node projects actually live. They don’t start with “Hello, world.” They start with “we have a working dependency tree,” and then they build from there: authentication, ORMs, queue clients, cloud SDK wrappers, testing utilities—plus a thousand smaller choices that accumulate over years.&lt;/p&gt;
&lt;p&gt;Deno 2.0 acknowledges that reality. Instead of forcing teams to rebuild the dependency landscape from scratch (or maintain awkward compatibility layers), it leans into compatibility so existing packages can be brought along with less pain.&lt;/p&gt;
&lt;p&gt;Practical payoff: you can move a project without turning it into a migration war.&lt;/p&gt;
&lt;p&gt;For example, suppose you have a Node service that depends on an established HTTP stack and a validation library. In the Deno 1.0 world, the decision might have been “rewrite to Deno style” or “stay on Node.” In the Deno 2.0 world, you can evaluate moving portions of the system first—starting with the parts that benefit from Deno’s strengths (security and TypeScript ergonomics) while keeping the rest familiar.&lt;/p&gt;
&lt;p&gt;That’s how adoption happens in the real world: not by converting the entire enterprise in one weekend, but by proving value in small, low-risk slices.&lt;/p&gt;
&lt;h2 id="npm-support-stop-fighting-the-thing-people-already-use"&gt;npm support: stop fighting the thing people already use&lt;/h2&gt;
&lt;p&gt;If Node compatibility is the door, npm support is the hinge. npm is where most projects—and most developers’ instincts—already point. You install packages. You update them. You rely on semver behavior. You search npm for examples when you hit edge cases you can’t afford to reinvent.&lt;/p&gt;
&lt;p&gt;Deno 2.0’s npm support means you don’t have to ask your team to stop doing the one thing they already know how to do. It also removes a common adoption blocker: the fear that “Deno means extra work forever.”&lt;/p&gt;
&lt;p&gt;A practical way to test this is to run a small dependency-heavy project. Choose one that uses a mix of transitive dependencies, not just a couple of simple libraries. If your biggest hurdle becomes “convert imports,” you’ll feel that pain quickly. If your biggest hurdle becomes “fix a handful of runtime differences,” you’re already in a healthier migration posture.&lt;/p&gt;
&lt;p&gt;The important shift is emotional as much as technical: Deno 2.0 makes it easier to say “yes” without feeling like you’re signing up for perpetual conversion.&lt;/p&gt;
&lt;h2 id="first-class-typescript-without-the-build-ritual"&gt;First-class TypeScript, without the build ritual&lt;/h2&gt;
&lt;p&gt;One of Deno’s strongest early claims was TypeScript without configuration. In practice, that meant fewer moving parts: no extra toolchain required just to get type checking and module resolution working.&lt;/p&gt;
&lt;p&gt;Deno 2.0 keeps that advantage, and it becomes more compelling when paired with npm-era expectations. Teams don’t want to pick between “nice TypeScript” and “use the same packages the rest of the world uses.” They want both.&lt;/p&gt;
&lt;p&gt;Consider a typical Node service that uses TypeScript. In many teams, TypeScript integration requires a patchwork of config files, build steps, and “just remember to run the right commands.” Deno’s model often collapses those steps into a simpler workflow: write TypeScript, run it directly, and rely on a runtime that understands the language without turning it into a complicated build chore.&lt;/p&gt;
&lt;p&gt;Pragmatic advice: when adopting Deno 2.0, aim to keep your developer loop tight. Start with a small service. Ensure it compiles and runs predictably. Then expand. You’ll get the most value when developers can iterate quickly without fighting compilation or module resolution at every step.&lt;/p&gt;
&lt;h2 id="denos-security-model-still-mattersespecially-after-node-gets-easier"&gt;Deno’s security model still matters—especially after Node gets easier&lt;/h2&gt;
&lt;p&gt;The story could have ended at “Deno now supports npm,” and that would still be useful. But Deno isn’t merely a compatibility layer. Its security model is the reason many teams wanted Deno in the first place.&lt;/p&gt;
&lt;p&gt;The core idea is simple: don’t let programs access the system by default. Make permissions explicit. In Deno 2.0, Node compatibility and npm support don’t erase that—if anything, they make it more valuable. When you can reuse packages from the wider ecosystem, you also reuse their assumptions about the world. Deno’s permission model becomes the safety net that helps you keep those assumptions from turning into accidental vulnerabilities.&lt;/p&gt;
&lt;p&gt;Practical framing: treat permissions as part of your deployment contract. Decide what your service should be allowed to do—network access, file reads, environment variable access—and enforce it at runtime. That way, even if a dependency goes sideways, the blast radius is smaller.&lt;/p&gt;
&lt;p&gt;In other words, Deno 2.0 makes “safe by design” more realistic for teams using real-world dependencies.&lt;/p&gt;
&lt;h2 id="packagejson-and-the-end-of-awkward-module-expectations"&gt;Package.json and the end of awkward module expectations&lt;/h2&gt;
&lt;p&gt;Deno 1.0 leaned heavily on URL-based imports and a “bring your own dependency graph” philosophy. That model can feel elegant until you’re forced to integrate with tooling, CI pipelines, or dependency conventions that expect package.json.&lt;/p&gt;
&lt;p&gt;Deno 2.0 supports package.json, which matters more than it sounds. package.json is not just a manifest; it’s the gravitational center of the JavaScript ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scripts like &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, and &lt;code&gt;lint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Dependency pinning and lockfile workflows&lt;/li&gt;
&lt;li&gt;Shared conventions across teams and repositories&lt;/li&gt;
&lt;li&gt;Compatibility with common tooling that expects npm-style project structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: if you’re migrating, don’t try to fight the shape of the ecosystem. Embrace package.json early so your team’s workflows remain recognizable. Then layer Deno-specific workflow changes gradually—permissions, runtime flags, and deployment specifics.&lt;/p&gt;
&lt;p&gt;This is how you avoid the most common failure mode of new runtimes: winning the runtime argument while losing the team’s daily routine.&lt;/p&gt;
&lt;h2 id="the-adoption-checklist-for-teams-considering-deno-20"&gt;The adoption checklist for teams considering Deno 2.0&lt;/h2&gt;
&lt;p&gt;If you’re evaluating whether Deno 2.0 “might actually work,” treat it like a migration decision, not a faith decision. Here’s a practical checklist that keeps the process grounded:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pick a real service, not a toy.&lt;/strong&gt; Something that uses a few non-trivial dependencies, like an auth helper, a database client, or a request router.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate TypeScript workflow end-to-end.&lt;/strong&gt; Ensure type checking and developer iteration feel better—not merely different.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test npm dependency usage early.&lt;/strong&gt; Confirm that the packages you rely on behave correctly in Deno’s runtime model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Define permissions explicitly.&lt;/strong&gt; Decide what the service needs and lock it down. Treat permission failures as good signals, not annoyances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run CI in a way your team already understands.&lt;/strong&gt; If your team uses scripts and lockfiles, make sure the Deno integration matches those expectations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure complexity, not ideology.&lt;/strong&gt; If the migration ends with “we fixed some code and reduced risk,” it’s a win. If it ends with “we invented a new process for every step,” you’re not winning.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This checklist is deliberately not about whether Deno is “more pure” than Node. It’s about whether it reduces friction while improving safety and developer experience.&lt;/p&gt;
&lt;h2 id="conclusion-deno-20-is-what-happens-when-a-runtime-stops-asking-permission"&gt;Conclusion: Deno 2.0 is what happens when a runtime stops asking permission&lt;/h2&gt;
&lt;p&gt;Deno 1.0 asked developers to rethink their ecosystem choices. Deno 2.0 stops asking and starts cooperating. Node compatibility and npm support remove the biggest adoption barrier. package.json support eliminates the daily friction of fighting conventions. And a mature, first-class TypeScript experience keeps Deno’s original promise intact—without forcing teams into a perpetual rewrite.&lt;/p&gt;
&lt;p&gt;Most importantly, Deno’s security model doesn’t get watered down by compatibility. It becomes more relevant precisely because real-world dependencies are now easier to bring along.&lt;/p&gt;
&lt;p&gt;This is the second act Deno needed: not a retreat from its ideals, but an expansion of its usefulness. If you’ve wanted Deno’s ideas without the ecosystem cost, Deno 2.0 feels like the moment the price tag finally matches the value.&lt;/p&gt;</content></item><item><title>Why I Think 2025 Is the Most Exciting Year to Be a Developer Since 2007</title><link>https://decastro.work/blog/2025-most-exciting-year-developer-since-2007/</link><pubDate>Tue, 28 Oct 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/2025-most-exciting-year-developer-since-2007/</guid><description>&lt;p&gt;In 2007, a device hit the market that didn’t just ship better apps—it changed what software &lt;em&gt;was&lt;/em&gt;. In 2025, AI agents are doing the same thing, except faster, messier, and arguably more consequential. If you’ve felt a familiar itch—like you’re watching the next platform form in real time—you’re not imagining it. The parallels with the iPhone era are uncanny, and the window for builders is open right now.&lt;/p&gt;
&lt;h2 id="1-the-platform-shift-from-app-to-agent"&gt;1) The platform shift: from “app” to “agent”&lt;/h2&gt;
&lt;p&gt;The iPhone didn’t magically improve mobile apps. It created a new interaction model—touch, gestures, always-with-you distribution—and developers had to reinvent their mental model of UI, navigation, and data flows. The winners weren’t the people who “ported” their desktop app to a smaller screen. They were the people who designed around the device’s new reality.&lt;/p&gt;</description><content>&lt;p&gt;In 2007, a device hit the market that didn’t just ship better apps—it changed what software &lt;em&gt;was&lt;/em&gt;. In 2025, AI agents are doing the same thing, except faster, messier, and arguably more consequential. If you’ve felt a familiar itch—like you’re watching the next platform form in real time—you’re not imagining it. The parallels with the iPhone era are uncanny, and the window for builders is open right now.&lt;/p&gt;
&lt;h2 id="1-the-platform-shift-from-app-to-agent"&gt;1) The platform shift: from “app” to “agent”&lt;/h2&gt;
&lt;p&gt;The iPhone didn’t magically improve mobile apps. It created a new interaction model—touch, gestures, always-with-you distribution—and developers had to reinvent their mental model of UI, navigation, and data flows. The winners weren’t the people who “ported” their desktop app to a smaller screen. They were the people who designed around the device’s new reality.&lt;/p&gt;
&lt;p&gt;Agent systems are forcing the same pivot. Instead of thinking in terms of “screens and buttons,” you’re building systems that can interpret goals, take actions across tools, and recover from failure. In practical terms, an “agent” application looks less like a traditional workflow app and more like a living collaboration between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an LLM (reasoning + language),&lt;/li&gt;
&lt;li&gt;tools (APIs, code execution, browsers, databases, task runners),&lt;/li&gt;
&lt;li&gt;memory (what the system knows about the user and the task),&lt;/li&gt;
&lt;li&gt;policies (what the agent is allowed to do),&lt;/li&gt;
&lt;li&gt;and orchestration (how the system decides, calls tools, and verifies outputs).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a concrete example: in a customer support product, a conventional app might provide a ticket UI and search. An agent-first approach can handle the full loop: interpret the user’s message, locate relevant documentation, draft a response, check policy constraints, and either send it or request approval. The “feature” isn’t a screen—it’s the end-to-end capability.&lt;/p&gt;
&lt;p&gt;That’s the platform shift. You’re not just adding intelligence to an existing product; you’re redesigning the product around a new unit of value: autonomy with guardrails.&lt;/p&gt;
&lt;h2 id="2-interaction-paradigms-are-changing-again"&gt;2) Interaction paradigms are changing (again)&lt;/h2&gt;
&lt;p&gt;In the iPhone era, “mobile-first” wasn’t a slogan—it meant you designed for thumbs, latency, attention, and constrained input. You couldn’t assume a keyboard, hover states, or multi-tab workflows. You rethought interaction.&lt;/p&gt;
&lt;p&gt;In 2025, “agent-first” is still settling, but the direction is clear: natural language as the primary interface, and tool-driven execution as the new output. That creates new design constraints you can’t ignore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Uncertainty is part of the experience.&lt;/strong&gt; Agents will be wrong sometimes. Your product must make failure modes legible and recoverable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action beats narration.&lt;/strong&gt; Users don’t just want explanations; they want tasks completed—booked, updated, summarized, created.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification becomes UX.&lt;/strong&gt; “Trust me” doesn’t scale. Interfaces will increasingly show what the agent did, why it decided, and what it verified.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A helpful way to frame your work: don’t start with “What should the agent say?” Start with “What should the agent &lt;em&gt;do&lt;/em&gt; safely?” Then design the prompts, tools, and UI to support that contract.&lt;/p&gt;
&lt;p&gt;One practical pattern: &lt;strong&gt;two-step confirmation&lt;/strong&gt; for high-impact actions. For example, an agent that manages invoices can draft a change request first (“I’m proposing to update vendor terms; I will notify accounting and attach this PDF.”). Then require explicit confirmation before committing changes. Users get speed without losing control.&lt;/p&gt;
&lt;h2 id="3-distribution-channels-will-reward-different-builders"&gt;3) Distribution channels will reward different builders&lt;/h2&gt;
&lt;p&gt;Mobile changed distribution overnight: the app store, push notifications, background execution constraints, and viral sharing mechanics shaped what “good” looked like. Developers who understood those channels—how users discover, install, and retain—outperformed those who treated distribution as an afterthought.&lt;/p&gt;
&lt;p&gt;Agent systems are likely to spread through different rails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;embedded inside existing products&lt;/strong&gt; (CRMs, IDEs, email, spreadsheets),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;through chat or workflow front-ends&lt;/strong&gt; people already use,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;via integrations&lt;/strong&gt; that turn “actions” into reusable capabilities,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;and via marketplaces for agent tools&lt;/strong&gt; (the equivalent of app marketplaces, but centered on functions and connectors).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve built a useful integration before, you already get the dynamic: the value accrues to the ecosystem pieces that are easiest to plug into. For agent-first products, this means you should prioritize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clean tool interfaces (inputs/outputs you can trust),&lt;/li&gt;
&lt;li&gt;predictable side effects (what happens when the agent calls your API),&lt;/li&gt;
&lt;li&gt;and strong observability (logs, traces, and human-readable audit trails).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ask yourself: could your capability be dropped into someone else’s agent runtime tomorrow? If yes, you’re building in the direction of platform gravity.&lt;/p&gt;
&lt;h2 id="4-new-business-models-autonomy-needs-pricing-and-accountability"&gt;4) New business models: autonomy needs pricing and accountability&lt;/h2&gt;
&lt;p&gt;The iPhone era didn’t just create new technology; it created new monetization models—paid apps, freemium, subscriptions, app bundles, and ad ecosystems built around mobile usage patterns.&lt;/p&gt;
&lt;p&gt;Agent-first systems complicate monetization because the “unit of value” shifts from UI features to outcomes. Users pay for results, not for screens. But autonomy also introduces variable cost: tool calls, model inference, retries, and—most importantly—human review when things go wrong.&lt;/p&gt;
&lt;p&gt;In practice, you’ll probably end up with pricing that reflects one of three realities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Seat-based&lt;/strong&gt; (for teams using an agent in a product like a developer tool or internal ops assistant),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Usage-based&lt;/strong&gt; (per task, per action, per successful resolution),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outcome-based&lt;/strong&gt; (rare early, but compelling once reliability improves: “bookings made,” “incidents resolved,” “tickets closed”).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My opinion: the best early agent products won’t pretend they can price perfectly. They’ll align cost to user value and make trade-offs explicit. For example, you can offer “fast draft” vs “verified and approved” modes. The cheaper mode is useful and quick; the verified mode costs more but reduces risk. That’s how you monetize autonomy while respecting the fact that agents are still learning.&lt;/p&gt;
&lt;h2 id="5-the-hard-part-isnt-the-modelits-the-system"&gt;5) The hard part isn’t “the model”—it’s the system&lt;/h2&gt;
&lt;p&gt;In 2008, plenty of people could build a basic mobile app. The generational outcomes came from those who solved the unsexy problems: performance, battery usage, flaky network behavior, state management, and platform conventions.&lt;/p&gt;
&lt;p&gt;In 2025, the shiny part is easy to demo: prompt an agent, get a useful response, show off a tool call. The generational outcomes will come from the systems work that makes agents dependable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool reliability:&lt;/strong&gt; timeouts, idempotency, retries, and consistent schemas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State and memory:&lt;/strong&gt; what the agent remembers, what it forgets, and how that affects correctness.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy enforcement:&lt;/strong&gt; role-based permissions, allowed operations, and safe browsing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluation and regression testing:&lt;/strong&gt; you need measurable behaviors, not vibes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability:&lt;/strong&gt; traces that answer “why did it do that?” in minutes, not hours.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete build plan for a team (or a solo builder) that wants to stand out fast:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick one workflow where mistakes are costly but recoverable (e.g., drafting internal docs, summarizing meeting actions, generating code changes with review).&lt;/li&gt;
&lt;li&gt;Implement the agent as a pipeline: plan → act via tools → verify → (optionally) ask a human.&lt;/li&gt;
&lt;li&gt;Log everything: tool inputs/outputs, intermediate decisions, and verification outcomes.&lt;/li&gt;
&lt;li&gt;Create a small eval suite: 50–200 representative tasks with expected behaviors.&lt;/li&gt;
&lt;li&gt;Ship a “conservative” mode first, then expand autonomy as reliability improves.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The developers who do this will build products that people trust. Trust is the platform.&lt;/p&gt;
&lt;h2 id="6-why-the-timing-feels-like-2007-all-over-again"&gt;6) Why the timing feels like 2007 all over again&lt;/h2&gt;
&lt;p&gt;Here’s what made mobile in the late 2000s special: the rules weren’t fully standardized yet. Nobody agreed on best practices. APIs evolved quickly. UI patterns were still forming. But the market was moving, and the people who experimented early learned faster than the people who waited for consensus.&lt;/p&gt;
&lt;p&gt;Agent-first development is in that same chaotic but fertile stage. The interaction model isn’t settled, the tooling ecosystem is still consolidating, and best practices are emerging through hard experience rather than textbooks. That’s exactly when developer leverage is highest.&lt;/p&gt;
&lt;p&gt;If you want a “generational outcome” mindset, borrow the mobile-era lesson: don’t just chase the novelty. Chase the fundamentals people will still need after the hype cools down:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;designing for reliability,&lt;/li&gt;
&lt;li&gt;building clean integrations,&lt;/li&gt;
&lt;li&gt;instrumenting the system end-to-end,&lt;/li&gt;
&lt;li&gt;and making automation safe enough to scale.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="7-a-personal-rule-build-something-that-will-still-be-valuable-when-agents-get-better"&gt;7) A personal rule: build something that will still be valuable when agents get better&lt;/h2&gt;
&lt;p&gt;The iPhone platform didn’t just reward people who built apps—it rewarded those who created experiences aligned with the new interaction reality. The equivalent test for agents is simple: if your agent got 10x smarter next year, would your product still matter?&lt;/p&gt;
&lt;p&gt;If your answer is “no,” you’re probably building around a temporary capability. If your answer is “yes,” you’re building around a workflow, a trust model, or an integration surface that will remain essential.&lt;/p&gt;
&lt;p&gt;Start small and concrete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build an agent that does one job end-to-end with verification.&lt;/li&gt;
&lt;li&gt;Make the tool interfaces clean enough to reuse.&lt;/li&gt;
&lt;li&gt;Provide an audit trail users can understand.&lt;/li&gt;
&lt;li&gt;Add a human-in-the-loop escape hatch.&lt;/li&gt;
&lt;li&gt;Focus on outcomes, not demos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The window is open because the platform is forming. That doesn’t guarantee success—but it does guarantee learning velocity. And learning velocity is how you earn leverage.&lt;/p&gt;
&lt;h2 id="conclusion-the-iphone-lesson-applied-to-2025"&gt;Conclusion: The iPhone lesson, applied to 2025&lt;/h2&gt;
&lt;p&gt;We’ve seen this movie before. A new platform arrives, interaction changes, distribution shifts, business models evolve, and the surface area of unsolved problems explodes. In 2007, developers who built for mobile early shaped entire careers. In 2025, developers who build for agents early will do the same—because they’ll understand what “agent-first” really means: autonomy with accountability.&lt;/p&gt;
&lt;p&gt;So don’t wait for the perfect definition. Find a real workflow, build the reliable loop, measure what matters, and ship. 2025 isn’t just an exciting year to be a developer—it’s a year to become one in a new way.&lt;/p&gt;</content></item><item><title>Zero-Knowledge Proofs Are the Sleeper Technology of the Decade</title><link>https://decastro.work/blog/zero-knowledge-proofs-sleeper-technology-decade/</link><pubDate>Wed, 22 Oct 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/zero-knowledge-proofs-sleeper-technology-decade/</guid><description>&lt;p&gt;For years, zero-knowledge proofs (ZKPs) lived in the shadowy corner of crypto research—fascinating, clever, and mostly optional. That’s changing. ZKPs have quietly matured into a general-purpose privacy technology: you can prove a claim without exposing the data behind it, and you can even prove that a computation was performed correctly without redoing the computation itself. If blockchain was the headline, authentication and verification are the next act—and they’ll matter more to everyday users than another token launch.&lt;/p&gt;</description><content>&lt;p&gt;For years, zero-knowledge proofs (ZKPs) lived in the shadowy corner of crypto research—fascinating, clever, and mostly optional. That’s changing. ZKPs have quietly matured into a general-purpose privacy technology: you can prove a claim without exposing the data behind it, and you can even prove that a computation was performed correctly without redoing the computation itself. If blockchain was the headline, authentication and verification are the next act—and they’ll matter more to everyday users than another token launch.&lt;/p&gt;
&lt;p&gt;This is why ZKPs are the sleeper technology of the decade: they solve a problem everyone has (privacy and trust), in a way that’s becoming practical (libraries, tooling, and composable designs). Let’s talk about what ZKPs actually do, where they’re landing now, and how teams can prepare.&lt;/p&gt;
&lt;h2 id="the-core-idea-prove-the-statement-not-the-secret"&gt;The core idea: prove the statement, not the secret&lt;/h2&gt;
&lt;p&gt;A zero-knowledge proof lets you convince a verifier that a statement is true without revealing &lt;em&gt;why&lt;/em&gt; in a way that leaks sensitive information. Think of it like showing you passed an exam without showing your entire work—or showing you’re eligible for a service without disclosing your birthdate, income, or identity documents.&lt;/p&gt;
&lt;p&gt;A classic framing is “prove you’re over 21”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You want to hide&lt;/strong&gt; the birthdate (and everything else you typed into the form).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want to prove&lt;/strong&gt; eligibility (“I’m at least 21 now”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A verifier should be convinced&lt;/strong&gt; without learning the exact date.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In ZK terms, your client generates a proof that it satisfies a constraint like: &lt;code&gt;now - birthdate &amp;gt;= 21 years&lt;/code&gt;. The verifier checks the proof using only public inputs (like “the cutoff date” or “current timestamp rules”) and never learns the birthdate itself.&lt;/p&gt;
&lt;p&gt;This is the key shift: privacy isn’t an afterthought or a masking trick; it’s encoded into the verification mechanism. That’s a fundamentally different design pattern from “encrypt the data and hope the system works.”&lt;/p&gt;
&lt;h2 id="zk-identity-privacy-preserving-authentication-that-actually-scales"&gt;ZK identity: privacy-preserving authentication that actually scales&lt;/h2&gt;
&lt;p&gt;Identity is where ZKPs become immediately compelling. Today, many onboarding flows are privacy-hostile by default: you submit documents, systems store them, and “trust” often becomes “we have your data.” Even if you use encryption and access controls, the data still exists—and breaches still happen.&lt;/p&gt;
&lt;p&gt;ZK-based identity can change the shape of the interaction. Instead of handing over raw attributes, a user proves properties about themselves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Age gates&lt;/strong&gt;: “Over 21” or “Under 18,” without revealing birthdate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Residency&lt;/strong&gt;: “Resident of jurisdiction X,” without exposing full address.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eligibility&lt;/strong&gt;: “Member of organization Y” or “Passed compliance checks,” without disclosing the underlying records.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account access&lt;/strong&gt;: “This device belongs to an enrolled user,” without exposing identifiers beyond what’s needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a practical example for a consumer product:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user’s wallet (or identity app) holds credentials from a trusted issuer.&lt;/li&gt;
&lt;li&gt;When the user needs access to age-restricted content, the app creates a ZK proof: &lt;em&gt;“I satisfy the predicate for age eligibility.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;The content provider verifies the proof and grants access—without ever receiving the birthdate or document scans.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From a system design perspective, the win is twofold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimized data exposure&lt;/strong&gt;: fewer sensitive attributes cross trust boundaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible compliance&lt;/strong&gt;: you can design proofs around specific policies (“only reveal what’s necessary”) instead of collecting everything “just in case.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The operational reality is that teams should treat ZK identity as a &lt;em&gt;protocol&lt;/em&gt; problem, not a “drop-in library” problem. You’ll need a clear issuer/verifier model, well-defined public parameters, and a threat model for things like replay attacks, revocation, and jurisdiction changes. ZKPs don’t remove engineering—they concentrate it in the right places.&lt;/p&gt;
&lt;h2 id="verifying-computation-integrity-proofs-without-re-running-the-work"&gt;Verifying computation integrity: proofs without re-running the work&lt;/h2&gt;
&lt;p&gt;The second big unlock is computation integrity. Many systems need to ensure that some action was computed correctly, but re-running the computation can be expensive, slow, or impossible to do trustlessly.&lt;/p&gt;
&lt;p&gt;ZK proofs address that. The prover performs the computation once (locally or in a trusted environment) and then generates a proof that the computation followed the rules. The verifier checks the proof quickly, without re-executing the computation.&lt;/p&gt;
&lt;p&gt;This shows up in two common patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Third-party computation&lt;/strong&gt;: If you outsource data processing or inference, ZK proofs can let you verify correctness without redoing the entire workflow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-chain or constrained verification&lt;/strong&gt;: If verification must happen in a limited environment (like smart contracts or secure hardware), ZKPs provide a compact way to validate results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple mental model: today, “verification” often means re-execution or trusting a result. With ZK, verification becomes “check a certificate.” Your systems shift from “do the work twice” to “do the work once, then verify efficiently.”&lt;/p&gt;
&lt;p&gt;The more you care about correctness under privacy constraints, the more ZKPs start to look like the missing primitive. You get integrity with less data movement—a combination that’s hard to replicate with conventional cryptography alone.&lt;/p&gt;
&lt;h2 id="zkvm-and-arbitrary-program-execution-from-toy-statements-to-real-software"&gt;zkVM and arbitrary program execution: from toy statements to real software&lt;/h2&gt;
&lt;p&gt;In early ZK systems, proving was often limited to hand-crafted circuits: you translate logic into an arithmetic form that the ZK system can prove efficiently. That translation can be tedious, and it limits what you can prove quickly.&lt;/p&gt;
&lt;p&gt;That’s why zkVM-style approaches matter. A &lt;strong&gt;zkVM&lt;/strong&gt; is a virtual machine designed so that you can generate a ZK proof for running an arbitrary program, not just a small set of specialized computations. In other words: write code, compile it, prove that it ran correctly.&lt;/p&gt;
&lt;p&gt;One prominent example is &lt;strong&gt;RISC Zero’s zkVM&lt;/strong&gt;, which targets the idea of proving program execution for a RISC-V instruction set. Practically, this means you can aim for a workflow closer to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write or compile the program you want to verify.&lt;/li&gt;
&lt;li&gt;Execute it to produce outputs.&lt;/li&gt;
&lt;li&gt;Generate a zero-knowledge proof that the program executed correctly over the inputs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t magically eliminate complexity, but it reduces the “circuit translation tax” that previously made many ZK applications feel like research projects. It also makes ZK more composable with the way software teams already work: compile, test, prove, verify.&lt;/p&gt;
&lt;p&gt;A good way to think about zkVMs is as the bridge between cryptographic theory and developer ergonomics. They turn ZK from “prove this math identity” into “prove this software behavior.”&lt;/p&gt;
&lt;h2 id="what-it-means-for-authentication-compliance-and-privacy"&gt;What it means for authentication, compliance, and privacy&lt;/h2&gt;
&lt;p&gt;ZKPs aren’t just a technical curiosity—they’re a reshaping force for how verification works across regulated and privacy-sensitive domains.&lt;/p&gt;
&lt;h3 id="authentication-fewer-secrets-fewer-leaks"&gt;Authentication: fewer secrets, fewer leaks&lt;/h3&gt;
&lt;p&gt;Instead of collecting sensitive attributes and storing them, systems can verify predicates. Users get control, providers get assurance, and attackers get less.&lt;/p&gt;
&lt;p&gt;For teams implementing ZK authentication, a practical rule is to start with &lt;strong&gt;one predicate&lt;/strong&gt; that maps cleanly to policy. Don’t begin with “full identity.” Begin with “over 21,” “passed KYC,” “owns required credential,” or “has authorization granted by issuer X.”&lt;/p&gt;
&lt;h3 id="compliance-prove-policy-adherence-not-document-contents"&gt;Compliance: prove policy adherence, not document contents&lt;/h3&gt;
&lt;p&gt;Compliance often demands proof that you followed rules. ZKPs let you prove compliance conditions without exposing the raw documents—especially valuable when you must show eligibility while minimizing sensitive disclosures.&lt;/p&gt;
&lt;h3 id="data-privacy-reduce-the-blast-radius"&gt;Data privacy: reduce the blast radius&lt;/h3&gt;
&lt;p&gt;Even if your encryption story is strong, breaches are still possible. ZK helps by design: if the server never receives the birthdate, an attacker can’t steal it from that server.&lt;/p&gt;
&lt;p&gt;This is the part most teams underestimate. ZK is not just about “privacy-friendly storage.” It’s about &lt;strong&gt;privacy-preserving verification&lt;/strong&gt;, meaning less sensitive data crosses boundaries in the first place.&lt;/p&gt;
&lt;h2 id="how-to-adopt-zkps-without-getting-lost-in-the-math"&gt;How to adopt ZKPs without getting lost in the math&lt;/h2&gt;
&lt;p&gt;Let’s be blunt: ZKPs can be mathematically dense, and it’s tempting to either overhype them or wait until they become effortless. The reality is that you can adopt them now with a pragmatic approach.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick a narrow use case with clear predicates.&lt;/strong&gt;&lt;br&gt;
Age checks, eligibility flags, credential possession, or “computation result validity” are better than “prove everything about me.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Design the verification interface first.&lt;/strong&gt;&lt;br&gt;
Decide what inputs are public, what outputs must be verified, and what the verifier actually needs to trust. A good ZK system fails less when the boundary is crisp.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lean on existing tooling and proven libraries.&lt;/strong&gt;&lt;br&gt;
You don’t want to implement cryptography from scratch. Use established libraries, reference zkVM workflows, and existing proof systems tailored for your environment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat performance as a product constraint.&lt;/strong&gt;&lt;br&gt;
ZK proofs have costs—proof generation time, verification overhead, and infrastructure considerations. Benchmark early using your target hardware and realistic statement sizes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Plan for revocation and policy updates.&lt;/strong&gt;&lt;br&gt;
If credentials expire, jurisdictions change, or rules evolve, you’ll need a strategy. Many ZK designs assume relatively stable public parameters; you should plan what happens when they’re not.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do these things, you’ll avoid the two common failure modes: building a “cool proof” that doesn’t integrate, or building a system that integrates but can’t meet performance and operations requirements.&lt;/p&gt;
&lt;h2 id="conclusion-the-privacy-and-trust-layer-hiding-in-plain-sight"&gt;Conclusion: the privacy-and-trust layer hiding in plain sight&lt;/h2&gt;
&lt;p&gt;Zero-knowledge proofs are the sleeper technology because they don’t chase adoption with flash—they quietly improve the foundations of verification. Identity becomes attribute-based without data hoarding. Compliance becomes proof of policy adherence without document sprawl. Computation integrity becomes a certificate-check instead of re-execution.&lt;/p&gt;
&lt;p&gt;The next decade won’t be defined by ZKPs replacing everything. It’ll be defined by ZKPs becoming the default answer to a recurring question: &lt;em&gt;How do we prove trust without exposing the sensitive parts?&lt;/em&gt; As zkVM approaches and libraries mature, the transition from research-grade demos to production-grade systems accelerates. The teams that start planning now—around predicates, interfaces, and operational realities—will ship the next generation of authentication and verification while everyone else is still debating what “privacy” should mean.&lt;/p&gt;</content></item><item><title>htmx 2.0 and the Maturation of the Hypermedia Renaissance</title><link>https://decastro.work/blog/htmx-2-0-hypermedia-renaissance-maturation/</link><pubDate>Fri, 10 Oct 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/htmx-2-0-hypermedia-renaissance-maturation/</guid><description>&lt;p&gt;For years, the “just use HTML” crowd was easy to dismiss as nostalgia. Then something happened: interactive apps got good enough that you stopped needing a JavaScript build pipeline for every UI. htmx didn’t just revive hypermedia—it made it practical, then repeatable. With htmx 2.0, the hypermedia renaissance isn’t a clever side project anymore; it’s becoming a legitimate architectural default for internal tools and framework-driven teams.&lt;/p&gt;
&lt;h2 id="from-rebellion-to-architecture-why-hypermedia-finally-stuck"&gt;From rebellion to architecture: why “hypermedia” finally stuck&lt;/h2&gt;
&lt;p&gt;The early hypermedia argument was philosophical: HTTP is already a messaging layer, and HTML is already a UI language. Server-rendered pages aren’t inherently static; they’re just missing the wiring for partial updates and event-driven interactions.&lt;/p&gt;</description><content>&lt;p&gt;For years, the “just use HTML” crowd was easy to dismiss as nostalgia. Then something happened: interactive apps got good enough that you stopped needing a JavaScript build pipeline for every UI. htmx didn’t just revive hypermedia—it made it practical, then repeatable. With htmx 2.0, the hypermedia renaissance isn’t a clever side project anymore; it’s becoming a legitimate architectural default for internal tools and framework-driven teams.&lt;/p&gt;
&lt;h2 id="from-rebellion-to-architecture-why-hypermedia-finally-stuck"&gt;From rebellion to architecture: why “hypermedia” finally stuck&lt;/h2&gt;
&lt;p&gt;The early hypermedia argument was philosophical: HTTP is already a messaging layer, and HTML is already a UI language. Server-rendered pages aren’t inherently static; they’re just missing the wiring for partial updates and event-driven interactions.&lt;/p&gt;
&lt;p&gt;What changed is that teams stopped asking, “Can we avoid JavaScript?” and started asking, “Can we ship faster with fewer moving parts?” That shift is why htmx found a home in everyday product engineering.&lt;/p&gt;
&lt;p&gt;In practice, “hypermedia” with htmx means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You keep your UI as HTML (and server templates).&lt;/li&gt;
&lt;li&gt;You enhance it with small attributes that trigger requests and swap fragments.&lt;/li&gt;
&lt;li&gt;You let the server remain the source of truth for UI state and validation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is especially attractive for internal applications—dashboards, admin panels, CRUD-heavy workflows—where the UI often looks complex but behaves like a set of forms, tables, filters, and modal interactions. Those interfaces don’t require a full SPA framework to be excellent. They require responsiveness, correctness, and maintainability.&lt;/p&gt;
&lt;h2 id="what-htmx-20-improves-smaller-surface-area-smoother-extension-steadier-streaming"&gt;What htmx 2.0 improves: smaller surface area, smoother extension, steadier streaming&lt;/h2&gt;
&lt;p&gt;htmx 2.0 brings meaningful “day-to-day” improvements: a smaller bundle, a better extension system, and improved support for Server-Sent Events (SSE). Even if you don’t care about internals, these are the kinds of enhancements that reduce friction when you’re building a real product instead of a demo.&lt;/p&gt;
&lt;h3 id="1-smaller-bundle-less-penalty-for-just-use-htmx"&gt;1) Smaller bundle, less penalty for “just use htmx”&lt;/h3&gt;
&lt;p&gt;When a library is lightweight, it’s easier to justify adoption across a large codebase. You don’t hesitate to include it on every page, and you’re less likely to end up with inconsistent patterns where only certain sections of the app use enhanced behaviors.&lt;/p&gt;
&lt;p&gt;A practical effect: you can progressively enhance. Start with a single endpoint powering an “inline edit” or “filter results” view, then expand without turning the rest of your templates into a legacy-free zone.&lt;/p&gt;
&lt;h3 id="2-a-better-extension-system-means-fewer-one-off-hacks"&gt;2) A better extension system means fewer one-off hacks&lt;/h3&gt;
&lt;p&gt;Every team has a moment where “we’ll just add one helper” becomes “we now maintain three parallel mini-frameworks for behavior.” A better extension system reduces that drift.&lt;/p&gt;
&lt;p&gt;Think about how extensions help with consistency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standardize how you handle error rendering across requests.&lt;/li&gt;
&lt;li&gt;Build reusable abstractions for common UI patterns (confirmation dialogs, optimistic spinners, accessibility-friendly focus management).&lt;/li&gt;
&lt;li&gt;Encapsulate analytics hooks so behavior isn’t scattered across templates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-improved-sse-support-makes-streaming-uis-feel-native"&gt;3) Improved SSE support makes streaming UIs feel native&lt;/h3&gt;
&lt;p&gt;SSE is a natural fit for internal tools: live activity feeds, long-running job updates, background processing status, and “processing…” progress bars that don’t require WebSocket complexity.&lt;/p&gt;
&lt;p&gt;With better SSE support, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stream status updates into a single DOM region.&lt;/li&gt;
&lt;li&gt;Keep the rest of the page stable.&lt;/li&gt;
&lt;li&gt;Avoid a “replace the world” approach typical of SPAs when the only changing part is a small component.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your users are waiting for the back office to finish processing an import, SSE-driven updates make the app feel responsive without building a realtime platform.&lt;/p&gt;
&lt;h2 id="how-enterprise-teams-actually-adopt-htmx-start-with-workflows-not-ideology"&gt;How enterprise teams actually adopt htmx: start with workflows, not ideology&lt;/h2&gt;
&lt;p&gt;The adoption pattern that keeps repeating is simple: teams pick one class of workflows where server-rendered HTML already makes sense, then add interactivity where it matters. That’s where htmx shines—because it doesn’t ask you to rewrite your app’s mental model.&lt;/p&gt;
&lt;p&gt;Here’s a concrete example that shows the usual progression:&lt;/p&gt;
&lt;h3 id="example-an-admin-user-management-screen"&gt;Example: an admin “user management” screen&lt;/h3&gt;
&lt;p&gt;Start with the classic server-rendered page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A table of users&lt;/li&gt;
&lt;li&gt;A search filter&lt;/li&gt;
&lt;li&gt;A “disable account” action&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, add just enough interactivity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Submitting the search filter updates the table via a partial request.&lt;/li&gt;
&lt;li&gt;Clicking “disable account” opens a confirm prompt that triggers a server action.&lt;/li&gt;
&lt;li&gt;The table row updates in-place after the action completes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You end up with an interface that feels modern, but the architecture remains stable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server renders the fragments.&lt;/li&gt;
&lt;li&gt;The client swaps them into place.&lt;/li&gt;
&lt;li&gt;Validation, authorization, and business logic stay on the server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how hypermedia becomes “legitimate.” It isn’t a replacement for application logic—it’s a smarter way to deliver UI logic without dragging in a build system for everything.&lt;/p&gt;
&lt;h3 id="the-key-advantage-fewer-cross-layer-bugs"&gt;The key advantage: fewer cross-layer bugs&lt;/h3&gt;
&lt;p&gt;When UI state and business state are separated (common in SPA architectures), you inevitably get mismatches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server rejects an update; the UI assumes success.&lt;/li&gt;
&lt;li&gt;A race condition changes data; the UI is stale until a manual refresh.&lt;/li&gt;
&lt;li&gt;Validation logic exists in two places and diverges.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With htmx, the server remains authoritative. The UI reflects the server’s decisions because the server returns the view (or the fragment) that the client applies.&lt;/p&gt;
&lt;p&gt;That’s not just developer ergonomics—it’s fewer production surprises.&lt;/p&gt;
&lt;h2 id="pairing-with-django-and-rails-interactive-uis-without-a-javascript-build-system"&gt;Pairing with Django and Rails: interactive UIs without a JavaScript build system&lt;/h2&gt;
&lt;p&gt;If you’re a Django or Rails developer, the appeal of htmx is obvious: your framework already excels at server-rendered views, routing, and forms. htmx lets you keep those strengths while upgrading the interaction model.&lt;/p&gt;
&lt;p&gt;A typical pairing looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django/Rails serve HTML templates as usual.&lt;/li&gt;
&lt;li&gt;htmx requests call the same endpoints (often with partial templates).&lt;/li&gt;
&lt;li&gt;Your templates contain the behavior annotations where it’s needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-pattern-html-forms-but-faster-feedback"&gt;Practical pattern: “HTML forms, but faster feedback”&lt;/h3&gt;
&lt;p&gt;Suppose you have a form that validates input and displays field errors. In a SPA, you might build a client-side form state machine. In an htmx approach, you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Submit the form normally (or via htmx).&lt;/li&gt;
&lt;li&gt;Return either:
&lt;ul&gt;
&lt;li&gt;A success fragment (e.g., updated profile card), or&lt;/li&gt;
&lt;li&gt;The form with validation errors rendered inline.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Users get immediate feedback without the burden of maintaining parallel validation logic. You can also keep accessibility straightforward because your forms remain real forms.&lt;/p&gt;
&lt;h3 id="practical-pattern-tables-that-filter-and-paginate"&gt;Practical pattern: “Tables that filter and paginate”&lt;/h3&gt;
&lt;p&gt;Most internal tools live or die by data exploration. With htmx:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pagination links become htmx requests.&lt;/li&gt;
&lt;li&gt;Sorting and filtering triggers partial updates.&lt;/li&gt;
&lt;li&gt;The page keeps its overall layout, while the data region swaps.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s the “it feels like an app” effect without pretending you built a frontend platform.&lt;/p&gt;
&lt;h2 id="where-htmx-is-not-the-answer-complex-spas-still-have-a-place"&gt;Where htmx is not the answer: complex SPAs still have a place&lt;/h2&gt;
&lt;p&gt;A mature architectural stance means knowing what you’re not replacing. htmx isn’t a universal hammer. If your product needs a highly interactive, client-heavy experience—think complex canvas editing, offline-first behavior, or deeply stateful workflows—React or other SPA approaches can still be the right tool.&lt;/p&gt;
&lt;p&gt;The important point is that the “SPA by default” mindset was never justified for most applications. Many teams built full client apps to render pages that are mostly server-meaningful anyway. For those apps, the cost of SPA architecture—bundling, state synchronization, build tooling, and ongoing UI complexity—was disproportionate.&lt;/p&gt;
&lt;p&gt;htmx doesn’t invalidate SPAs. It rebalances the decision. The question becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does this UI benefit from a rich client runtime?&lt;/li&gt;
&lt;li&gt;Or does it mostly need fast, correct, interactive server-driven rendering?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For many enterprise and internal workflows, the second answer wins.&lt;/p&gt;
&lt;h2 id="the-hypermedia-renaissance-now-mainstream-what-production-teams-are-learning"&gt;The hypermedia renaissance, now mainstream: what production teams are learning&lt;/h2&gt;
&lt;p&gt;The hypermedia renaissance matured when teams stopped treating it as a lifestyle brand and started treating it as a delivery mechanism. htmx 2.0’s improvements accelerate that shift: better performance, better extension ergonomics, and more robust streaming support make it easier to scale from prototypes into production.&lt;/p&gt;
&lt;p&gt;In my view, the most important lesson is cultural: you don’t “graduate” to htmx by announcing it in a blog post. You graduate by building one area of your app the healthier way—then noticing the difference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster iterations because you’re editing templates, not frontend state logic.&lt;/li&gt;
&lt;li&gt;Fewer integration layers between server and UI.&lt;/li&gt;
&lt;li&gt;Easier correctness because the server renders the truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then you expand. Not because it’s trendy, but because it reduces the overhead that keeps enterprise teams stuck.&lt;/p&gt;
&lt;h2 id="conclusion-a-quieter-kind-of-modern"&gt;Conclusion: a quieter kind of modern&lt;/h2&gt;
&lt;p&gt;htmx 2.0 is not a dramatic revolution. It’s a refinement—and, more importantly, it’s a validation. Hypermedia stopped being an act of contrarian faith and became a stable architectural choice.&lt;/p&gt;
&lt;p&gt;If your application doesn’t need a complex SPA runtime, htmx offers a compelling path to interactive UIs with fewer moving parts. And in 2026, “fewer moving parts” isn’t a compromise—it’s a competitive advantage.&lt;/p&gt;</content></item><item><title>The Real Cost of 'Just Use a Microservice'</title><link>https://decastro.work/blog/real-cost-just-use-microservice/</link><pubDate>Sun, 28 Sep 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/real-cost-just-use-microservice/</guid><description>&lt;p&gt;Microservices are sold as an easy path to speed: ship faster, scale independently, sleep better. In practice, most teams discover a harsher truth—operational complexity doesn’t just add work, it multiplies it. And the gap shows up not in theory, but in infrastructure bills, incident volume, and the calendar time your engineers spend doing things that don’t feel like building.&lt;/p&gt;
&lt;p&gt;Below is a data-driven look at what happens when eight teams decomposed monoliths into microservices over roughly three years—and what that means for anyone planning the next rewrite.&lt;/p&gt;</description><content>&lt;p&gt;Microservices are sold as an easy path to speed: ship faster, scale independently, sleep better. In practice, most teams discover a harsher truth—operational complexity doesn’t just add work, it multiplies it. And the gap shows up not in theory, but in infrastructure bills, incident volume, and the calendar time your engineers spend doing things that don’t feel like building.&lt;/p&gt;
&lt;p&gt;Below is a data-driven look at what happens when eight teams decomposed monoliths into microservices over roughly three years—and what that means for anyone planning the next rewrite.&lt;/p&gt;
&lt;h2 id="what-scale-really-means-and-why-most-teams-get-it-wrong"&gt;What “Scale” Really Means (And Why Most Teams Get It Wrong)&lt;/h2&gt;
&lt;p&gt;Here’s the core mistake: most discussions treat “scale” as traffic. If your monolith serves 10,000 RPS, you might think you’re already “large enough” to justify microservices. But for microservices to pay off, the scale that matters is organizational—how many independently changing ownership boundaries you have.&lt;/p&gt;
&lt;p&gt;A monolith handling 10,000 RPS can be perfectly fine because the system boundary is clear: one deployable unit, one runtime model, one place to reason about failures. The moment you split along team ownership—multiple teams making coordinated changes with different release cadences—you create a different kind of scaling problem: coordination overhead.&lt;/p&gt;
&lt;p&gt;In the eight-team pattern observed, teams were often attempting to optimize deployment and scaling by traffic characteristics, even though the real driver should have been organizational autonomy: “Can these components change independently without constant cross-team coordination?” If the answer is “not really,” microservices become a tax collector for everything that used to be internal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical framing:&lt;/strong&gt; Ask this before you refactor.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If team A deploys independently, can team B reliably consume those changes without waiting on A?&lt;/li&gt;
&lt;li&gt;Can you design stable contracts (APIs/events) that won’t break every release?&lt;/li&gt;
&lt;li&gt;Do you already have automated integration and observability that make cross-service failures diagnosable within minutes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you can’t answer yes, you don’t have a microservices need—you have a monolith that could benefit from modularization.&lt;/p&gt;
&lt;h2 id="the-operating-overhead-you-dont-see-in-the-architecture-diagram"&gt;The Operating Overhead You Don’t See in the Architecture Diagram&lt;/h2&gt;
&lt;p&gt;The most consistent outcome across the eight teams wasn’t that microservices “failed.” It was that the operational cost rose far faster than the benefits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operational costs increased by roughly 3–8×&lt;/strong&gt;, while &lt;strong&gt;deployment velocity improved by only ~1.5×&lt;/strong&gt;. That discrepancy is the heart of the story. Microservices didn’t prevent slowdowns; they redistributed them.&lt;/p&gt;
&lt;p&gt;Where did the overhead come from?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;More moving parts, more runtime knobs&lt;/strong&gt;&lt;br&gt;
You go from “one service with one stack” to N services each with their own configurations, dependencies, versioning, and failure modes. Even if each service is simple, the system is not.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Service mesh and platform complexity&lt;/strong&gt;&lt;br&gt;
Service mesh can be useful, but it comes with its own operational model: sidecars, traffic policies, certificate management, routing rules, debugging tools, and failure semantics that differ from “plain HTTP.” Teams frequently underestimated how much time is spent validating mesh behavior during incidents and rollouts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Distributed tracing and metrics pipelines&lt;/strong&gt;&lt;br&gt;
You don’t just “turn on tracing.” You instrument, configure sampling strategies, manage trace context propagation, ensure dashboards are meaningful, and debug gaps where spans don’t line up. The result is better visibility—but it also means more engineering time maintaining the visibility stack itself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cross-service integration testing&lt;/strong&gt;&lt;br&gt;
Unit tests are not enough. Once behavior spans service boundaries, you need integration tests that can stand up multiple components reliably. Those tests become brittle if contracts evolve quickly or environments aren’t standardized.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;On-call rotation expansion&lt;/strong&gt;&lt;br&gt;
More services means more alerts. Even with better tooling, the number of things that can page someone increases. Teams found they needed more coverage per time window, which directly reduces the time available for feature work.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;A concrete example:&lt;/strong&gt; one team decomposed a monolith into billing, catalog, and orders services. Everything looked clean on day one. But in the first month, their biggest recurring work wasn’t “fixing billing.” It was diagnosing why orders were delayed after catalog responses changed. The issue lived at the seams: contract drift, retry behavior, and inconsistent error mapping. None of that would exist in a monolith because the failure is within one deployable boundary.&lt;/p&gt;
&lt;p&gt;Microservices don’t eliminate failure—they relocate it to the interfaces.&lt;/p&gt;
&lt;h2 id="deployment-velocity-didnt-scale-linearly-because-integration-is-the-real-bottleneck"&gt;Deployment Velocity Didn’t Scale Linearly (Because Integration Is the Real Bottleneck)&lt;/h2&gt;
&lt;p&gt;Teams expected microservices to make deployments faster. The reality: deployment cadence improved modestly, but release throughput didn’t translate into overall delivery speed because integration costs grew in parallel.&lt;/p&gt;
&lt;p&gt;Why does that happen?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Independent deployments still require coordinated correctness.&lt;/strong&gt;&lt;br&gt;
Even if you can deploy services independently, users experience end-to-end behavior. If you deploy billing today and orders tomorrow, you still need confidence that the combination works.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backward compatibility becomes a lifecycle, not a promise.&lt;/strong&gt;&lt;br&gt;
Stable APIs and versioned events are essential—but they also create more work, more code paths, and more test matrices.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Rollbacks become multi-service events.&lt;/strong&gt;&lt;br&gt;
In a monolith, rollback is one action. In microservices, rollback might mean reverting one service, temporarily throttling traffic, and restoring an older integration contract while other services remain running.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, teams saw a ~1.5× deployment velocity improvement, but that did not match the overhead required to make those deployments safe. If your integration and observability aren’t “first-class,” the pipeline becomes a treadmill: faster builds, slower safe releases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; treat “integration readiness” as a deployment prerequisite, not an afterthought. If you can’t answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What happens when service A returns a new shape of error?&lt;/li&gt;
&lt;li&gt;How quickly can we trace a user journey across 6 services?&lt;/li&gt;
&lt;li&gt;Do we have realistic staging environments with production-like dependencies?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…then microservices will feel like speed with hidden braking.&lt;/p&gt;
&lt;h2 id="the-seam-problem-mesh-complexity-tracing-overhead-and-contract-drift"&gt;The Seam Problem: Mesh Complexity, Tracing Overhead, and Contract Drift&lt;/h2&gt;
&lt;p&gt;If there’s a villain in most microservice stories, it’s the seams—those moments where systems touch. Microservices increase the number of seams, and each seam needs deliberate design and maintenance.&lt;/p&gt;
&lt;h3 id="service-mesh-complexity"&gt;Service mesh complexity&lt;/h3&gt;
&lt;p&gt;Service meshes can standardize cross-cutting concerns like retries, timeouts, and mTLS. But teams also found mesh configuration itself became a source of subtle failures. Misconfigured retries can amplify load during partial outages. Routing rules can create “works in staging, fails in prod” scenarios. And when incidents happen, debugging mesh behavior often requires specialized knowledge your broader team may not have.&lt;/p&gt;
&lt;h3 id="distributed-tracing-overhead"&gt;Distributed tracing overhead&lt;/h3&gt;
&lt;p&gt;Tracing improves diagnosis, but it also adds operational work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ensure trace context propagation across languages and libraries,&lt;/li&gt;
&lt;li&gt;maintain consistent span naming and attributes,&lt;/li&gt;
&lt;li&gt;decide sampling to avoid drowning in telemetry,&lt;/li&gt;
&lt;li&gt;and keep dashboards actionable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you deploy without a mature tracing strategy, the first incidents will be slower because you don’t yet know where to look.&lt;/p&gt;
&lt;h3 id="cross-service-integration-testing-and-contract-drift"&gt;Cross-service integration testing and contract drift&lt;/h3&gt;
&lt;p&gt;Integration tests are where microservices either earn their keep or reveal their cost. Teams that invested in contract testing, consumer-driven contracts, and automated compatibility checks reduced surprises. Teams that didn’t discovered that the “independent” parts weren’t independent at all—they were coupled through emergent behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sharp takeaway:&lt;/strong&gt; microservices demand stronger discipline than monoliths. If you can’t enforce contracts, you’ll pay for it through outages and regression debugging.&lt;/p&gt;
&lt;h2 id="when-microservices-actually-make-sense-and-when-they-dont"&gt;When Microservices Actually Make Sense (And When They Don’t)&lt;/h2&gt;
&lt;p&gt;This is the part people skip, but it’s where the decision becomes obvious.&lt;/p&gt;
&lt;p&gt;Microservices are more likely to work when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ownership is already split across teams&lt;/strong&gt; with independent change needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can design stable interfaces&lt;/strong&gt; (APIs or events) with clear versioning strategies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You have an operational platform&lt;/strong&gt; (or time to build one) that makes observability and incident response routine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can standardize environments&lt;/strong&gt; so integration tests run consistently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The system has true modular boundaries&lt;/strong&gt;—not just “different folders in a repo.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Microservices are a bad bet when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The monolith’s modularity is already improving with internal refactoring.&lt;/li&gt;
&lt;li&gt;Most services need synchronized releases to avoid breakage.&lt;/li&gt;
&lt;li&gt;Your org isn’t ready for the operational load (on-call coverage, incident culture, tooling ownership).&lt;/li&gt;
&lt;li&gt;You’re chasing traffic scaling while organizational coupling remains high.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yes: a monolith handling 10,000 RPS is often fine. A monolith owned by 10 teams is the scenario that becomes risky—because release coordination becomes the bottleneck, and slow merges drag down throughput.&lt;/p&gt;
&lt;p&gt;So the decision should be less “microservices or monolith?” and more “how much organizational coupling are we willing to pay for?”&lt;/p&gt;
&lt;h2 id="a-better-path-modular-monoliths-then-measured-decomposition"&gt;A Better Path: Modular Monoliths, Then Measured Decomposition&lt;/h2&gt;
&lt;p&gt;If your goal is speed, the safest route is incremental. A &lt;strong&gt;modular monolith&lt;/strong&gt; can preserve the operational simplicity of one deployable while still structuring code around bounded contexts. You get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one deployment unit,&lt;/li&gt;
&lt;li&gt;fewer seams,&lt;/li&gt;
&lt;li&gt;simpler tracing,&lt;/li&gt;
&lt;li&gt;and quicker incident recovery.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, decompose only the parts that meet the bar for autonomy—where you can articulate ownership boundaries, define contracts, and prove that integration testing and observability are mature enough to keep failures diagnosable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical approach teams used successfully (or wished they had):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Stabilize and document internal interfaces first—even before splitting.&lt;/li&gt;
&lt;li&gt;Introduce contract tests while still in the monolith.&lt;/li&gt;
&lt;li&gt;Build an end-to-end observability story early, including correlation IDs and trace propagation patterns.&lt;/li&gt;
&lt;li&gt;Decompose in slices that reduce coupling, not slices that just “seem different.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This isn’t anti-microservice. It’s pro-clarity.&lt;/p&gt;
&lt;h2 id="conclusion-dont-choose-microserviceschoose-measurable-autonomy"&gt;Conclusion: Don’t Choose Microservices—Choose Measurable Autonomy&lt;/h2&gt;
&lt;p&gt;“Just use a microservice” is an architecture reflex, not a strategy. The data from eight teams is blunt: operational costs climbed by 3–8× while deployment velocity improved by only ~1.5×. The productivity gains were eaten by service mesh and platform complexity, distributed tracing overhead, cross-service integration testing, and larger on-call rotations.&lt;/p&gt;
&lt;p&gt;Microservices can be the right tool—when the real driver is organizational scale and when you can invest in the seams: contracts, integration testing, and observability. If you can’t, you’re not buying speed. You’re buying a larger system that requires constant operational attention.&lt;/p&gt;
&lt;p&gt;Start with modular boundaries. Measure your integration pain. And decompose only where autonomy is real—not where complexity is merely fashionable.&lt;/p&gt;</content></item><item><title>The Rise of Local-First Software and Why It Matters</title><link>https://decastro.work/blog/rise-of-local-first-software-why-matters/</link><pubDate>Tue, 16 Sep 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rise-of-local-first-software-why-matters/</guid><description>&lt;p&gt;The best software doesn’t ask permission from the network. It just works—instantly, smoothly, and even when you’re offline. Local-first software flips the old model on its head: instead of treating the cloud as the source of truth and the client as a temporary viewer, it treats your device as the working system. The result is software that feels fast because it doesn’t wait, and resilient because it doesn’t break when the connection does.&lt;/p&gt;</description><content>&lt;p&gt;The best software doesn’t ask permission from the network. It just works—instantly, smoothly, and even when you’re offline. Local-first software flips the old model on its head: instead of treating the cloud as the source of truth and the client as a temporary viewer, it treats your device as the working system. The result is software that feels fast because it doesn’t wait, and resilient because it doesn’t break when the connection does.&lt;/p&gt;
&lt;h2 id="what-local-first-actually-means-and-what-it-rejects"&gt;What “local-first” actually means (and what it rejects)&lt;/h2&gt;
&lt;p&gt;Local-first isn’t a marketing slogan; it’s a design stance. In a local-first app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The source of truth starts on your device.&lt;/strong&gt; Reads and writes happen locally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cloud is a sync destination, not a gatekeeper.&lt;/strong&gt; It receives changes and distributes them to other devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The app can operate without a network.&lt;/strong&gt; You can create, edit, and reorganize data even if connectivity disappears.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conflicts are handled by design, not by user prompts.&lt;/strong&gt; The system merges updates instead of forcing humans to arbitrate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rejected alternative is the classic client-server workflow: load from the server, edit, then save back—often with UI logic that turns into a “loading spinner” festival. Even if the network is usually fast, this model keeps injecting round trips into the user experience. Slow networks and flaky Wi‑Fi aren’t edge cases; they’re normal life.&lt;/p&gt;
&lt;p&gt;Local-first removes the round trips from the critical path. When you type into a note app, the note appears immediately because it’s written locally. When you reconnect, synchronization happens in the background.&lt;/p&gt;
&lt;h2 id="why-the-old-approach-was-good-enough-until-it-wasnt"&gt;Why the old approach was “good enough” until it wasn’t&lt;/h2&gt;
&lt;p&gt;For years, many teams treated “offline” as a special mode: store things locally, then reconcile later. That sounds close to local-first, but the practical difference is where correctness lives. In the old approach, the server often remains the authority. The client may cache, but it still depends on server truth.&lt;/p&gt;
&lt;p&gt;That becomes painful in four common scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Concurrent edits across devices&lt;/strong&gt;&lt;br&gt;
Open the same document on your laptop and phone, edit both, and then watch the conflict resolution dance begin.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Slow or unstable connections&lt;/strong&gt;&lt;br&gt;
If saves are required to render a “successful state,” the UI will stall. Users blame your app even when the problem is the Wi‑Fi.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Real-time-ish features without real-time guarantees&lt;/strong&gt;&lt;br&gt;
Systems built around polling or ephemeral WebSocket state can degrade into “looks fine until it doesn’t.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Complex merge logic&lt;/strong&gt;&lt;br&gt;
The more structured the data, the harder conflict handling becomes. Teams either simplify the product or punt conflicts to the user.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Local-first doesn’t magically eliminate distributed systems complexity—but it puts the complexity where it belongs: inside the sync layer.&lt;/p&gt;
&lt;h2 id="the-sync-problem-merges-without-drama"&gt;The sync problem: merges without drama&lt;/h2&gt;
&lt;p&gt;If multiple devices can change the same data offline, you need a sync system that can merge changes deterministically. You can’t rely on “last writer wins” without corrupting user intent. And you can’t afford to ask users to resolve conflicts every time they lose connectivity for ten minutes.&lt;/p&gt;
&lt;p&gt;This is the core role of &lt;strong&gt;CRDTs (Conflict-free Replicated Data Types)&lt;/strong&gt;. A CRDT is a data structure designed so that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each device can apply updates locally.&lt;/li&gt;
&lt;li&gt;Updates can be exchanged in any order.&lt;/li&gt;
&lt;li&gt;Eventually, all replicas converge to the same state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important practical point is not the math—it’s the UX. With CRDTs, you get &lt;em&gt;merge-by-default&lt;/em&gt;. Instead of detecting conflicts and escalating them, the system merges changes automatically in a way that preserves intent as much as the model allows.&lt;/p&gt;
&lt;h3 id="a-concrete-example-collaborative-notes"&gt;A concrete example: collaborative notes&lt;/h3&gt;
&lt;p&gt;Imagine a shared note with a checklist. On your laptop you tick item 3. On your phone you add a new item right below item 2—offline. When you reconnect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your local edits are already present on each device.&lt;/li&gt;
&lt;li&gt;Sync exchanges the operations.&lt;/li&gt;
&lt;li&gt;The CRDT merges them into a single consistent checklist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Users don’t need to guess which version is “correct,” and the app doesn’t need to block until reconciliation completes. The UI never has to wait for a network round trip to show a usable state.&lt;/p&gt;
&lt;h2 id="crdts-in-practice-automerge-and-yjs-as-the-merge-engines"&gt;CRDTs in practice: Automerge and Yjs as the “merge engines”&lt;/h2&gt;
&lt;p&gt;CRDTs aren’t one monolithic thing. Different CRDT approaches exist, and different ecosystems optimize for different workloads. Two popular building blocks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automerge&lt;/strong&gt;: often used for structured document editing where you want a high-level “document that merges.” It’s a good fit when your app’s data model is rich and you want predictable merging behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yjs&lt;/strong&gt;: widely adopted for real-time collaborative editing patterns. It’s commonly paired with providers that handle transport (WebRTC, WebSocket, etc.) and persistence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical takeaway: you don’t build a custom merge algorithm for every feature. You pick a proven CRDT library, model your data as CRDT-managed structures, and let the library handle convergence.&lt;/p&gt;
&lt;h3 id="a-developer-friendly-mindset"&gt;A developer-friendly mindset&lt;/h3&gt;
&lt;p&gt;To make CRDTs work smoothly, structure your app so that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local changes are first-class.&lt;/strong&gt; Your UI writes to the CRDT state immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync is incremental and backgrounded.&lt;/strong&gt; You send/receive updates continuously rather than doing full document overwrites.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Persistence is explicit.&lt;/strong&gt; Store the CRDT state (or its updates) locally so reopening the app doesn’t re-run “download to render” flows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your app architecture is already event-driven, CRDTs fit naturally: local events mutate local state, and sync propagates the same state transitions outward.&lt;/p&gt;
&lt;h2 id="electricsql-and-the-database-that-speaks-crdt-idea"&gt;ElectricSQL and the “database that speaks CRDT” idea&lt;/h2&gt;
&lt;p&gt;CRDTs are often presented as collaboration tools, but their real leverage appears when you treat them as a persistence layer rather than just an in-memory sync strategy. That’s where tools like &lt;strong&gt;ElectricSQL&lt;/strong&gt; enter the conversation: they aim to make local-first development closer to normal database workflows.&lt;/p&gt;
&lt;p&gt;Instead of thinking, “How do I synchronize this custom document format?” you think, “How do I keep my local database consistent with other replicas while still supporting offline work?”&lt;/p&gt;
&lt;p&gt;The most valuable shift is psychological and architectural:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You stop designing around the idea that the server must be reachable to do useful work.&lt;/li&gt;
&lt;li&gt;You design around local correctness first, then synchronization as a system behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practical terms, teams can build features that feel like standard database-backed apps—except the sync layer handles replication and conflict resolution without asking the UI to babysit the network.&lt;/p&gt;
&lt;h2 id="what-changes-for-ux-performance-and-engineering-teams"&gt;What changes for UX, performance, and engineering teams&lt;/h2&gt;
&lt;p&gt;Local-first isn’t only a technical change; it changes what your product can promise.&lt;/p&gt;
&lt;h3 id="1-the-end-of-loading-to-edit"&gt;1) The end of “loading to edit”&lt;/h3&gt;
&lt;p&gt;With local-first, “load” becomes “hydrate” and “render,” not “wait.” If your data is already on-device, the app can show content immediately. Sync catches up later.&lt;/p&gt;
&lt;p&gt;That’s how you avoid the spinner loops users hate: &lt;em&gt;“Fetching updates…”&lt;/em&gt;, &lt;em&gt;“Syncing…”&lt;/em&gt;, &lt;em&gt;“Reconnecting…”&lt;/em&gt;. The app remains interactive because the user is never blocked by remote state.&lt;/p&gt;
&lt;h3 id="2-offline-becomes-a-feature-not-a-fallback"&gt;2) Offline becomes a feature, not a fallback&lt;/h3&gt;
&lt;p&gt;Offline-first used to mean “read-only with occasional edits.” Local-first means offline edits are the default. If sync is robust, offline isn’t an exception path—it’s just another condition.&lt;/p&gt;
&lt;h3 id="3-engineering-shifts-from-requestresponse-to-replication-mindset"&gt;3) Engineering shifts from request/response to replication mindset&lt;/h3&gt;
&lt;p&gt;You stop designing APIs around “submit changes, server validates, client displays.” Instead, you design around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;local mutation&lt;/li&gt;
&lt;li&gt;durable local storage&lt;/li&gt;
&lt;li&gt;deterministic merge&lt;/li&gt;
&lt;li&gt;background replication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This also clarifies responsibilities: the sync engine owns distribution and convergence; the UI owns immediate feedback.&lt;/p&gt;
&lt;h3 id="4-testing-becomes-more-realistic"&gt;4) Testing becomes more realistic&lt;/h3&gt;
&lt;p&gt;Network simulations matter, but local-first changes the test surface dramatically. You can test correctness by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;applying updates independently on two replicas&lt;/li&gt;
&lt;li&gt;permuting update order&lt;/li&gt;
&lt;li&gt;verifying convergence after sync&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s a cleaner mental model than trying to reproduce timing-dependent server-side race conditions.&lt;/p&gt;
&lt;h2 id="the-future-isnt-faster-networksits-fewer-required-networks"&gt;The future isn’t faster networks—it’s fewer required networks&lt;/h2&gt;
&lt;p&gt;It’s tempting to chase performance by upgrading infrastructure: lower latency CDNs, faster APIs, better caching. Those improvements help, but local-first attacks the root UX problem: &lt;strong&gt;network round trips shouldn’t be on the critical path for core operations.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When CRDT-based sync is done right, most user actions don’t depend on connectivity. The cloud becomes a distribution mechanism, not a dependency.&lt;/p&gt;
&lt;p&gt;That’s why local-first is rising now. It’s not just because developers want offline mode—it’s because CRDTs and modern sync tooling have matured into practical, integrable building blocks. Libraries like Automerge and Yjs provide the merge semantics. Systems like ElectricSQL bring the “database-like” experience closer to what teams already understand.&lt;/p&gt;
&lt;p&gt;The common outcome is straightforward: apps that start instantly, feel responsive under stress, and recover gracefully. Not because the network is magical—but because the app no longer needs the network to be “real.”&lt;/p&gt;
&lt;h2 id="conclusion-build-for-local-truth-then-sync-with-confidence"&gt;Conclusion: Build for local truth, then sync with confidence&lt;/h2&gt;
&lt;p&gt;Local-first software is a bet on a simple idea: your device should be able to do real work without waiting. CRDTs make the hard part—conflicting updates—tractable by ensuring convergence without user drama. With proven tools and sync engines, teams can finally deliver the experience users expect: edits that appear instantly, offline reliability, and background sync that doesn’t interrupt the flow. The next wave of software won’t just be faster—it will be less dependent on the connection entirely.&lt;/p&gt;</content></item><item><title>Hono Became the Express.js of the Edge Computing Era</title><link>https://decastro.work/blog/hono-became-expressjs-edge-era/</link><pubDate>Wed, 10 Sep 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/hono-became-expressjs-edge-era/</guid><description>&lt;p&gt;Edge computing used to feel like a niche hobby—deploy a toy API to a single platform and pray your code survives the next runtime update. Hono changes that vibe. It’s the rare web framework that feels purpose-built for today’s reality: many runtimes, many deployment targets, and too much time wasted on “it works on Workers but not on Lambda.”&lt;/p&gt;
&lt;p&gt;Hono has crossed 50,000 GitHub stars for a reason. It’s small, standards-forward, runtime-agnostic, and surprisingly pleasant to use. If Express defined the Node.js web era, Hono is shaping what comes next: runtime-independent, edge-first services that you can ship without rewriting everything every time your infrastructure changes.&lt;/p&gt;</description><content>&lt;p&gt;Edge computing used to feel like a niche hobby—deploy a toy API to a single platform and pray your code survives the next runtime update. Hono changes that vibe. It’s the rare web framework that feels purpose-built for today’s reality: many runtimes, many deployment targets, and too much time wasted on “it works on Workers but not on Lambda.”&lt;/p&gt;
&lt;p&gt;Hono has crossed 50,000 GitHub stars for a reason. It’s small, standards-forward, runtime-agnostic, and surprisingly pleasant to use. If Express defined the Node.js web era, Hono is shaping what comes next: runtime-independent, edge-first services that you can ship without rewriting everything every time your infrastructure changes.&lt;/p&gt;
&lt;h2 id="what-makes-hono-feel-like-express-but-for-everything"&gt;What Makes Hono Feel Like “Express, but for Everything”&lt;/h2&gt;
&lt;p&gt;Express became the default because it hit a sweet spot: simple routing, middleware that’s easy to reason about, and an ecosystem that kept growing. Hono hits a similar sweet spot—but with a crucial upgrade: it runs on &lt;em&gt;every&lt;/em&gt; JavaScript runtime you care about.&lt;/p&gt;
&lt;p&gt;The headline is the size: Hono is roughly 14KB. That matters more than it sounds. Smaller frameworks mean fewer abstraction leaks, faster cold-starts in serverless environments, and less overhead when you’re pushing requests through an edge runtime where every millisecond counts.&lt;/p&gt;
&lt;p&gt;But the bigger reason Hono feels familiar is how it models the web. Hono follows standards-based request/response patterns instead of forcing you into a platform-specific API. In practice, that means your handlers look like web code—not like a runtime-specific experiment.&lt;/p&gt;
&lt;p&gt;Concretely, you can write the same application using the same core APIs across Node.js, Bun, Deno, Cloudflare Workers, and AWS Lambda-style environments—without “runtime branches” scattered throughout your codebase.&lt;/p&gt;
&lt;h2 id="standards-based-by-default-write-web-code-not-runtime-code"&gt;Standards-Based by Default: Write Web Code, Not Runtime Code&lt;/h2&gt;
&lt;p&gt;The easiest way to understand Hono is to treat it like you’re writing for the web first. You’re working with the semantics you already know: HTTP requests, headers, bodies, status codes, and responses.&lt;/p&gt;
&lt;p&gt;This matters when you move to the edge because edge runtimes don’t share the same assumptions as traditional Node.js servers. Some don’t support the Node.js standard library the way you expect. Some don’t behave like a long-running process. Some have different streaming and body-handling behavior.&lt;/p&gt;
&lt;p&gt;Hono avoids the worst of that by aligning with the web platform’s primitives. The result is that migrating from “local Node server” to “edge deployment” is far less about porting and far more about packaging and configuration—exactly what you want.&lt;/p&gt;
&lt;h3 id="practical-example-a-minimal-route-that-stays-portable"&gt;Practical example: a minimal route that stays portable&lt;/h3&gt;
&lt;p&gt;With Hono, you can keep your app logic focused on request handling rather than on the runtime’s quirks. A route stays a route whether you deploy it to a worker or a serverless function. The interface is familiar, but the portability is the point.&lt;/p&gt;
&lt;p&gt;You won’t need to learn a separate framework dialect for each platform—because Hono isn’t pretending the platforms are the same. It’s building a stable layer on top of their shared web capabilities.&lt;/p&gt;
&lt;h2 id="typescript-youll-actually-enjoy-plus-an-rpc-helper"&gt;TypeScript You’ll Actually Enjoy (Plus an RPC Helper)&lt;/h2&gt;
&lt;p&gt;Most “frameworks with TypeScript support” still feel like TypeScript-as-afterthought. Hono doesn’t. TypeScript integration is one of the places where it feels modern and confident, not bolted on.&lt;/p&gt;
&lt;p&gt;You get solid inference and ergonomic patterns that help you keep types accurate as your handlers grow. But the real differentiator for teams building real services is the RPC helper—an approach that supports end-to-end type safety.&lt;/p&gt;
&lt;p&gt;That’s not just a developer-experience win; it’s an operational win. When client and server disagree about shapes—payload fields, return types, or error formats—you usually find out in production. With an RPC-style workflow, you can catch those mismatches at development time.&lt;/p&gt;
&lt;h3 id="what-end-to-end-type-safety-means-in-practice"&gt;What “end-to-end type safety” means in practice&lt;/h3&gt;
&lt;p&gt;Imagine you have a &lt;code&gt;createUser&lt;/code&gt; operation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your API expects &lt;code&gt;{ email: string; name: string }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Your frontend supplies those fields&lt;/li&gt;
&lt;li&gt;Your code transforms them into a database-ready model&lt;/li&gt;
&lt;li&gt;Your API returns a structured response your UI uses to render the next state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With a type-safe RPC helper, you’re not maintaining two separate contracts in two separate places. You’re maintaining one. That reduces churn and makes refactors dramatically less scary—especially when you’re shipping quickly across environments.&lt;/p&gt;
&lt;h2 id="middleware-and-ecosystem-mature-enough-to-build-real-products"&gt;Middleware and Ecosystem: Mature Enough to Build Real Products&lt;/h2&gt;
&lt;p&gt;A framework is only as good as its “boring parts”—logging, auth, validation, compression, error handling, and the rest of the daily glue code that turns routes into a product.&lt;/p&gt;
&lt;p&gt;Hono’s middleware ecosystem is mature enough that you can assemble common patterns without reinventing everything. That’s crucial if you want to migrate from Express without feeling like you’re starting from scratch.&lt;/p&gt;
&lt;p&gt;You can take your existing Express mental model and map it over:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Middleware chains become middleware chains&lt;/li&gt;
&lt;li&gt;Route handlers remain handlers&lt;/li&gt;
&lt;li&gt;Composition stays straightforward&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The migration story here is the opposite of the painful kind. You shouldn’t be rewriting your business logic. You should be swapping the framework layer, adjusting a few API details, and shipping.&lt;/p&gt;
&lt;h2 id="migrating-from-express-the-sharp-practical-path"&gt;Migrating from Express: The Sharp, Practical Path&lt;/h2&gt;
&lt;p&gt;Let’s be honest: most teams don’t “rewrite” when they adopt a new framework. They migrate.&lt;/p&gt;
&lt;p&gt;The good news is that migrating from Express to Hono is typically straightforward because your architecture already exists:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Routes and controllers&lt;/strong&gt;: Move handler functions over with minimal logic changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Middleware&lt;/strong&gt;: Convert middleware registration to Hono’s approach.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request/response details&lt;/strong&gt;: Adjust anything that relied on Node-specific behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation and serialization&lt;/strong&gt;: Prefer Hono-friendly patterns so your code stays runtime-agnostic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy target&lt;/strong&gt;: Pick the edge runtime you want first—then make sure the app behaves the same everywhere.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="a-realistic-example-turning-an-express-style-api-into-hono"&gt;A realistic example: turning an Express-style API into Hono&lt;/h3&gt;
&lt;p&gt;Say your Express app has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/health&lt;/code&gt; route&lt;/li&gt;
&lt;li&gt;authentication middleware&lt;/li&gt;
&lt;li&gt;a couple of JSON endpoints&lt;/li&gt;
&lt;li&gt;centralized error handling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Hono, the structure remains familiar: you define routes, attach middleware, and keep your logic in handler functions. The biggest differences tend to show up only where Express code relied on Node’s streaming quirks, request object extensions, or nonstandard response helpers.&lt;/p&gt;
&lt;p&gt;The payoff is immediate: once deployed, edge runtimes typically feel snappier. Your service stops “waiting” on the same server characteristics every time and benefits from closer geographic placement and a more lightweight runtime model.&lt;/p&gt;
&lt;p&gt;And since Hono is runtime-agnostic, you’re no longer locked into “whatever Node server we run today.” Your architecture becomes resilient to infrastructure churn.&lt;/p&gt;
&lt;h2 id="performance-and-deployment-why-runs-everywhere-changes-the-business"&gt;Performance and Deployment: Why “Runs Everywhere” Changes the Business&lt;/h2&gt;
&lt;p&gt;Frameworks shouldn’t be a theology debate, and Hono isn’t either. It’s a practical tool.&lt;/p&gt;
&lt;p&gt;Edge-first changes the distribution of latency across your app. It reduces the distance between user and execution environment and can improve perceived speed. But the real business impact is operational: fewer rewrite cycles, faster iteration, and simpler platform selection.&lt;/p&gt;
&lt;p&gt;When your framework is runtime-agnostic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can start with a worker and later move to another platform&lt;/li&gt;
&lt;li&gt;You can use serverless for some endpoints and edge for others&lt;/li&gt;
&lt;li&gt;You can test different deployment targets without changing application architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hono makes “platform flexibility” real by making the code portable, not merely “deployable with minor changes.”&lt;/p&gt;
&lt;h2 id="conclusion-express-built-a-node-erahono-is-building-the-next-one"&gt;Conclusion: Express Built a Node Era—Hono Is Building the Next One&lt;/h2&gt;
&lt;p&gt;Express defined how millions of developers built web services in the Node.js world. Hono is defining how teams build web services across the edge-first, runtime-agnostic world—standards-based, runtime-agnostic, TypeScript-friendly, and fast without feeling fragile.&lt;/p&gt;
&lt;p&gt;If you’re still treating deployment targets as a reason to fork your codebase, it’s time to stop. Hono gives you the one thing modern engineering desperately needs: a framework that lets you ship the same application everywhere, with less ceremony and fewer surprises.&lt;/p&gt;</content></item><item><title>PostgreSQL 17 Features That Make Me Unreasonably Excited</title><link>https://decastro.work/blog/postgresql-17-features-unreasonably-excited/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgresql-17-features-unreasonably-excited/</guid><description>&lt;p&gt;PostgreSQL has never been the database that tries to impress you with gimmicks. It’s the database that quietly fixes the exact problems you didn’t want to admit you had—then ships the fix in a release that feels almost… inevitable. PostgreSQL 17 is one of those releases. And yes, it has me unreasonably excited.&lt;/p&gt;
&lt;p&gt;This time, it’s a rare combo: operational pain gets easier (incremental backup), everyday developer work gets cleaner (enhanced JSON-to-relational), and schema modeling finally behaves the way identity columns were supposed to. On top of that, the boring-but-critical scaling knobs—vacuuming and parallel execution—get meaningful attention. Let’s dig in.&lt;/p&gt;</description><content>&lt;p&gt;PostgreSQL has never been the database that tries to impress you with gimmicks. It’s the database that quietly fixes the exact problems you didn’t want to admit you had—then ships the fix in a release that feels almost… inevitable. PostgreSQL 17 is one of those releases. And yes, it has me unreasonably excited.&lt;/p&gt;
&lt;p&gt;This time, it’s a rare combo: operational pain gets easier (incremental backup), everyday developer work gets cleaner (enhanced JSON-to-relational), and schema modeling finally behaves the way identity columns were supposed to. On top of that, the boring-but-critical scaling knobs—vacuuming and parallel execution—get meaningful attention. Let’s dig in.&lt;/p&gt;
&lt;h2 id="incremental-backup-less-downtime-fewer-backup-day-rituals"&gt;Incremental backup: less downtime, fewer “backup day” rituals&lt;/h2&gt;
&lt;p&gt;If you’ve ever backed up a large PostgreSQL database, you already know the emotional arc: start the job, watch throughput, pray the storage doesn’t choke, and then spend the rest of the day in “restore rehearsal” mode because “we should really test that.”&lt;/p&gt;
&lt;p&gt;Incremental backup in PostgreSQL 17 targets that entire experience. Instead of treating every backup as a full snapshot, PostgreSQL can produce backups that include only changes since the last backup (conceptually speaking). The payoff is straightforward: backup windows shrink, and the operational cost of “regular backups” becomes realistic rather than aspirational.&lt;/p&gt;
&lt;h3 id="what-this-looks-like-in-practice"&gt;What this looks like in practice&lt;/h3&gt;
&lt;p&gt;Imagine a system where full backups take 6 hours during a busy window, and you can only run them at night. With incremental backups, you can shift toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;More frequent backups&lt;/strong&gt; (because each one is cheaper).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shorter “backup-impact” periods&lt;/strong&gt; (because you’re not rewriting the universe each time).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster recovery planning&lt;/strong&gt; (because you can combine backups and log changes more efficiently than “one huge full backup plus everything else”).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical strategy I like: run a &lt;strong&gt;baseline full backup&lt;/strong&gt; regularly (say, weekly), then use &lt;strong&gt;incremental backups&lt;/strong&gt; more frequently (daily or even more often, depending on your change rate and storage budget). That gives you a recovery chain that’s neither too shallow (riskier) nor too deep (harder to manage).&lt;/p&gt;
&lt;h3 id="a-caution-worth-keeping"&gt;A caution worth keeping&lt;/h3&gt;
&lt;p&gt;Incremental backups are not magic. You still need to think about retention, cataloging backup sets, and ensuring you can reliably restore from the chain. The win is that the chain becomes less painful to maintain, not that you can skip testing restores.&lt;/p&gt;
&lt;p&gt;If you haven’t restored from your backups recently, consider PostgreSQL 17 a forcing function. Your future self will thank you.&lt;/p&gt;
&lt;h2 id="enhanced-json_table-stop-duct-taping-json-into-relational-queries"&gt;Enhanced JSON_TABLE: stop duct-taping JSON into relational queries&lt;/h2&gt;
&lt;p&gt;JSON is great for flexibility. It’s also a magnet for complexity. You start with “just a small nested object,” then add five more fields over time, and suddenly your SQL is full of repetitive extraction logic, lateral joins, and hand-rolled transformations that nobody wants to touch.&lt;/p&gt;
&lt;p&gt;PostgreSQL 17’s enhanced &lt;code&gt;JSON_TABLE&lt;/code&gt; moves the needle toward something much more pleasant: transforming JSON into a relational shape in a way that is designed for querying.&lt;/p&gt;
&lt;h3 id="why-json_table-changes-the-feel-of-the-code"&gt;Why &lt;code&gt;JSON_TABLE&lt;/code&gt; changes the feel of the code&lt;/h3&gt;
&lt;p&gt;The key improvement is that you can treat JSON arrays and objects as &lt;strong&gt;structured rows and columns&lt;/strong&gt; without turning your query into spaghetti. Instead of scattering &lt;code&gt;-&amp;gt;&lt;/code&gt;, &lt;code&gt;-&amp;gt;&amp;gt;&lt;/code&gt;, casting, and &lt;code&gt;COALESCE&lt;/code&gt; across half a dozen expressions, you can describe the target table-like structure more directly.&lt;/p&gt;
&lt;p&gt;Here’s the pattern that tends to become common:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You receive JSON payloads (from an API, event stream, or document store style workflow).&lt;/li&gt;
&lt;li&gt;You want to filter, aggregate, or join that payload with relational tables.&lt;/li&gt;
&lt;li&gt;You don’t want to implement “JSON shredding” differently for every endpoint.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;code&gt;JSON_TABLE&lt;/code&gt;, your SQL can become a consistent pipeline: &lt;strong&gt;JSON in → rows/columns out → normal SQL behavior&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="example-query-an-array-of-items-with-relational-clarity"&gt;Example: query an array of items with relational clarity&lt;/h3&gt;
&lt;p&gt;Suppose you store an “order events” JSON document like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &lt;span style="color:#f92672"&gt;&amp;#34;sku&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;A1&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;#34;qty&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;9.99&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &lt;span style="color:#f92672"&gt;&amp;#34;sku&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;B2&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;#34;qty&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;19.50&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To sum quantities per SKU, instead of repeatedly extracting fields, you can use &lt;code&gt;JSON_TABLE&lt;/code&gt; to map &lt;code&gt;items[]&lt;/code&gt; into rows with typed columns (&lt;code&gt;sku&lt;/code&gt;, &lt;code&gt;qty&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;). From there, it’s normal SQL: &lt;code&gt;GROUP BY sku&lt;/code&gt;, joins to a products table, filtering by price thresholds, etc.&lt;/p&gt;
&lt;p&gt;The practical benefit: fewer edge cases and fewer “why does this cast differently in this one query?” moments.&lt;/p&gt;
&lt;h3 id="opinionated-advice-treat-json-as-an-input-format-not-your-long-term-schema"&gt;Opinionated advice: treat JSON as an input format, not your long-term schema&lt;/h3&gt;
&lt;p&gt;If you use JSON as a storage format, fine—just don’t let it become a long-term substitute for proper tables when the data is genuinely relational. Use &lt;code&gt;JSON_TABLE&lt;/code&gt; to bridge the gap, then consider whether the most important fields deserve first-class columns (or at least generated columns / indexing strategies where appropriate).&lt;/p&gt;
&lt;p&gt;PostgreSQL is great at accommodating both worlds; PostgreSQL 17 makes the bridge sturdier.&lt;/p&gt;
&lt;h2 id="identity-columns-done-right-schemas-stop-feeling-accidentally-correct"&gt;Identity columns done right: schemas stop feeling “accidentally correct”&lt;/h2&gt;
&lt;p&gt;Identity columns have been one of those features that people repeatedly tried to use—but often with a slightly haunted feeling. You want auto-increment behavior that’s reliable, safe under concurrency, and explicit in schema. You also want it to play nicely with migrations.&lt;/p&gt;
&lt;p&gt;PostgreSQL 17’s improvements around identity columns make them behave the way developers expect: cleaner semantics, fewer surprises, and a more consistent migration story.&lt;/p&gt;
&lt;h3 id="what-done-right-means-for-daily-work"&gt;What “done right” means for daily work&lt;/h3&gt;
&lt;p&gt;In real systems, identity columns are involved in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Importing data into tables with existing keys.&lt;/li&gt;
&lt;li&gt;Writing migrations that add identity columns to existing tables.&lt;/li&gt;
&lt;li&gt;Handling sequences and ownership correctly so that the database enforces integrity.&lt;/li&gt;
&lt;li&gt;Preventing the classic failure mode: “someone inserted rows and now the next id collides.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If identity columns are configured and maintained cleanly, you don’t have to keep a mental model of how sequences were created, who owns them, and what happens when rows are inserted out of band.&lt;/p&gt;
&lt;h3 id="migration-tip-validate-behavior-dont-just-rely-on-it"&gt;Migration tip: validate behavior, don’t just rely on it&lt;/h3&gt;
&lt;p&gt;When you change identity behavior or retrofit identity onto an existing table, do two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run a migration in a staging environment with representative data volume.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify the next generated value&lt;/strong&gt; (especially after inserts that might bypass the ORM path).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It’s not about fear—it’s about certainty. Identity columns are where correctness should be boring.&lt;/p&gt;
&lt;p&gt;PostgreSQL 17 is making that boredom easier to achieve.&lt;/p&gt;
&lt;h2 id="vacuuming-improvements-make-eventually-mean-predictably"&gt;Vacuuming improvements: make “eventually” mean “predictably”&lt;/h2&gt;
&lt;p&gt;Vacuum is one of those topics that always arrives right after you’ve already been burned. The database is slow, table bloat is creeping up, autovacuum settings were “fine” until they weren’t, and now you’re debugging performance like it’s a crime scene.&lt;/p&gt;
&lt;p&gt;PostgreSQL 17 improves aspects of vacuuming performance and behavior, which matters because vacuuming is how PostgreSQL maintains its MVCC reality. In other words: vacuum isn’t optional; it’s the price you pay for concurrency and consistency.&lt;/p&gt;
&lt;h3 id="why-this-is-more-than-housekeeping"&gt;Why this is more than housekeeping&lt;/h3&gt;
&lt;p&gt;When vacuuming is efficient and more predictable, it impacts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Query latency&lt;/strong&gt; (less bloat, fewer slowdowns)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index health&lt;/strong&gt; (less churn, fewer pathological states)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational stability&lt;/strong&gt; (fewer “why is it worse today?” mysteries)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical result is that scaling becomes less dependent on heroic tuning. You still need sane defaults, monitoring, and maintenance—but you get a more forgiving baseline.&lt;/p&gt;
&lt;h3 id="what-to-do-after-upgrading"&gt;What to do after upgrading&lt;/h3&gt;
&lt;p&gt;Don’t just deploy and forget. After upgrading to PostgreSQL 17:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Review your autovacuum settings and ensure they’re aligned with your workload pattern.&lt;/li&gt;
&lt;li&gt;Monitor table bloat trends before and after.&lt;/li&gt;
&lt;li&gt;Watch for changes in vacuum runtime characteristics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you previously had to over-tune to keep latency stable, this release might allow you to simplify. That’s a win you feel every day.&lt;/p&gt;
&lt;h2 id="parallel-query-execution-scaling-without-turning-your-hardware-into-a-lottery"&gt;Parallel query execution: scaling without turning your hardware into a lottery&lt;/h2&gt;
&lt;p&gt;PostgreSQL has made parallel execution increasingly capable over time, but performance scaling has often felt like an art project: sometimes parallelism helps a lot, sometimes it barely moves the needle, and sometimes the plan changes in ways that make you second-guess the optimizer.&lt;/p&gt;
&lt;p&gt;PostgreSQL 17 continues the trend of improving parallel query execution. That matters because modern workloads aren’t just bigger—they’re more varied, more concurrent, and more latency-sensitive.&lt;/p&gt;
&lt;h3 id="practical-expectations"&gt;Practical expectations&lt;/h3&gt;
&lt;p&gt;Parallel query benefits most when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queries scan large datasets.&lt;/li&gt;
&lt;li&gt;Work is naturally decomposable (e.g., per-partition operations).&lt;/li&gt;
&lt;li&gt;You’re able to provide enough CPU and memory headroom for worker processes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get value from parallelism, you generally need three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Appropriate indexes&lt;/strong&gt; (so parallel work isn’t wasted).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost settings&lt;/strong&gt; that don’t discourage parallel plans.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware and configuration&lt;/strong&gt; that allow workers to run without starving the system.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="make-it-concrete-check-plans-the-right-way"&gt;Make it concrete: check plans the right way&lt;/h3&gt;
&lt;p&gt;After upgrading, test critical query paths and compare execution plans. Look for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Whether the planner chooses parallel plans.&lt;/li&gt;
&lt;li&gt;Whether the degree of parallelism is reasonable.&lt;/li&gt;
&lt;li&gt;Whether runtime improvements align with reductions in scanned rows or expensive operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don’t treat “parallel” as a checkbox. Treat it as a tool—and verify the tool is being used effectively.&lt;/p&gt;
&lt;h2 id="scaling-and-correctness-improvements-that-add-up"&gt;Scaling and correctness improvements that add up&lt;/h2&gt;
&lt;p&gt;The features above aren’t isolated wins. They form a coherent story:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incremental backup reduces operational friction and enables safer recovery practices.&lt;/li&gt;
&lt;li&gt;Enhanced &lt;code&gt;JSON_TABLE&lt;/code&gt; reduces query complexity and makes JSON-to-relational transformations more consistent.&lt;/li&gt;
&lt;li&gt;Identity columns improve schema correctness and migration sanity.&lt;/li&gt;
&lt;li&gt;Vacuuming and parallel execution improvements address the last-mile scaling concerns that tend to turn “it works” into “why is it painful?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever considered switching away from PostgreSQL because “it’s tough at scale,” PostgreSQL 17 is the kind of release that challenges that narrative. Not by adding magic, but by sanding down the rough edges in the parts of the system where teams actually suffer.&lt;/p&gt;
&lt;h2 id="conclusion-the-kind-of-release-you-build-on"&gt;Conclusion: the kind of release you build on&lt;/h2&gt;
&lt;p&gt;PostgreSQL 17 feels like a mature upgrade: it doesn’t ask you to relearn your database, and it doesn’t chase flashy novelty. Instead, it makes the day-to-day better—faster backups, cleaner JSON queries, identity columns that behave, and performance improvements that help your system stay healthy under load.&lt;/p&gt;
&lt;p&gt;If you manage production databases or write SQL for a living, this is the release where you’ll notice the improvements not in benchmarks, but in fewer outages, fewer migrations-that-keep-you-up, and fewer “why is this query different today?” moments. That’s the good stuff.&lt;/p&gt;</content></item><item><title>Every Framework Claims Server-Side Rendering—Here's What Actually Matters</title><link>https://decastro.work/blog/every-framework-claims-ssr-what-actually-matters/</link><pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/every-framework-claims-ssr-what-actually-matters/</guid><description>&lt;p&gt;Your feed is full of badges: SSR! SSG! ISR! Streaming! Partial hydration! Resumability! The industry keeps inventing new labels for the same handful of problems: render fast, ship content that search engines can find, and keep interaction snappy without melting your servers. The good news: you don’t need to memorize taxonomy. You need to optimize a small set of user-visible outcomes—and pick the strategy that serves them.&lt;/p&gt;
&lt;p&gt;Let’s cut through the marketing fog and talk about what actually matters: &lt;strong&gt;time to first byte&lt;/strong&gt;, &lt;strong&gt;time to interactive&lt;/strong&gt;, and &lt;strong&gt;indexability&lt;/strong&gt;. Then we’ll map those outcomes to the handful of architectures worth caring about.&lt;/p&gt;</description><content>&lt;p&gt;Your feed is full of badges: SSR! SSG! ISR! Streaming! Partial hydration! Resumability! The industry keeps inventing new labels for the same handful of problems: render fast, ship content that search engines can find, and keep interaction snappy without melting your servers. The good news: you don’t need to memorize taxonomy. You need to optimize a small set of user-visible outcomes—and pick the strategy that serves them.&lt;/p&gt;
&lt;p&gt;Let’s cut through the marketing fog and talk about what actually matters: &lt;strong&gt;time to first byte&lt;/strong&gt;, &lt;strong&gt;time to interactive&lt;/strong&gt;, and &lt;strong&gt;indexability&lt;/strong&gt;. Then we’ll map those outcomes to the handful of architectures worth caring about.&lt;/p&gt;
&lt;h2 id="the-taxonomy-problem-most-rendering-is-just-caching-and-scheduling"&gt;The Taxonomy Problem: Most “Rendering” Is Just Caching and Scheduling&lt;/h2&gt;
&lt;p&gt;Framework teams market rendering modes as if they’re fundamentally different technologies. In practice, many of them differ in &lt;strong&gt;when&lt;/strong&gt; HTML is produced and &lt;strong&gt;how&lt;/strong&gt; it’s cached and delivered.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSR&lt;/strong&gt; typically means: render HTML on demand (per request).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSG&lt;/strong&gt; typically means: render HTML ahead of time (build time).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISR&lt;/strong&gt; typically means: render HTML ahead of time, but refresh it later on a schedule or on demand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming SSR&lt;/strong&gt; means: start sending the HTML before the full page is ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial hydration / islands&lt;/strong&gt; means: ship HTML for everything, but only hydrate interactive parts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under the hood, you’re juggling three levers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Latency to HTML&lt;/strong&gt; (how fast the server can produce bytes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency to interactivity&lt;/strong&gt; (how fast JS becomes functional)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cacheability and freshness&lt;/strong&gt; (how often you need to redo work)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you keep these levers in mind, the taxonomy stops being confusing and starts being useful.&lt;/p&gt;
&lt;h2 id="the-only-metrics-that-pay-rent-ttfb-tti-and-indexability"&gt;The Only Metrics That Pay Rent: TTFB, TTI, and Indexability&lt;/h2&gt;
&lt;p&gt;Before you choose a framework feature, decide what you’re optimizing for. For most real products, these three metrics answer the question:&lt;/p&gt;
&lt;h3 id="1-time-to-first-byte-ttfb-do-users-see-something-quickly"&gt;1) Time to First Byte (TTFB): Do users see something quickly?&lt;/h3&gt;
&lt;p&gt;TTFB is the moment the browser receives the first byte of HTML (or a meaningful payload). People don’t judge “performance” in milliseconds; they judge whether the page feels responsive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical test:&lt;/strong&gt; throttle to a slow 4G profile, load the page, and see how quickly the first paint starts. If your SSR takes too long to produce HTML, streaming or pre-rendering might be the fix.&lt;/p&gt;
&lt;h3 id="2-time-to-interactive-tti-can-users-do-anything-without-waiting"&gt;2) Time to Interactive (TTI): Can users do anything without waiting?&lt;/h3&gt;
&lt;p&gt;TTFB can be great and still feel slow if you block interactivity with heavy JS bundles or delayed hydration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical test:&lt;/strong&gt; use DevTools to inspect long tasks and the moment event handlers become active. If “interactive” arrives late, your problem is often hydration strategy, not HTML delivery.&lt;/p&gt;
&lt;h3 id="3-indexability-will-search-engines-reliably-understand-the-content"&gt;3) Indexability: Will search engines reliably understand the content?&lt;/h3&gt;
&lt;p&gt;This isn’t about being “SEO-friendly” in the abstract—it’s about whether bots can fetch and interpret the content consistently.&lt;/p&gt;
&lt;p&gt;If your “server-side” story depends on client-side rendering for the meaningful text, you’re forcing your users (and bots) to wait for JavaScript that may not run the way you expect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical rule:&lt;/strong&gt; if a page’s value is the content itself (docs, marketing copy, product descriptions), indexability should be non-negotiable.&lt;/p&gt;
&lt;p&gt;With these three metrics, you can evaluate any rendering approach without caring what brand of taxonomy it wears.&lt;/p&gt;
&lt;h2 id="static-generation-ssg-the-best-default-for-rarely-changing-content"&gt;Static Generation (SSG): The Best Default for Rarely Changing Content&lt;/h2&gt;
&lt;p&gt;When content rarely changes, static generation isn’t just a “mode”—it’s the simplest path to excellent performance and stable indexing.&lt;/p&gt;
&lt;h3 id="why-it-works"&gt;Why it works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The server isn’t doing work at request time.&lt;/li&gt;
&lt;li&gt;CDN caching is straightforward.&lt;/li&gt;
&lt;li&gt;HTML exists before the first user asks for it.&lt;/li&gt;
&lt;li&gt;Search engines can fetch the final markup immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="where-to-use-it"&gt;Where to use it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Documentation sites&lt;/li&gt;
&lt;li&gt;Blog archives and evergreen guides&lt;/li&gt;
&lt;li&gt;Product pages where updates are infrequent and can be deployed in batches&lt;/li&gt;
&lt;li&gt;Marketing pages with a predictable release cycle&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="concrete-example"&gt;Concrete example&lt;/h3&gt;
&lt;p&gt;Imagine a “How it works” page for a SaaS that updates quarterly. Rendering it as SSG means the CDN can serve the same HTML worldwide with minimal latency. Your users get fast page loads, and your search engine doesn’t need to execute application code just to see the content.&lt;/p&gt;
&lt;h3 id="what-about-but-my-data-changes"&gt;What about “but my data changes”?&lt;/h3&gt;
&lt;p&gt;That’s where ISR comes in. But if the content truly changes rarely, resist the temptation to “dynamic render everything” just because you can. Complexity is a tax.&lt;/p&gt;
&lt;h2 id="isr-and-similar-caching-strategies-freshness-without-sacrificing-speed"&gt;ISR and Similar Caching Strategies: Freshness Without Sacrificing Speed&lt;/h2&gt;
&lt;p&gt;Incremental Static Regeneration (ISR) is often marketed as a distinct revolution. What it’s really offering is a compromise: &lt;strong&gt;static delivery with periodic or on-demand refresh&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="the-practical-goal"&gt;The practical goal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Keep most requests fast (served from cache or pre-rendered HTML)&lt;/li&gt;
&lt;li&gt;Update content eventually so it doesn’t go stale forever&lt;/li&gt;
&lt;li&gt;Avoid full SSR load spikes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="where-it-shines"&gt;Where it shines&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Content that changes more often than quarterly but not every second
&lt;ul&gt;
&lt;li&gt;News tickers that update multiple times daily&lt;/li&gt;
&lt;li&gt;Pricing pages that update occasionally&lt;/li&gt;
&lt;li&gt;Catalog pages where inventory changes are frequent but don’t need perfect “live” accuracy on every request&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="concrete-example-1"&gt;Concrete example&lt;/h3&gt;
&lt;p&gt;A pricing comparison page might update weekly. With ISR, you ship pre-rendered HTML for immediate responsiveness, then regenerate in the background when it’s due. Users still get a fast TTFB, and the page remains indexable because it’s real HTML, not an empty shell.&lt;/p&gt;
&lt;h3 id="the-decision-you-should-make"&gt;The decision you should make&lt;/h3&gt;
&lt;p&gt;Choose your freshness model intentionally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time-based revalidation:&lt;/strong&gt; “Update at most every X minutes.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request-triggered regeneration:&lt;/strong&gt; “Update when a user hits it after it’s stale.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The marketing label matters less than how you control the staleness window—and how you prevent regeneration storms.&lt;/p&gt;
&lt;h2 id="streaming-ssr-when-personalization-and-data-fetching-dominate"&gt;Streaming SSR: When Personalization and Data Fetching Dominate&lt;/h2&gt;
&lt;p&gt;SSR becomes a necessity when each request’s HTML depends on personalized data or complex server-side computation. But naive SSR can be slow if you block on everything before sending bytes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Streaming SSR&lt;/strong&gt; solves a specific pain: it lets you start sending the response early, before the slowest data sources finish.&lt;/p&gt;
&lt;h3 id="why-it-works-1"&gt;Why it works&lt;/h3&gt;
&lt;p&gt;Instead of doing this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fetch A, fetch B, fetch C&lt;/li&gt;
&lt;li&gt;Render the whole page&lt;/li&gt;
&lt;li&gt;Send HTML&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Streaming does this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send the shell/layout immediately&lt;/li&gt;
&lt;li&gt;Stream sections as data arrives&lt;/li&gt;
&lt;li&gt;Finish the response when the last pieces are ready&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="where-it-shines-1"&gt;Where it shines&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Personalized dashboards where the “chrome” can render immediately, but some widgets depend on user-specific data&lt;/li&gt;
&lt;li&gt;Account pages, onboarding flows, and “resume where you left off” pages&lt;/li&gt;
&lt;li&gt;Pages with expensive server calls where you want the user to see progress rather than a blank screen&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="concrete-example-2"&gt;Concrete example&lt;/h3&gt;
&lt;p&gt;A dashboard can often render:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;navigation + page title immediately&lt;/li&gt;
&lt;li&gt;“recent activity” once queried&lt;/li&gt;
&lt;li&gt;“recommendations” when the recommender returns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Streaming means users see the dashboard skeleton early, improving perceived responsiveness even if total server work still takes time.&lt;/p&gt;
&lt;h3 id="the-trade-off"&gt;The trade-off&lt;/h3&gt;
&lt;p&gt;Streaming complicates your HTML and component boundaries. Don’t adopt it because it sounds modern—adopt it because your pages can benefit from &lt;strong&gt;progressive section delivery&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="partial-hydration-and-islands-fixing-the-interactivity-bottleneck"&gt;Partial Hydration and Islands: Fixing the Interactivity Bottleneck&lt;/h2&gt;
&lt;p&gt;Many teams assume “SSR means fast.” It doesn’t—at least not fully. A page can arrive quickly and still feel dead if you hydrate everything, eagerly, with a massive JS bundle.&lt;/p&gt;
&lt;p&gt;That’s where islands and partial hydration earn their keep: you ship HTML for the whole page, but only hydrate interactive regions.&lt;/p&gt;
&lt;h3 id="why-it-works-2"&gt;Why it works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Static or read-only content doesn’t pay for JS.&lt;/li&gt;
&lt;li&gt;Interaction is enabled only where needed.&lt;/li&gt;
&lt;li&gt;Your time to interactive improves because the browser does less work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="where-it-shines-2"&gt;Where it shines&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Content sites with interactive widgets:
&lt;ul&gt;
&lt;li&gt;search boxes&lt;/li&gt;
&lt;li&gt;comment systems&lt;/li&gt;
&lt;li&gt;subscribe forms&lt;/li&gt;
&lt;li&gt;polls&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Documentation or blog pages with a few islands (like code copy buttons or embedded diagrams)&lt;/li&gt;
&lt;li&gt;Commerce pages where some sections require interactivity but the rest is largely static HTML&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="concrete-example-3"&gt;Concrete example&lt;/h3&gt;
&lt;p&gt;A blog post page might have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the article body (no interactivity needed)&lt;/li&gt;
&lt;li&gt;a “copy code” button (small interactive island)&lt;/li&gt;
&lt;li&gt;a newsletter form (interactive island)&lt;/li&gt;
&lt;li&gt;a related posts carousel (interactive island)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you hydrate the entire page as a full client app, you’re wasting time executing code that does nothing on most screens.&lt;/p&gt;
&lt;h3 id="practical-advice"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Treat islands like “budgeted JavaScript.” If an island’s code grows, it should fight for its place. Measure hydration cost. Keep interactive widgets small and lazy-load them when they enter view—if your UX allows it.&lt;/p&gt;
&lt;h2 id="what-about-streaming-resumability-and-everything-else"&gt;What About Streaming, Resumability, and Everything Else?&lt;/h2&gt;
&lt;p&gt;You don’t need to treat every new label as a new dimension of value. Most “advanced” features boil down to one of the same three outcomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Better TTFB&lt;/strong&gt; (send bytes earlier, reduce blocking)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better TTI&lt;/strong&gt; (reduce JS, hydrate less, unblock interaction)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better indexability&lt;/strong&gt; (deliver meaningful HTML reliably)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a “resumability” story doesn’t map to user-visible improvement—or it only helps edge cases you don’t actually have—you should be skeptical. Great engineering deserves skepticism. Marketing deserves it more.&lt;/p&gt;
&lt;p&gt;A useful framing: ask whether the feature changes what the browser receives and when it becomes usable, not whether it sounds impressive at a conference.&lt;/p&gt;
&lt;h2 id="conclusion-choose-rendering-like-a-product-manager-not-like-a-theorist"&gt;Conclusion: Choose Rendering Like a Product Manager, Not Like a Theorist&lt;/h2&gt;
&lt;p&gt;The rendering landscape is noisy because frameworks compete on labels, not outcomes. Your job is to pick the approach that meets user needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use SSG&lt;/strong&gt; for content that doesn’t change much: fast, cacheable, indexable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use ISR/caching regeneration&lt;/strong&gt; when you need freshness without losing CDN speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use streaming SSR&lt;/strong&gt; when personalized or data-heavy pages can benefit from early partial delivery.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use partial hydration / islands&lt;/strong&gt; when interactivity is localized and hydrating everything would slow users down.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stop chasing “server-side” as a badge. Chase &lt;strong&gt;TTFB&lt;/strong&gt;, &lt;strong&gt;TTI&lt;/strong&gt;, and &lt;strong&gt;indexability&lt;/strong&gt;—and let the architecture follow.&lt;/p&gt;</content></item><item><title>The Pragmatist's Guide to Choosing a JavaScript Runtime in 2025</title><link>https://decastro.work/blog/pragmatists-guide-javascript-runtime-2025/</link><pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/pragmatists-guide-javascript-runtime-2025/</guid><description>&lt;p&gt;If you’ve spent even five minutes in a developer chat, you’ve seen it: the runtime debate that turns into a theology lesson. Node, Bun, and Deno aren’t just “different ways to run JavaScript”—they’re different bets about what matters most (ecosystems, speed, and safety). In 2025, the winning move isn’t picking a side. It’s choosing the runtime that lets your team ship the next feature with the least friction.&lt;/p&gt;
&lt;p&gt;This guide is unapologetically practical: what each runtime optimizes for, when that optimization actually matters, and how to decide without getting pulled into a religious war.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve spent even five minutes in a developer chat, you’ve seen it: the runtime debate that turns into a theology lesson. Node, Bun, and Deno aren’t just “different ways to run JavaScript”—they’re different bets about what matters most (ecosystems, speed, and safety). In 2025, the winning move isn’t picking a side. It’s choosing the runtime that lets your team ship the next feature with the least friction.&lt;/p&gt;
&lt;p&gt;This guide is unapologetically practical: what each runtime optimizes for, when that optimization actually matters, and how to decide without getting pulled into a religious war.&lt;/p&gt;
&lt;h2 id="start-with-reality-runtime-choice-affects-your-team-not-your-demo"&gt;Start with reality: runtime choice affects your team, not your demo&lt;/h2&gt;
&lt;p&gt;A runtime is a long-term operational commitment. The demo you write today is rarely the production system you run next year.&lt;/p&gt;
&lt;p&gt;So instead of asking, “Which runtime is best?”, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What code will you run?&lt;/strong&gt; (Existing apps vs. new builds)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What dependencies will you rely on?&lt;/strong&gt; (npm vs. web APIs vs. TypeScript-first)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How will you deploy and operate?&lt;/strong&gt; (hosting support, CI/CD, monitoring)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What constraints do you actually have?&lt;/strong&gt; (security requirements, performance budgets, developer skillset)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the rule I follow: pick the runtime that reduces your &lt;em&gt;total cost of change&lt;/em&gt;—not just the cost of initial setup.&lt;/p&gt;
&lt;h2 id="nodejs-the-ecosystem-gravity-well-and-why-enterprises-keep-defaulting-to-it"&gt;Node.js: the ecosystem gravity well (and why enterprises keep defaulting to it)&lt;/h2&gt;
&lt;p&gt;Node is the safe default because it’s the ecosystem default. If you’ve got an existing codebase, Node is the least surprising choice: most npm packages “just work,” most tutorials assume Node, and most hosting providers support it without special handling.&lt;/p&gt;
&lt;h3 id="when-node-is-the-right-move"&gt;When Node is the right move&lt;/h3&gt;
&lt;p&gt;Choose Node if one or more of these are true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’re migrating or maintaining an existing application.&lt;/li&gt;
&lt;li&gt;You depend on mainstream npm packages (auth, payment SDKs, queues, ORMs, admin tooling).&lt;/li&gt;
&lt;li&gt;Your team’s hiring and training pipeline is optimized for Node.&lt;/li&gt;
&lt;li&gt;Your organization wants the lowest operational uncertainty.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-example-we-already-have-dependencies"&gt;Practical example: “we already have dependencies”&lt;/h3&gt;
&lt;p&gt;Imagine a B2B dashboard that uses a mix of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an ORM ecosystem&lt;/li&gt;
&lt;li&gt;a session/auth library&lt;/li&gt;
&lt;li&gt;multiple UI-adjacent tooling packages&lt;/li&gt;
&lt;li&gt;a few legacy internal plugins&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if Bun or Deno could run the code, you’re effectively betting on compatibility across that dependency graph. Node minimizes that bet.&lt;/p&gt;
&lt;h3 id="the-cost-of-node-and-how-to-manage-it"&gt;The cost of Node (and how to manage it)&lt;/h3&gt;
&lt;p&gt;Node’s downside isn’t “it’s slow.” It’s that performance and developer experience improvements often come as add-ons: bundlers, test runners, linters, and platform-specific optimizations. If you need speed, you’ll assemble it. That can be fine—until you have strict startup latency or serverless cold-start sensitivity.&lt;/p&gt;
&lt;p&gt;If you’re going the Node route, treat performance as a first-class engineering task:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use a modern bundler (so you don’t ship “everything”)&lt;/li&gt;
&lt;li&gt;keep your dependency graph tidy&lt;/li&gt;
&lt;li&gt;measure startup and request latency early, not after launch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bun-the-performance-first-runtime-that-rewards-greenfield-speed"&gt;Bun: the performance-first runtime that rewards greenfield speed&lt;/h2&gt;
&lt;p&gt;Bun’s value proposition in 2025 is simple: it’s built for velocity—both developer velocity (tooling included) and runtime velocity (startup, bundling, execution).&lt;/p&gt;
&lt;p&gt;The important nuance: Bun shines when your project is &lt;strong&gt;new enough&lt;/strong&gt; that you can design around the runtime rather than dragging an old dependency ecosystem into it.&lt;/p&gt;
&lt;h3 id="when-bun-is-the-right-move"&gt;When Bun is the right move&lt;/h3&gt;
&lt;p&gt;Choose Bun if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you’re starting a greenfield service and want to move fast&lt;/li&gt;
&lt;li&gt;you care about startup latency (serverless, edge-adjacent systems, ephemeral workers)&lt;/li&gt;
&lt;li&gt;you want an “all-in-one” dev loop (run, bundle, test—less ceremony)&lt;/li&gt;
&lt;li&gt;your dependencies are compatible with Bun’s world&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-example-serverless-workers-and-quick-deploy-loops"&gt;Practical example: serverless workers and quick deploy loops&lt;/h3&gt;
&lt;p&gt;Say you’re building event-driven background processing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;short-lived compute&lt;/li&gt;
&lt;li&gt;frequent redeploys&lt;/li&gt;
&lt;li&gt;dozens of small worker processes rather than a single long-lived server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this scenario, startup time and deployment speed become product features. Bun’s performance characteristics can turn a “wait a few seconds each redeploy” workflow into something closer to instant feedback.&lt;/p&gt;
&lt;h3 id="the-cost-of-bun-and-how-to-de-risk-it"&gt;The cost of Bun (and how to de-risk it)&lt;/h3&gt;
&lt;p&gt;The risk is not that Bun is “bad.” The risk is that the dependency ecosystem is broader in Node. Bun can run many npm packages, but if you’re heavily invested in niche native modules or very Node-specific tooling, you may hit friction.&lt;/p&gt;
&lt;p&gt;To de-risk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run a compatibility pass on your dependency tree early&lt;/li&gt;
&lt;li&gt;pin versions aggressively&lt;/li&gt;
&lt;li&gt;create a small “canary” service in Bun that covers the riskiest libraries first&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="deno-secure-by-default-typescript-native-and-web-standards-oriented"&gt;Deno: secure-by-default, TypeScript-native, and web-standards oriented&lt;/h2&gt;
&lt;p&gt;Deno takes a different stance: it treats security and modern API design as fundamentals, not optional upgrades. Its default model—permissioned access—forces you to be explicit about what your code can do. It’s also TypeScript-native, which means fewer translation steps between “the language we wrote” and “the language we run.”&lt;/p&gt;
&lt;h3 id="when-deno-is-the-right-move"&gt;When Deno is the right move&lt;/h3&gt;
&lt;p&gt;Choose Deno if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your organization prioritizes security boundaries and least privilege&lt;/li&gt;
&lt;li&gt;TypeScript is non-negotiable (not “we’ll add types later”)&lt;/li&gt;
&lt;li&gt;you want to lean into web-standard APIs (instead of Node’s legacy-inherited patterns)&lt;/li&gt;
&lt;li&gt;you prefer runtime-level controls over relying solely on tooling and process discipline&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-example-permissioned-integrations"&gt;Practical example: permissioned integrations&lt;/h3&gt;
&lt;p&gt;Consider an internal automation service that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;downloads files from a specific URL&lt;/li&gt;
&lt;li&gt;reads from a specific directory&lt;/li&gt;
&lt;li&gt;calls out to an external API&lt;/li&gt;
&lt;li&gt;writes logs to stdout only&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Deno, you can align runtime permissions to the exact behaviors you intend. That turns “someone accidentally added a filesystem call” into something that’s either blocked or requires conscious permission changes.&lt;/p&gt;
&lt;h3 id="the-cost-of-deno-and-how-to-manage-it"&gt;The cost of Deno (and how to manage it)&lt;/h3&gt;
&lt;p&gt;The trade-off is ecosystem fit and patterns. Some Node-targeted packages and assumptions may require adaptation. If your team is deeply embedded in a Node-first workflow, switching to Deno can mean learning new patterns and dealing with compatibility edges.&lt;/p&gt;
&lt;p&gt;To manage this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start with a small service that benefits from Deno’s design goals (security boundaries, TypeScript ergonomics, web APIs)&lt;/li&gt;
&lt;li&gt;standardize how your team structures code and permissions&lt;/li&gt;
&lt;li&gt;invest in internal wrappers around external services so the codebase stays consistent even when underlying libraries differ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-decision-framework-you-can-use-in-a-meeting-without-derailing-into-vibes"&gt;A decision framework you can use in a meeting (without derailing into vibes)&lt;/h2&gt;
&lt;p&gt;When teams get stuck, it’s usually because they’re arguing about “what’s cool” instead of “what’s required.” Use this checklist and make it concrete.&lt;/p&gt;
&lt;h3 id="1-legacy-and-dependency-gravity"&gt;1) Legacy and dependency gravity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mostly new code, small set of dependencies?&lt;/strong&gt; Bun or Deno becomes viable quickly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large existing npm-based system?&lt;/strong&gt; Node is usually the path with the fewest unknowns.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-deployment-model-and-performance-sensitivity"&gt;2) Deployment model and performance sensitivity&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Serverless / ephemeral compute / frequent cold starts?&lt;/strong&gt; Bun often earns a spot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-lived services where latency matters but cold start isn’t brutal?&lt;/strong&gt; Node is often fine with the right optimizations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-security-posture"&gt;3) Security posture&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You want least-privilege enforced at the runtime level?&lt;/strong&gt; Deno is compelling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your security model already relies heavily on infrastructure policies and process discipline?&lt;/strong&gt; Node can still work—just be rigorous.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-typescript-and-developer-workflow"&gt;4) TypeScript and developer workflow&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TypeScript is central to how you build and review?&lt;/strong&gt; Deno’s TypeScript-native approach may reduce friction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your toolchain is already optimized for Node+TypeScript?&lt;/strong&gt; Node remains the pragmatic default.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-team-and-hiring-reality"&gt;5) Team and hiring reality&lt;/h3&gt;
&lt;p&gt;This is the part nobody wants to say out loud, but it matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the team is Node-leaning and the org hires Node engineers, Node reduces onboarding cost.&lt;/li&gt;
&lt;li&gt;If your team is comfortable exploring newer runtimes and you can ramp quickly, Bun or Deno can pay off.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-to-choose-without-making-it-a-forever-decision"&gt;How to choose without making it a forever decision&lt;/h2&gt;
&lt;p&gt;Even with the best framework, you’ll still want a strategy to avoid “big bang” regret.&lt;/p&gt;
&lt;p&gt;Here’s a pragmatic approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pick the runtime for the first service, not the whole company.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a compatibility spike&lt;/strong&gt;: a small proof that runs your highest-risk dependencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure what matters&lt;/strong&gt;: startup time, local iteration speed, and build/test reliability—not abstract benchmarks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codify your defaults&lt;/strong&gt;: linting, formatting, testing, CI pipeline, deployment scripts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leave an exit ramp&lt;/strong&gt;: keep business logic isolated from runtime-specific quirks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re doing this well, you’re not committing to ideology—you’re building institutional muscle.&lt;/p&gt;
&lt;h2 id="conclusion-choose-the-runtime-that-makes-shipping-inevitable"&gt;Conclusion: choose the runtime that makes shipping inevitable&lt;/h2&gt;
&lt;p&gt;In 2025, the “right” JavaScript runtime isn’t the one with the loudest supporters. It’s the one that aligns with your constraints and reduces operational and developer friction.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Node for enterprise continuity and ecosystem certainty.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose Bun for greenfield performance-sensitive projects where speed and tooling matter.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose Deno for security-first, TypeScript-native teams that want web-standard APIs and permissioned execution.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best decision is the one you can explain in terms of trade-offs—and implement without waiting for the internet to agree.&lt;/p&gt;</content></item><item><title>Coding Agents Changed How I Ship Software and There's No Going Back</title><link>https://decastro.work/blog/coding-agents-changed-how-ship-software/</link><pubDate>Mon, 11 Aug 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/coding-agents-changed-how-ship-software/</guid><description>&lt;p&gt;For the last month, I let coding agents touch almost everything: features, bug fixes, refactors, and the boring-but-critical glue that keeps a production codebase healthy. I expected a productivity bump. I didn’t expect my whole workflow to reorganize around delegation. And after enough shipping, I can say this with conviction: agents are transformative for well-defined work—and dangerous when you blur the line between “implementation” and “decision.”&lt;/p&gt;
&lt;h2 id="the-month-i-let-agents-drive"&gt;The Month I Let Agents Drive&lt;/h2&gt;
&lt;p&gt;I didn’t run a controlled experiment. I did what most teams do when tools get promising: I used them where they seemed to fit, then watched what broke, what improved, and what forced me to change my process.&lt;/p&gt;</description><content>&lt;p&gt;For the last month, I let coding agents touch almost everything: features, bug fixes, refactors, and the boring-but-critical glue that keeps a production codebase healthy. I expected a productivity bump. I didn’t expect my whole workflow to reorganize around delegation. And after enough shipping, I can say this with conviction: agents are transformative for well-defined work—and dangerous when you blur the line between “implementation” and “decision.”&lt;/p&gt;
&lt;h2 id="the-month-i-let-agents-drive"&gt;The Month I Let Agents Drive&lt;/h2&gt;
&lt;p&gt;I didn’t run a controlled experiment. I did what most teams do when tools get promising: I used them where they seemed to fit, then watched what broke, what improved, and what forced me to change my process.&lt;/p&gt;
&lt;p&gt;I used three agent-capable workflows in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude Code–style project assistance&lt;/strong&gt;: iterating on changes within a local repo, asking it to propose edits, then verifying.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot CLI–style command workflows&lt;/strong&gt;: quick generation, test writing, and fix suggestions based on the codebase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic “delegation loops”&lt;/strong&gt;: the pattern of “assign task → agent proposes changes → I review/approve → run tests → repeat.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My rule at the start was simple: I would not delegate architectural choices or high-risk behavior. Then, as the week went on, I realized something uncomfortable. The more I trusted the agent, the easier it became to slip into delegating decisions.&lt;/p&gt;
&lt;p&gt;That’s the core lesson: coding agents don’t just write code—they reshape your habits. Your system either keeps them in their lane or quietly turns them into your co-author for things they shouldn’t be touching.&lt;/p&gt;
&lt;h2 id="where-agents-actually-shine-and-why-it-feels-like-cheating"&gt;Where Agents Actually Shine (and Why It Feels Like Cheating)&lt;/h2&gt;
&lt;p&gt;Coding agents are best at tasks that are &lt;strong&gt;mechanically bounded&lt;/strong&gt; and &lt;strong&gt;verifiable&lt;/strong&gt;. In that zone, they feel almost unfairly fast.&lt;/p&gt;
&lt;h3 id="test-writing-that-would-otherwise-steal-hours"&gt;Test writing that would otherwise steal hours&lt;/h3&gt;
&lt;p&gt;The most immediate win was test coverage. When I had a bug with a clear reproduction path, I’d ask the agent to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;locate the relevant code path,&lt;/li&gt;
&lt;li&gt;draft a focused test,&lt;/li&gt;
&lt;li&gt;run it (or at least align it with existing test patterns),&lt;/li&gt;
&lt;li&gt;iterate until it fails for the right reason and then passes after the fix.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Example: a flaky behavior in a background job. The “human” version of this story is messy—hand-wave the setup, write a brittle test, and spend the afternoon debugging the test instead of the code. The agent’s strength was structural: it knew which fixtures and helpers existed in the repo, and it tended to generate tests that matched the project’s existing conventions. The result was quicker feedback loops.&lt;/p&gt;
&lt;h3 id="boilerplate-and-boring-correctness"&gt;Boilerplate and “boring correctness”&lt;/h3&gt;
&lt;p&gt;Agents are excellent at repetitive edits: adding endpoints, wiring parameters, updating DTOs, handling serialization, and mirroring patterns across modules. If your codebase already has a style, agents can replicate it with surprising fidelity.&lt;/p&gt;
&lt;p&gt;I used this for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;adding a new API field across request/response types,&lt;/li&gt;
&lt;li&gt;creating the “same but slightly different” service layer method,&lt;/li&gt;
&lt;li&gt;introducing a new migration with the correct shape and rollback behavior (with my review, obviously).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mechanical-refactors-with-tight-acceptance-criteria"&gt;Mechanical refactors with tight acceptance criteria&lt;/h3&gt;
&lt;p&gt;Refactors are where agent value jumps from “nice” to “can’t go back.” If you can define success as “the build passes, tests pass, and the diff matches this transformation,” agents can do a lot.&lt;/p&gt;
&lt;p&gt;One example: renaming a set of internal functions and updating all call sites. A competent agent reduced the grunt work drastically—then I enforced a review pass that focused on the &lt;strong&gt;diff quality&lt;/strong&gt;, not whether I fully understood every line.&lt;/p&gt;
&lt;p&gt;This is the key: when the work is checkable, agents are brilliant. When it’s not, they become expensive.&lt;/p&gt;
&lt;h2 id="where-agents-get-risky-fast-especially-in-production"&gt;Where Agents Get Risky Fast (Especially in Production)&lt;/h2&gt;
&lt;p&gt;Agents become genuinely dangerous when you ask them to make ambiguous decisions—or when the acceptance criteria are fuzzy.&lt;/p&gt;
&lt;h3 id="architectural-decisions-are-context-work"&gt;Architectural decisions are “context work”&lt;/h3&gt;
&lt;p&gt;In every mature codebase, architecture is less about syntax and more about tradeoffs: performance constraints, operational realities, domain boundaries, and team conventions. Agents don’t have that context unless your prompts and repository structure explicitly encode it.&lt;/p&gt;
&lt;p&gt;I saw this in two failure modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Overconfident design changes&lt;/strong&gt;: an agent proposed a “cleaner” abstraction that ignored existing boundaries and introduced new coupling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implicit assumptions about invariants&lt;/strong&gt;: it changed logic in a way that compiled fine but subtly violated how the system behaves under load or in edge cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In both cases, the problem wasn’t that the agent was incompetent. The problem was that the agent optimized for what was easiest to modify, not what was safest to alter.&lt;/p&gt;
&lt;h3 id="subtle-bugs-hide-behind-looks-correct"&gt;Subtle bugs hide behind “looks correct”&lt;/h3&gt;
&lt;p&gt;The scariest failures aren’t obvious test breakages. They’re logic changes that still pass tests but alter behavior in ways your tests don’t cover.&lt;/p&gt;
&lt;p&gt;This is why I stopped delegating anything that required deep reasoning beyond what tests capture. If I couldn’t define the behavior precisely, the agent didn’t get a free hand. I learned to treat “it should work” as unacceptable as an acceptance criterion.&lt;/p&gt;
&lt;h3 id="context-beyond-the-codebase-doesnt-exist"&gt;Context beyond the codebase doesn’t exist&lt;/h3&gt;
&lt;p&gt;Agents can only infer what they see. If your system depends on operational knowledge—timeouts chosen for production, rate limits negotiated with upstream partners, deployment quirks, feature flags with historical baggage—agents will guess unless you tell them.&lt;/p&gt;
&lt;p&gt;If you’ve ever regretted a “quick fix” that only worked in development, you already know the danger zone.&lt;/p&gt;
&lt;h2 id="my-delegation-pattern-break-work-into-agent-sized-chunks-and-review-aggressively"&gt;My Delegation Pattern: Break Work Into Agent-Sized Chunks and Review Aggressively&lt;/h2&gt;
&lt;p&gt;The optimal workflow I landed on is not “give agent tasks and hope.” It’s a delegation discipline.&lt;/p&gt;
&lt;h3 id="1-convert-vague-work-into-concrete-subtasks"&gt;1) Convert vague work into concrete subtasks&lt;/h3&gt;
&lt;p&gt;Instead of: “Refactor this module for better design.”&lt;/p&gt;
&lt;p&gt;Use: “Rename these functions, update call sites, keep public API unchanged. Ensure no behavior changes. Add/adjust tests covering current behavior.”&lt;/p&gt;
&lt;p&gt;Agents love bounded tasks because they produce bounded diffs. Humans should love bounded tasks because they reduce review risk.&lt;/p&gt;
&lt;h3 id="2-force-a-verification-loop-every-time"&gt;2) Force a verification loop every time&lt;/h3&gt;
&lt;p&gt;I treated each agent output as a proposed patch, not an implementation. After every meaningful change, I ran:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the test suite (or the relevant subset),&lt;/li&gt;
&lt;li&gt;lint/type checks,&lt;/li&gt;
&lt;li&gt;and—when behavior mattered—targeted checks around the risky path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when the agent claimed the change “should pass,” I didn’t trust it. The point of agents is speed; the point of your workflow is correctness.&lt;/p&gt;
&lt;h3 id="3-review-the-diff-like-a-publication-editor"&gt;3) Review the diff like a publication editor&lt;/h3&gt;
&lt;p&gt;My review strategy shifted. I stopped reading every line with equal attention and started looking for patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are there silent changes to error handling?&lt;/li&gt;
&lt;li&gt;Did it modify conditional logic or ordering?&lt;/li&gt;
&lt;li&gt;Does it introduce new dependencies or broaden coupling?&lt;/li&gt;
&lt;li&gt;Are tests added or just updated?&lt;/li&gt;
&lt;li&gt;Are names consistent with intent, or just mechanically transformed?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the agent writes boilerplate, the diff can be huge but low-risk. When it touches control flow, concurrency, caching, permissions, or data transformation, the diff is small but high-risk. I adjusted scrutiny accordingly.&lt;/p&gt;
&lt;h3 id="4-ask-for-smaller-artifacts-not-whole-solutions"&gt;4) Ask for smaller artifacts, not whole solutions&lt;/h3&gt;
&lt;p&gt;If you ask an agent to “implement the feature,” you’ll often get a stitched-together solution that’s hard to review.&lt;/p&gt;
&lt;p&gt;Instead, ask for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Propose the exact test cases first.”&lt;/li&gt;
&lt;li&gt;“Draft the interface changes only.”&lt;/li&gt;
&lt;li&gt;“Show the diff for wiring before implementing business logic.”&lt;/li&gt;
&lt;li&gt;“Enumerate the invariants you’re assuming, then wait for approval.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes the agent’s thinking observable. That’s crucial.&lt;/p&gt;
&lt;h2 id="a-practical-playbook-for-your-next-iteration"&gt;A Practical Playbook for Your Next Iteration&lt;/h2&gt;
&lt;p&gt;If you’re adopting coding agents tomorrow, here’s how I’d start without creating chaos.&lt;/p&gt;
&lt;h3 id="use-agents-for"&gt;Use agents for:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Test creation and expansion&lt;/strong&gt; (especially around known failing cases)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boilerplate and repetitive wiring&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mechanical refactors with explicit transformation rules&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Small, scoped features where behavior is already well-understood&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation updates tied to concrete code changes&lt;/strong&gt; (with review)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="avoid-agents-for"&gt;Avoid agents for:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture and boundary redesign&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission/security model changes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance-critical logic without targeted tests&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge-case behavior you don’t already have solid coverage for&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Any “global” change that depends on tribal knowledge&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="establish-a-merge-gate"&gt;Establish a “merge gate”&lt;/h3&gt;
&lt;p&gt;Even a lightweight gate helps. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;agent-generated code must be accompanied by tests for the changed behavior,&lt;/li&gt;
&lt;li&gt;no agent changes land without running the suite in CI (even if it feels slow),&lt;/li&gt;
&lt;li&gt;and high-risk areas require an explicit human approval checklist.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how you keep the speed without importing the risk.&lt;/p&gt;
&lt;h2 id="the-real-outcome-productivity-but-also-better-engineering-taste"&gt;The Real Outcome: Productivity, But Also Better Engineering Taste&lt;/h2&gt;
&lt;p&gt;Here’s the part that surprised me: delegation didn’t just make me faster. It made me more selective.&lt;/p&gt;
&lt;p&gt;Once you can move quickly, you stop spending time on the drudgery that doesn’t differentiate you as an engineer. That time goes to the work that actually matters: defining the problem, enforcing invariants, tightening acceptance criteria, and reviewing carefully where it counts.&lt;/p&gt;
&lt;p&gt;And yes—agents can make you sloppy if you let them. They will tempt you to offload thinking. But if you treat them like a powerful junior who needs boundaries, you get leverage.&lt;/p&gt;
&lt;p&gt;The strongest teams I can imagine won’t be the ones that “use agents.” They’ll be the ones that build a delegation culture: agent-sized chunks, explicit verification, and aggressive review. Master that pattern and you don’t just ship faster—you ship cleaner.&lt;/p&gt;
&lt;h2 id="conclusion-agents-are-a-multiplier-not-an-autopilot"&gt;Conclusion: Agents Are a Multiplier, Not an Autopilot&lt;/h2&gt;
&lt;p&gt;Coding agents changed how I ship software because they collapse the time between idea and implemented diff—especially for tests, boilerplate, and mechanical refactors. But they’re genuinely dangerous when tasks are ambiguous or when architecture-level decisions creep in.&lt;/p&gt;
&lt;p&gt;So my takeaway is simple and non-negotiable: &lt;strong&gt;break work into agent-sized chunks and review aggressively.&lt;/strong&gt; Treat agents as accelerators for verifiable implementation, not as authorities for system design. If you do that, there’s no going back.&lt;/p&gt;</content></item><item><title>The Developer Tool Landscape Is Consolidating Around AI-Native Platforms</title><link>https://decastro.work/blog/developer-tool-landscape-consolidating-ai-native/</link><pubDate>Tue, 05 Aug 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/developer-tool-landscape-consolidating-ai-native/</guid><description>&lt;p&gt;For years, “AI in the IDE” has meant bolting a chatbot onto a familiar workflow. But the editor is no longer the same interface—it’s becoming an AI workbench. Cursor, Windsurf, and Zed aren’t merely adding smarter completions to existing tooling; they’re reshaping how you reason about a codebase by making AI a first-class interaction model. And that shift is quietly but decisively consolidating mindshare away from the extension-first approach that powered VS Code’s rise.&lt;/p&gt;</description><content>&lt;p&gt;For years, “AI in the IDE” has meant bolting a chatbot onto a familiar workflow. But the editor is no longer the same interface—it’s becoming an AI workbench. Cursor, Windsurf, and Zed aren’t merely adding smarter completions to existing tooling; they’re reshaping how you reason about a codebase by making AI a first-class interaction model. And that shift is quietly but decisively consolidating mindshare away from the extension-first approach that powered VS Code’s rise.&lt;/p&gt;
&lt;h2 id="from-ai-added-to-ai-native-the-real-change"&gt;From “AI-added” to “AI-native”: the real change&lt;/h2&gt;
&lt;p&gt;VS Code’s architecture made it easy to win adoption: a clean editor core, extensibility everywhere, and a plugin ecosystem that covered everything from linting to Git workflows. AI followed the same path. Copilot-style experiences fit neatly into that model—suggestions, inline chat, and helper features implemented as extensions that sit on top of the editor.&lt;/p&gt;
&lt;p&gt;AI-native platforms take a different stance: they treat AI as the operating layer of the product rather than a feature. That difference shows up in three places:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context isn’t an afterthought.&lt;/strong&gt; AI-first editors are designed from the beginning to understand and use broader project state, not just the current buffer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edits aren’t limited to suggestions.&lt;/strong&gt; They’re framed as intentional transformations—apply changes across multiple files, keep structure consistent, and verify outcomes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The workflow includes agency.&lt;/strong&gt; Instead of “ask → get completion,” you get “plan → perform → iterate,” sometimes with agent-like behavior that can execute multi-step tasks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why the headline isn’t “AI makes edits smarter.” It’s “AI changes what the editor is for.”&lt;/p&gt;
&lt;h2 id="multi-file-context-is-where-the-illusion-breaks"&gt;Multi-file context is where the illusion breaks&lt;/h2&gt;
&lt;p&gt;If you’ve ever tried to refactor something non-trivial with an AI that only sees what you’re currently looking at, you’ve felt the limitation: answers get vague, changes become inconsistent, or the tool confidently suggests edits that don’t match surrounding usage patterns.&lt;/p&gt;
&lt;p&gt;AI-native editors build a habit into the UI: when you ask for a change, the system assumes the relevant context spans the repository. Cursor and Windsurf, for example, are built around the idea that the assistant can reference the project at scale to implement a coherent edit rather than producing a patch confined to the visible file. Zed leans into the same “fast, fluid, repository-aware” philosophy with an emphasis on performance and responsiveness that keeps the loop tight.&lt;/p&gt;
&lt;p&gt;Practical example: imagine you want to “rename &lt;code&gt;AuthService&lt;/code&gt; to &lt;code&gt;AuthenticationService&lt;/code&gt;, update imports, fix references, and adjust tests.” In a Copilot-style workflow, you might get a series of isolated suggestions—or you’ll be forced to manually manage the blast radius. In an AI-native workflow, you can often describe the intent once and let the editor coordinate the edit across the codebase, then show you a diff you can review.&lt;/p&gt;
&lt;p&gt;The key isn’t just that it can see more. It’s that the editor &lt;em&gt;uses&lt;/em&gt; that context to structure the work.&lt;/p&gt;
&lt;h2 id="natural-language-project-wide-edits-beat-autocomplete-for-code"&gt;Natural-language project-wide edits beat “autocomplete for code”&lt;/h2&gt;
&lt;p&gt;The marketing term for AI in an editor is often “autocomplete.” But autocomplete is a single-shot tool. It assumes the user’s intent is already fully expressed in the current cursor position.&lt;/p&gt;
&lt;p&gt;AI-native editors move the intent boundary outward. You state the goal in natural language, and the editor translates that goal into a series of code changes. That sounds obvious, but the UX implications are profound:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You stop thinking in keystrokes and start thinking in outcomes.&lt;/li&gt;
&lt;li&gt;You treat the codebase like a document you can transform, not a buffer you can fill.&lt;/li&gt;
&lt;li&gt;You rely on reviewable changes rather than trusting each suggestion blindly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider an incremental modernization task: “Migrate this module from callbacks to async/await, update all call sites, and ensure error handling remains consistent.” In an autocomplete-centric flow, you’ll likely patch one function at a time and chase compile errors manually. In an AI-native flow, the editor can perform the transformation across multiple files, keep the error-handling pattern consistent, and present a coherent set of changes for you to validate.&lt;/p&gt;
&lt;p&gt;This is where the extension model often runs out of runway. Copilot-as-plugin can provide assistance, but it usually can’t fully replace the editor’s conceptual model of what a “change” is.&lt;/p&gt;
&lt;h2 id="agent-mode-shifts-the-burden-from-you-to-the-tool"&gt;Agent-mode shifts the burden from you to the tool&lt;/h2&gt;
&lt;p&gt;There’s a reason “agent mode” is showing up everywhere: it’s the closest thing to removing busywork from the development loop. The best implementations don’t just generate text—they can carry out tasks with some autonomy, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;searching the codebase for usages,&lt;/li&gt;
&lt;li&gt;making a sequence of changes,&lt;/li&gt;
&lt;li&gt;running checks (or preparing changes that will pass),&lt;/li&gt;
&lt;li&gt;and iterating based on feedback.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when agent-mode features aren’t fully autonomous in the strictest sense, the workflow changes. Instead of repeatedly prompting with “what next?”, you prompt with “do the task.” The editor becomes a collaborator that can coordinate steps.&lt;/p&gt;
&lt;p&gt;Here’s a concrete workflow that teams like because it reduces cognitive overhead: “Create a new API endpoint at &lt;code&gt;/v2/users&lt;/code&gt; that mirrors the existing &lt;code&gt;/v1/users&lt;/code&gt; logic, but uses the new repository layer; update routing, add the handler, and adjust tests.” In an AI-native editor, you’re not stuck translating your intent into a sequence of commands. The tool interprets the task as a set of coordinated edits.&lt;/p&gt;
&lt;p&gt;And crucially: you still review the diff. Agent-mode isn’t a surrender button—it’s a way to compress the time between “idea” and “candidate implementation.”&lt;/p&gt;
&lt;h2 id="why-vs-codes-extension-model-cant-fully-absorb-the-paradigm-shift"&gt;Why VS Code’s extension model can’t fully absorb the paradigm shift&lt;/h2&gt;
&lt;p&gt;VS Code remains a phenomenal platform, and it will keep attracting developers for good reasons. But the extension model has a ceiling when the core interaction model changes.&lt;/p&gt;
&lt;p&gt;When the AI becomes the primary interface, you need deep integration with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how the editor assembles context,&lt;/li&gt;
&lt;li&gt;how it plans and applies multi-step edits,&lt;/li&gt;
&lt;li&gt;how it presents diffs in a reviewable way,&lt;/li&gt;
&lt;li&gt;how it manages long-running tasks,&lt;/li&gt;
&lt;li&gt;and how it synchronizes UI state with “work in progress.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Extensions can do a lot, but they often operate at the edges of the editor. They can add chat panels, commands, and helpers. They can’t easily re-architect the editor’s center of gravity around AI as an editing engine.&lt;/p&gt;
&lt;p&gt;Put differently: VS Code’s superpower is customization by composition. AI-native tools are optimizing for experience by design. That difference matters when the product’s value is increasingly about &lt;em&gt;how&lt;/em&gt; work gets done, not just whether features are available.&lt;/p&gt;
&lt;p&gt;This is the same pattern we saw with earlier editor transitions: the winner isn’t merely the tool with the most plugins; it’s the tool with the most frictionless mental model for the modern task.&lt;/p&gt;
&lt;h2 id="the-consolidation-dynamic-trust-speed-and-iteration-loops"&gt;The consolidation dynamic: trust, speed, and iteration loops&lt;/h2&gt;
&lt;p&gt;Why are Cursor, Windsurf, and Zed “eating lunch” instead of VS Code simply absorbing the features? Because developers aren’t choosing tools on capability alone—they’re choosing tools on loop time.&lt;/p&gt;
&lt;p&gt;An AI-native editor compresses the loop from:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;understand intent →&lt;/li&gt;
&lt;li&gt;gather context →&lt;/li&gt;
&lt;li&gt;propose changes →&lt;/li&gt;
&lt;li&gt;let the user review →&lt;/li&gt;
&lt;li&gt;iterate—&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;into something that feels immediate. If the AI needs five minutes and three manual steps to produce a coherent multi-file refactor, the “assistant” becomes another job. If it produces a reviewable change in seconds, the assistant becomes infrastructure.&lt;/p&gt;
&lt;p&gt;Two practical selection criteria you can use right now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Can it edit across the repo with a single intent?&lt;/strong&gt; Try a realistic refactor on your own project. If it’s mainly confined to the current file, you’re still in the AI-added era.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does the diff feel coherent?&lt;/strong&gt; The best tools don’t just output code—they output a change you can safely review. Look for stable formatting, consistent naming, and minimal unnecessary churn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s also a social factor: when teams standardize on AI-native workflows, code review expectations change. Instead of reviewing “suggested lines,” you review “agent-produced transformations.” That shifts how developers collaborate—and consolidates tooling accordingly.&lt;/p&gt;
&lt;h2 id="what-to-do-if-youre-staying-on-vs-code-or-if-youre-migrating"&gt;What to do if you’re staying on VS Code (or if you’re migrating)&lt;/h2&gt;
&lt;p&gt;If you’re committed to VS Code—fair—don’t treat this as a declaration of defeat. You can still capture some of the benefits by focusing on workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use AI features that support repository context and multi-file changes rather than purely inline suggestions.&lt;/li&gt;
&lt;li&gt;Prefer tools and workflows that produce reviewable diffs for larger changes.&lt;/li&gt;
&lt;li&gt;Build habits around “describe the transformation, then verify the diff,” not “accept suggestions until it compiles.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re migrating, don’t start with “which editor has the best chat.” Start with: “Can I do the three tasks I do most often with fewer steps?” For many developers, that’s refactors, migrations, and test updates. AI-native editors win when those tasks become compressible.&lt;/p&gt;
&lt;p&gt;Also, set guardrails. Even the best AI-first experiences can overshoot. Make “review before apply” non-negotiable, and require tests or linters for anything that touches critical paths.&lt;/p&gt;
&lt;h2 id="conclusion-the-editor-is-becoming-a-co-author-not-a-text-box"&gt;Conclusion: the editor is becoming a co-author, not a text box&lt;/h2&gt;
&lt;p&gt;The most important shift in the IDE market isn’t that AI can write code. It’s that the editing experience is being rebuilt around AI as an interactive engine: multi-file context, natural-language transformations, and agent-style task execution are changing what developers expect from their tools.&lt;/p&gt;
&lt;p&gt;VS Code’s extension ecosystem will remain influential, but AI-native platforms are redefining the baseline. The consolidation isn’t just about features—it’s about the workflow that feels inevitable once you’ve tried it.&lt;/p&gt;</content></item><item><title>Understanding WASM Components Will Make You a Better Engineer</title><link>https://decastro.work/blog/understanding-wasm-components-better-engineer/</link><pubDate>Wed, 30 Jul 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/understanding-wasm-components-better-engineer/</guid><description>&lt;p&gt;If you’ve ever stitched together systems with a patchwork of ABIs, you already understand the pain that the WebAssembly component model is trying to solve. The component model is the missing “adult supervision” layer for WebAssembly: a standardized way for separately compiled modules—written in different languages, built by different teams—to talk to each other without brittle, bespoke glue code. Most developers haven’t heard of it yet, but it’s the closest thing the industry has seen to a universal ABI in decades.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever stitched together systems with a patchwork of ABIs, you already understand the pain that the WebAssembly component model is trying to solve. The component model is the missing “adult supervision” layer for WebAssembly: a standardized way for separately compiled modules—written in different languages, built by different teams—to talk to each other without brittle, bespoke glue code. Most developers haven’t heard of it yet, but it’s the closest thing the industry has seen to a universal ABI in decades.&lt;/p&gt;
&lt;p&gt;And once you understand it, you’ll write better systems even outside WebAssembly—because you’ll stop treating interfaces as afterthoughts.&lt;/p&gt;
&lt;h2 id="the-abi-problem-we-never-truly-fixed"&gt;The ABI problem we never truly fixed&lt;/h2&gt;
&lt;p&gt;An ABI (Application Binary Interface) is the contract between compiled code and the runtime: calling conventions, data layout, how values cross boundaries, what “string” means at the machine boundary, and who owns memory. For a long time, the industry has punted on universal solutions and instead invented a thousand variants:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You export functions from one environment and call them from another using a specific FFI toolchain.&lt;/li&gt;
&lt;li&gt;You define a C struct and hope everyone interprets it the same way.&lt;/li&gt;
&lt;li&gt;You serialize everything to bytes, because nothing else is portable enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This works, but it’s expensive—in complexity, in time, and in reliability. It also forces teams into a tradeoff: either constrain your integration surface (so you can keep it simple) or pay an integration tax (so you can keep it flexible).&lt;/p&gt;
&lt;p&gt;The component model targets the integration tax head-on by standardizing the shape of &lt;em&gt;interfaces&lt;/em&gt;, not just the ability to run code.&lt;/p&gt;
&lt;h2 id="what-components-actually-give-you"&gt;What “components” actually give you&lt;/h2&gt;
&lt;p&gt;A WebAssembly module is great at running code in isolation. But modules weren’t designed as a universal interop language; they’re primarily an execution unit. The component model builds a higher-level abstraction on top: a &lt;em&gt;component&lt;/em&gt; exposes an interface, and other components can import and use that interface in a structured way.&lt;/p&gt;
&lt;p&gt;The practical idea is simple: compile your library in one language, export a well-defined interface, and import it from another language without you manually writing low-level glue.&lt;/p&gt;
&lt;p&gt;Where this becomes exciting is the difference between “it runs” and “it composes.”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plain WebAssembly modules&lt;/strong&gt; often require you to agree on calling conventions and data representation out-of-band.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Components&lt;/strong&gt; define a standard interface model so cross-language calls can be mediated consistently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it like moving from “RPC over raw sockets” to “RPC over an IDL with agreed semantics.” You still call a function, but you don’t have to babysit every byte.&lt;/p&gt;
&lt;h2 id="a-concrete-example-rust-image-processing-into-python"&gt;A concrete example: Rust image processing into Python&lt;/h2&gt;
&lt;p&gt;Let’s make the dream tangible. Suppose you have an image processing library written in Rust that performs resizing and filtering. You want to use it inside a Python application—without writing C shims, hand-rolling marshaling logic, or juggling memory ownership.&lt;/p&gt;
&lt;p&gt;With a component-based approach, the Rust side exports an interface that describes what it needs and what it returns—at the component level. On the Python side, you import that component and call the functions as if they were “normal” library calls, except the runtime mediates the data crossing the boundary.&lt;/p&gt;
&lt;p&gt;In practical terms, what changes is where the complexity lives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You define an interface once (in the component world), not a one-off FFI binding for every consumer.&lt;/li&gt;
&lt;li&gt;You don’t need to rebuild or reinterpret data layouts in every integration path.&lt;/li&gt;
&lt;li&gt;The runtime has enough information to convert between representations appropriately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you don’t use Python specifically, the pattern is the same: component boundaries let you treat compiled code from other languages as reusable &lt;em&gt;dependencies&lt;/em&gt;, not fragile experiments.&lt;/p&gt;
&lt;h2 id="universal-abi-on-the-web-the-server-and-the-edge"&gt;Universal ABI on the web, the server, and the edge&lt;/h2&gt;
&lt;p&gt;The “universal” part matters. If this model only works in one environment, it’s just another platform-specific solution. The component model is designed to be portable across runtimes and deployment targets, which is exactly what you want if you’re building systems that span:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The browser&lt;/strong&gt; (where you want safe, efficient execution)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The server&lt;/strong&gt; (where you want composable services and fast startup)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The edge&lt;/strong&gt; (where you want predictable resource usage and fast distribution)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The value proposition is obvious: ship one artifact—one component interface—and reuse it everywhere instead of rebuilding bindings per environment.&lt;/p&gt;
&lt;p&gt;This is why engineers who understand components tend to design interfaces more like products than like one-off glue. You stop thinking “how do I call it from here?” and start thinking “how will other systems call it reliably?”&lt;/p&gt;
&lt;h2 id="why-you-should-care-even-if-you-never-write-components-directly"&gt;Why you should care even if you never write components directly&lt;/h2&gt;
&lt;p&gt;You might not author component interfaces tomorrow. You might just consume them. Still, understanding the model changes how you engineer.&lt;/p&gt;
&lt;h3 id="1-youll-design-safer-boundaries"&gt;1) You’ll design safer boundaries&lt;/h3&gt;
&lt;p&gt;When you define an interface, you’re forced to decide questions that are easy to ignore until they break:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are the inputs and their types?&lt;/li&gt;
&lt;li&gt;Who owns memory?&lt;/li&gt;
&lt;li&gt;How do errors propagate?&lt;/li&gt;
&lt;li&gt;What is synchronous vs. asynchronous behavior?&lt;/li&gt;
&lt;li&gt;What happens with large buffers (images, files, data streams)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Components push you toward explicit semantics. That reduces “mystery failures” where one side assumes a different representation than the other.&lt;/p&gt;
&lt;h3 id="2-youll-stop-over-serializing"&gt;2) You’ll stop over-serializing&lt;/h3&gt;
&lt;p&gt;A common pattern in cross-language integration is to convert everything to bytes or JSON. It’s robust, but it’s also slow and noisy. With a standardized interface model, you have a path to more direct calls and fewer transformations—so your system stays both fast and legible.&lt;/p&gt;
&lt;p&gt;You still may serialize when it’s the right tradeoff, but you’ll do it deliberately rather than by default.&lt;/p&gt;
&lt;h3 id="3-youll-build-libraries-that-are-easier-to-reuse"&gt;3) You’ll build libraries that are easier to reuse&lt;/h3&gt;
&lt;p&gt;Reusable code doesn’t only mean “it’s correct.” It means “other people can use it without heroic effort.” Components help because they turn your export surface into something that other language ecosystems can reliably import.&lt;/p&gt;
&lt;p&gt;In other words: you ship &lt;em&gt;an API contract&lt;/em&gt;, not just &lt;em&gt;an implementation&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="shipping-today-preview-runtimes-and-real-adoption"&gt;Shipping today: preview runtimes and real adoption&lt;/h2&gt;
&lt;p&gt;The most encouraging part is that this isn’t trapped in theory. Preview runtimes and tooling are already exploring component support, and the ecosystem is actively iterating on how developers package and consume these interfaces.&lt;/p&gt;
&lt;p&gt;So the right move isn’t waiting for perfection. It’s building fluency now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn the conceptual model: exports, imports, and interface mediation.&lt;/li&gt;
&lt;li&gt;Track what your runtime supports today.&lt;/li&gt;
&lt;li&gt;Prototype with one realistic integration—like the Rust image library example.&lt;/li&gt;
&lt;li&gt;Measure the pain you eliminate: less glue code, fewer marshaling bugs, faster iteration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re thinking “but we’ll have to learn a new system,” you’re right. But the trade is worth it: you’re learning the thing that will reduce integration overhead for years, not just for one stack.&lt;/p&gt;
&lt;h2 id="conclusion-the-best-engineers-treat-interfaces-as-first-class"&gt;Conclusion: The best engineers treat interfaces as first-class&lt;/h2&gt;
&lt;p&gt;WebAssembly’s component model is more than a spec feature—it’s a mindset upgrade. It’s an attempt to standardize the contract between compiled worlds, so libraries become plug-and-play dependencies instead of bespoke integration projects.&lt;/p&gt;
&lt;p&gt;If you want to be a better engineer, stop treating interfaces as the messy last step. Learn how components model boundaries, and you’ll design cleaner APIs, fewer glue layers, and more reusable systems—whether you’re working inside WebAssembly or not.&lt;/p&gt;</content></item><item><title>The Next.js Backlash Is Overblown (But Not Entirely Wrong)</title><link>https://decastro.work/blog/nextjs-backlash-overblown-not-entirely-wrong/</link><pubDate>Fri, 18 Jul 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/nextjs-backlash-overblown-not-entirely-wrong/</guid><description>&lt;p&gt;The internet loves a villain, and lately that villain has been Next.js. The loudest criticisms—vendor lock-in, App Router complexity, confusing caching defaults, and a release cadence that feels like it never stops—are real enough to deserve attention. But the conclusion people jump to (“Next.js is broken” or “it’s all hype”) is too lazy. The truth is more interesting: Next.js has genuine architectural wins, and the discourse is partly right about where teams get burned.&lt;/p&gt;</description><content>&lt;p&gt;The internet loves a villain, and lately that villain has been Next.js. The loudest criticisms—vendor lock-in, App Router complexity, confusing caching defaults, and a release cadence that feels like it never stops—are real enough to deserve attention. But the conclusion people jump to (“Next.js is broken” or “it’s all hype”) is too lazy. The truth is more interesting: Next.js has genuine architectural wins, and the discourse is partly right about where teams get burned.&lt;/p&gt;
&lt;p&gt;Let’s separate what’s performative from what’s actionable—because if you’re building with Next.js right now, you should care about both.&lt;/p&gt;
&lt;h2 id="what-nextjs-gets-right-modern-rendering-as-a-product-not-a-hack"&gt;What Next.js Gets Right: Modern Rendering as a Product, Not a Hack&lt;/h2&gt;
&lt;p&gt;A lot of the heat around Next.js ignores the core reason it’s so dominant: it made modern React rendering patterns feel approachable. React Server Components (RSC), streaming server-side rendering, and a pragmatic approach to hybrid static/dynamic pages are not just buzzwords—they’re architectural options you can use without reinventing the tooling each time.&lt;/p&gt;
&lt;p&gt;Take streaming SSR. Before frameworks mainstreamed it, teams either settled for slower first paint or built brittle custom stacks. Next.js’s model lets you stream content to the browser while the server continues working. That matters for user experience and perceived performance, especially for pages that depend on multiple asynchronous data sources. Even if you never optimize “like a performance blog,” streaming gives you headroom.&lt;/p&gt;
&lt;p&gt;Then there’s incremental static regeneration (ISR). The basic promise is simple: generate pages ahead of time, but refresh them on a schedule or on demand. Practically, this gives you an escape hatch from the binary choice between “all static” and “all dynamic.” For marketing sites, documentation, and content-heavy apps, ISR is a sweet spot. You can keep most pages fast and cacheable while still updating without redeploying everything.&lt;/p&gt;
&lt;p&gt;My opinionated takeaway: Next.js’s success isn’t just ecosystem gravity. It’s that it turned serious rendering primitives into a developer workflow. That’s the part the backlash keeps skipping.&lt;/p&gt;
&lt;h2 id="the-vercel-lock-in-claim-not-wrong-just-misframed"&gt;The Vercel Lock-In Claim: Not Wrong, Just Misframed&lt;/h2&gt;
&lt;p&gt;The “vendor lock-in” argument usually appears in two flavors.&lt;/p&gt;
&lt;p&gt;The first is emotional: “If you use Next.js on Vercel, you’re trapped.” That’s not quite how it works. Next.js itself is a framework; you can run it elsewhere. But the second flavor is more concrete: Vercel’s platform features are so smooth—caching behavior, deployment workflow, edge/runtime integration—that they can encourage coupling to the assumptions of that environment.&lt;/p&gt;
&lt;p&gt;Here’s a practical way to evaluate lock-in rather than doomscrolling tweets: identify which parts of your app depend on platform-specific behavior.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you rely heavily on Vercel’s caching knobs and runtime behaviors, you may need a migration plan that explicitly tests those assumptions on your target host.&lt;/li&gt;
&lt;li&gt;If you use route handlers and server actions in ways that map neatly to Vercel’s execution model, you might face subtle behavior differences elsewhere.&lt;/li&gt;
&lt;li&gt;If your CI/CD pipeline, environment variables, secrets management, or build caching are all tightly Vercel-shaped, you’ve locked in operational convenience—even if the code remains portable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So yes: the criticism has teeth, but it’s not fatal by default. Treat deployment platform features as optional enhancements, not your entire strategy. If your architecture depends on “works only when hosted like this,” that’s not a Next.js problem—it’s an engineering risk you chose.&lt;/p&gt;
&lt;h2 id="app-router-complexity-the-problem-isnt-app-routerits-mental-load"&gt;App Router Complexity: The Problem Isn’t App Router—It’s Mental Load&lt;/h2&gt;
&lt;p&gt;The App Router critique is often presented as “it’s too complex.” That’s too vague to help. What’s actually happening is that the App Router pushes you into a different set of mental models than the old pages-based approach.&lt;/p&gt;
&lt;p&gt;Instead of a single convention for routing and rendering, you now juggle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server and client component boundaries&lt;/li&gt;
&lt;li&gt;Data fetching patterns (and their impact on caching)&lt;/li&gt;
&lt;li&gt;Streaming and suspense semantics&lt;/li&gt;
&lt;li&gt;Layouts, nested routes, and route-level configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve been building React apps for years, the conceptual shift feels manageable at first—and then the edge cases arrive.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine a team that moves a component into the client boundary to “fix a bug,” and suddenly the page stops streaming as expected or data fetching behavior changes. The app still works, but its performance characteristics and caching are no longer what the team thinks they are. The code compiles; the mental model doesn’t.&lt;/p&gt;
&lt;p&gt;The best practical advice I can offer is to enforce architectural boundaries early:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make “server by default” a team norm.&lt;/li&gt;
&lt;li&gt;Treat client components as a scarce resource.&lt;/li&gt;
&lt;li&gt;Document where data fetching happens and what caching policy is expected.&lt;/li&gt;
&lt;li&gt;Add lightweight code review checklists for component boundary changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;App Router isn’t inherently wrong. It rewards teams that think clearly about boundaries. The backlash exists because many teams adopted it without also upgrading their engineering discipline.&lt;/p&gt;
&lt;h2 id="caching-defaults-this-is-the-criticism-with-the-most-real-world-bite"&gt;Caching Defaults: This Is the Criticism With the Most Real-World Bite&lt;/h2&gt;
&lt;p&gt;If you want one place where the backlash stops being noise and starts being useful, it’s caching. Next.js can behave in ways that feel unintuitive—especially when teams assume “it works like React” or “it works like plain Node SSR.”&lt;/p&gt;
&lt;p&gt;Two recurring pain points show up in real projects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Caching policy is not always obvious from the component tree.&lt;/strong&gt; When caching is tied to route-level or fetch-level semantics, it’s easy for developers to misattribute cause and effect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The defaults can be surprising.&lt;/strong&gt; Developers may deploy something that appears correct, then later notice updates not showing up when they expect, or differences between local behavior and production behavior.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To make this practical, here’s what good teams do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decide up front which routes should be “cacheable,” “revalidated,” or “dynamic.”&lt;/li&gt;
&lt;li&gt;For each fetch call, be explicit about caching and revalidation intent. Don’t rely on vibes.&lt;/li&gt;
&lt;li&gt;Create a staging environment that mirrors production caching settings as closely as possible.&lt;/li&gt;
&lt;li&gt;Add regression tests for content freshness—especially for routes that depend on CMS updates or time-sensitive data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your docs are “thin” (and the ecosystem often is), you compensate with internal guidance. A one-page engineering doc—“our caching rules for Next.js”—beats a week of Slack debates.&lt;/p&gt;
&lt;p&gt;This is where the backlash is genuinely right: caching behavior needs better mental models and better tooling support. But the solution isn’t abandoning Next.js; it’s getting disciplined about cache intent and verifying freshness.&lt;/p&gt;
&lt;h2 id="release-cadence-fast-doesnt-mean-recklessbut-it-can-feel-like-it"&gt;Release Cadence: Fast Doesn’t Mean Reckless—But It Can Feel Like It&lt;/h2&gt;
&lt;p&gt;Next.js evolves quickly. That’s not inherently bad—web frameworks should not ossify. But rapid release cadence can be brutal when breaking changes land and teams lack time or tooling to validate every upgrade.&lt;/p&gt;
&lt;p&gt;The core issue isn’t speed; it’s unpredictability. When a framework changes frequently, teams need a stable upgrade pipeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pin versions in lockfiles and update in controlled batches.&lt;/li&gt;
&lt;li&gt;Run end-to-end tests for key routes (not just unit tests).&lt;/li&gt;
&lt;li&gt;Pay attention to deprecations and migration guides early, not during firefights.&lt;/li&gt;
&lt;li&gt;Keep an “upgrade budget” in your planning, like you would for infrastructure maintenance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re encountering “things break every time,” that’s often a process failure, not only a framework failure. Frameworks move. Your job is to insulate production from constant churn.&lt;/p&gt;
&lt;p&gt;One pragmatic stance: upgrade regularly, but not always immediately. Move in small steps, measure, and roll forward with confidence. Teams that do this usually experience “steady improvement.” Teams that wait until they’re multiple versions behind experience “random catastrophe.”&lt;/p&gt;
&lt;h2 id="so-should-you-use-nextjs-anyway"&gt;So… Should You Use Next.js Anyway?&lt;/h2&gt;
&lt;p&gt;Here’s the nuanced position I’d actually recommend: don’t treat the Next.js backlash as a signal to abandon the framework. Treat it as a prompt to be more intentional about how you build.&lt;/p&gt;
&lt;p&gt;Next.js’s architectural strengths—server components, streaming SSR, hybrid rendering, ISR—are real capabilities that improve the baseline quality of many applications. But the criticisms around caching clarity, App Router mental load, and operational coupling to hosting platforms are legitimate risk factors if you ignore them.&lt;/p&gt;
&lt;p&gt;If you want a simple decision checklist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Are you prepared to be explicit about caching policy?&lt;/strong&gt; If not, fix that first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do you have team norms for server/client boundaries?&lt;/strong&gt; If not, codify them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do you understand how your platform features map to deployment portability?&lt;/strong&gt; If not, run a migration thought experiment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do you have a predictable upgrade process?&lt;/strong&gt; If not, build one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Done well, Next.js becomes less of a “framework you tolerate” and more of a platform that helps you ship faster—without surrendering control over performance and delivery.&lt;/p&gt;
&lt;h2 id="conclusion-the-discourse-is-loud-the-engineering-is-where-the-truth-lives"&gt;Conclusion: The Discourse Is Loud. The Engineering Is Where the Truth Lives.&lt;/h2&gt;
&lt;p&gt;The Next.js backlash is overblown in its certainty—but not entirely wrong in its instincts. The framework’s architectural advances are worth serious credit. The pain points—caching confusion, App Router cognitive load, and platform coupling—are real enough to demand better practices, clearer documentation, and more careful team discipline.&lt;/p&gt;
&lt;p&gt;In other words: Next.js isn’t the villain. But the backlash is a reminder that frameworks don’t remove engineering responsibility—they concentrate it.&lt;/p&gt;</content></item><item><title>Effective AI Code Review: A Framework for Teams</title><link>https://decastro.work/blog/effective-ai-code-review-framework-teams/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/effective-ai-code-review-framework-teams/</guid><description>&lt;p&gt;AI-generated code can look “done” in a way human code often doesn’t—clean diffs, sensible naming, and an almost eerie confidence. But that confidence is the problem. AI doesn’t merely make mistakes; it fails in &lt;em&gt;predictable&lt;/em&gt; ways. If your team reviews AI code the same way it reviews human-written code, you’ll systematically miss the specific bugs AI is most likely to introduce.&lt;/p&gt;
&lt;p&gt;Below is a practical framework—built for teams—that treats AI code review as a different discipline, with its own heuristics, checklists, and testing strategy.&lt;/p&gt;</description><content>&lt;p&gt;AI-generated code can look “done” in a way human code often doesn’t—clean diffs, sensible naming, and an almost eerie confidence. But that confidence is the problem. AI doesn’t merely make mistakes; it fails in &lt;em&gt;predictable&lt;/em&gt; ways. If your team reviews AI code the same way it reviews human-written code, you’ll systematically miss the specific bugs AI is most likely to introduce.&lt;/p&gt;
&lt;p&gt;Below is a practical framework—built for teams—that treats AI code review as a different discipline, with its own heuristics, checklists, and testing strategy.&lt;/p&gt;
&lt;h2 id="why-ai-code-review-breaks-traditional-heuristics"&gt;Why AI Code Review Breaks Traditional Heuristics&lt;/h2&gt;
&lt;p&gt;Human code review mostly targets misunderstandings: the developer read the requirements slightly wrong, chose the wrong invariant, or forgot how an API behaves. Those are real and common, but they come with telltale signs—confusing naming, awkward structure, and obvious missing cases.&lt;/p&gt;
&lt;p&gt;AI code review fails differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subtle correctness errors&lt;/strong&gt;: The code “works” on the happy path but violates an unstated constraint (time zones, idempotency, pagination, authorization boundaries).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confident abstraction mismatch&lt;/strong&gt;: AI often lands on patterns that are common on the internet, not necessarily the architecture your team uses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge-case blindness&lt;/strong&gt;: The model may not consider rare-but-critical inputs—empty lists, nulls, duplicated events, malformed UTF-8, boundary timestamps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spec drift&lt;/strong&gt;: The output may reflect a nearby “typical” interpretation of your request rather than your true requirement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure-mode coupling&lt;/strong&gt;: The bug type correlates with how the model generated the code (e.g., type assumptions, error-handling style, and assumptions about data shape).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the right move isn’t “be more skeptical.” The right move is to be skeptical in &lt;em&gt;the right directions&lt;/em&gt;—with a review checklist and targeted tests that map to AI’s predictable failure patterns.&lt;/p&gt;
&lt;h2 id="step-1-establish-an-ai-specific-review-checklist"&gt;Step 1: Establish an AI-Specific Review Checklist&lt;/h2&gt;
&lt;p&gt;A good human review checklist asks “Did we meet the requirements?” An AI checklist asks “Where might the requirements be silently violated?”&lt;/p&gt;
&lt;p&gt;Use a two-layer checklist: &lt;strong&gt;static reasoning&lt;/strong&gt; (what to look for in the diff) and &lt;strong&gt;behavioral validation&lt;/strong&gt; (what to test).&lt;/p&gt;
&lt;h3 id="static-checks-in-the-pr-diff"&gt;Static checks (in the PR diff)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge-case scan&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Look for explicit handling of: empty inputs, null/undefined, negative values, overflow/underflow, and boundary conditions.&lt;/li&gt;
&lt;li&gt;Ask: &lt;em&gt;What does the code do when the real world refuses to be polite?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Input validation and normalization&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are there schema checks before business logic?&lt;/li&gt;
&lt;li&gt;Are strings normalized (trimmed, case-folded) where your domain expects it?&lt;/li&gt;
&lt;li&gt;Does it handle malformed data gracefully, or does it assume cleanliness?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Error handling behavior&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the code preserve error context for debugging?&lt;/li&gt;
&lt;li&gt;Does it convert errors into the right HTTP status / domain error types?&lt;/li&gt;
&lt;li&gt;Does it accidentally swallow errors (e.g., &lt;code&gt;catch {}&lt;/code&gt;) or return success on failure?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Authorization and security boundaries&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI will often “wire the flow” but forget the &lt;em&gt;policy&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Confirm that permission checks occur at the correct boundary (e.g., before data access, not after partial transformation).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Abstraction alignment with your architecture&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does it use your service layer conventions, or does it introduce a new “helpful” pattern that bypasses them?&lt;/li&gt;
&lt;li&gt;Watch for: direct DB calls from handlers, ad-hoc HTTP clients, bypassing your logging/tracing utilities, reinvented caching.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Assumptions about data shape&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the code uses fields like &lt;code&gt;user.email&lt;/code&gt; or &lt;code&gt;request.body.items[0]&lt;/code&gt;, confirm those fields are guaranteed by your contract.&lt;/li&gt;
&lt;li&gt;AI often assumes optional fields are always present, especially when the prompt is vague.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="behavioral-checks-questions-for-the-author"&gt;Behavioral checks (questions for the author)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What invariants are guaranteed?&lt;/strong&gt;&lt;br&gt;
Example: “This operation is idempotent” or “This endpoint is safe under retries.” If the author can’t answer, you’ve found a review gap.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What edge cases are tested?&lt;/strong&gt;&lt;br&gt;
Require at least one test per risk category relevant to the change.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Where can the model have guessed wrong?&lt;/strong&gt;&lt;br&gt;
For example: time zone handling, pagination semantics, event ordering, numeric rounding rules, or database transaction boundaries.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your team treats this as a “nice to have,” it will become theater. Make it explicit: every AI PR must satisfy the checklist—or document why deviations are safe.&lt;/p&gt;
&lt;h2 id="step-2-verify-abstractions-match-your-architecture-not-the-internet"&gt;Step 2: Verify Abstractions Match Your Architecture (Not the Internet)&lt;/h2&gt;
&lt;p&gt;AI tends to optimize for &lt;em&gt;what’s common&lt;/em&gt;, not what’s correct for you. “Common” patterns can still be wrong when they conflict with your constraints: tracing, dependency injection, repository boundaries, job orchestration, or deployment topology.&lt;/p&gt;
&lt;p&gt;Concrete example: imagine AI generates a feature that queries the database directly inside an HTTP handler:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI output&lt;/strong&gt;: &lt;code&gt;app.get('/items', async (req, res) =&amp;gt; { const rows = await db.query(...) ... })&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your architecture&lt;/strong&gt;: handlers call services; services call repositories; repositories attach metrics and enforce tenant filters; tracing spans are created at the service boundary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if the direct query works functionally, it can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;skip tenant scoping,&lt;/li&gt;
&lt;li&gt;miss structured logging fields,&lt;/li&gt;
&lt;li&gt;break metrics aggregation,&lt;/li&gt;
&lt;li&gt;and make future refactors painful.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your review should explicitly ask: &lt;strong&gt;Does this code obey our layering rules?&lt;/strong&gt; If not, require a refactor before approving.&lt;/p&gt;
&lt;p&gt;Practical advice: maintain a short “house pattern” doc with links to examples (e.g., “How we do DB access,” “How we do retries,” “How we do pagination”). When AI code violates those patterns, reviewers shouldn’t negotiate on taste—point to the house rules.&lt;/p&gt;
&lt;h2 id="step-3-test-for-ai-specific-failure-modes"&gt;Step 3: Test for AI-Specific Failure Modes&lt;/h2&gt;
&lt;p&gt;AI bugs are best caught by tests that target &lt;em&gt;behavior&lt;/em&gt;, not just implementation details. Humans can reason through “what if the developer misunderstood X.” AI requires a different stance: “what if the model made a reasonable-sounding assumption that isn’t true here?”&lt;/p&gt;
&lt;p&gt;Here are failure-mode-focused test categories that map well to AI-generated code:&lt;/p&gt;
&lt;h3 id="1-boundary-and-emptiness-tests"&gt;1) Boundary and emptiness tests&lt;/h3&gt;
&lt;p&gt;If the code processes collections:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;empty list → should return empty result, not error&lt;/li&gt;
&lt;li&gt;single element → should be correct&lt;/li&gt;
&lt;li&gt;maximum size near limits → should behave predictably&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If it processes time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;epoch values, daylight savings transitions, time zone offsets, and rounding boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-contract-tests-data-shape-and-optionality"&gt;2) Contract tests (data shape and optionality)&lt;/h3&gt;
&lt;p&gt;AI frequently assumes fields exist. Add tests that cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;missing optional fields&lt;/li&gt;
&lt;li&gt;unexpected types (string vs number)&lt;/li&gt;
&lt;li&gt;unknown enum values&lt;/li&gt;
&lt;li&gt;malformed JSON payloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your aim isn’t to reject everything—it&amp;rsquo;s to ensure the system fails safely and predictably.&lt;/p&gt;
&lt;h3 id="3-error-path-correctness"&gt;3) Error-path correctness&lt;/h3&gt;
&lt;p&gt;Require tests for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;downstream service timeouts&lt;/li&gt;
&lt;li&gt;invalid credentials&lt;/li&gt;
&lt;li&gt;database constraint violations&lt;/li&gt;
&lt;li&gt;retries and idempotency behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common AI mistake is to handle “happy errors” (throwing) but mishandle “messy errors” (partial failure or retries).&lt;/p&gt;
&lt;h3 id="4-security-relevant-tests"&gt;4) Security-relevant tests&lt;/h3&gt;
&lt;p&gt;For endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ensure authorization happens before data access&lt;/li&gt;
&lt;li&gt;ensure tenant scoping is applied consistently&lt;/li&gt;
&lt;li&gt;ensure logs do not leak sensitive values&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple test harness that verifies “unauthorized user cannot access resource ID X” can catch a surprising amount of AI-generated wiring mistakes.&lt;/p&gt;
&lt;h3 id="5-concurrency-and-ordering-assumptions"&gt;5) Concurrency and ordering assumptions&lt;/h3&gt;
&lt;p&gt;AI often assumes sequential execution. Add tests for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;duplicate requests&lt;/li&gt;
&lt;li&gt;out-of-order event arrival&lt;/li&gt;
&lt;li&gt;concurrent updates causing race conditions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your domain has event sourcing or background jobs, prioritize tests around deduplication and idempotency keys.&lt;/p&gt;
&lt;h2 id="step-4-make-review-a-collaborative-workflow-not-a-gate"&gt;Step 4: Make Review a Collaborative Workflow, Not a Gate&lt;/h2&gt;
&lt;p&gt;Teams get stuck because they treat AI code review as a one-person “spot the bug” activity. Instead, adopt a loop where the reviewer guides the AI output into a safer shape.&lt;/p&gt;
&lt;p&gt;Practical workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reviewer flags risks early&lt;/strong&gt;&lt;br&gt;
Before asking for changes, name the likely failure modes: “I’m worried about optional fields,” or “This pagination logic looks too optimistic.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Author runs targeted tests and shares results&lt;/strong&gt;&lt;br&gt;
Don’t stop at “tests pass.” Ask: “Which tests cover the risk categories?”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Second iteration tightens contracts&lt;/strong&gt;&lt;br&gt;
If the model assumed types or invariants, force explicit contract checks in code (schemas, guards, assertions) and reflect them in tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Document assumptions in code comments or design notes&lt;/strong&gt;&lt;br&gt;
If the code relies on “items always exists,” codify it with input validation—or with an explicit comment plus a test proving the assumption.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This turns AI code review into a reliability conversation, not an aesthetics debate.&lt;/p&gt;
&lt;h2 id="step-5-adopt-an-ai-pr-approval-policy"&gt;Step 5: Adopt an “AI PR” Approval Policy&lt;/h2&gt;
&lt;p&gt;You don’t need to ban AI-generated code. You do need consistency.&lt;/p&gt;
&lt;p&gt;A simple policy teams can enforce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI-generated PRs require the AI review checklist&lt;/strong&gt; (static + behavioral).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At least one reviewer must be a domain owner&lt;/strong&gt; for changes in security, payments, data integrity, or critical business logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage must include edge-case categories&lt;/strong&gt; relevant to the change—not necessarily “100%,” but “risk-aligned.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Abstraction mismatches require refactor&lt;/strong&gt; to house patterns, not “approval with a comment.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To keep it lightweight, create a template reviewers can use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Edge cases identified:&lt;/li&gt;
&lt;li&gt;Tests added/updated:&lt;/li&gt;
&lt;li&gt;Security/authorization validated:&lt;/li&gt;
&lt;li&gt;Architecture alignment confirmed:&lt;/li&gt;
&lt;li&gt;Remaining assumptions documented:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The template is the point. It prevents the review from devolving into “seems fine.”&lt;/p&gt;
&lt;h2 id="conclusion-review-ai-like-its-a-different-kind-of-developer"&gt;Conclusion: Review AI Like It’s a Different Kind of Developer&lt;/h2&gt;
&lt;p&gt;AI code review isn’t harder because the code is uglier. It’s harder because AI fails in quieter ways—confidently implementing the &lt;em&gt;wrong&lt;/em&gt; assumption. Teams win by switching heuristics: scan for edge cases, verify architecture-aligned abstractions, and test the specific failure modes AI is prone to introduce.&lt;/p&gt;
&lt;p&gt;When you treat AI code review as a dedicated workflow—with explicit checklists and risk-aligned tests—you don’t just catch bugs faster. You build software that stays correct when the model confidently gets something slightly off.&lt;/p&gt;</content></item><item><title>Developer AI Skepticism Is Growing and That's the Healthiest Thing I've Seen</title><link>https://decastro.work/blog/developer-ai-skepticism-growing-healthiest-thing/</link><pubDate>Sun, 06 Jul 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/developer-ai-skepticism-growing-healthiest-thing/</guid><description>&lt;p&gt;Developers aren’t turning away from AI—they’re turning up the scrutiny. And that shift, paradoxical on the surface, is the healthiest sign the software industry could ask for: adoption is rising, trust is falling, and expectations are becoming more realistic.&lt;/p&gt;
&lt;h2 id="the-paradox-isnt-a-paradox"&gt;The “paradox” isn’t a paradox&lt;/h2&gt;
&lt;p&gt;You can summarize the current mood like this: more teams are using AI tooling, but fewer people believe the output should be treated as truth. That sounds like contradiction until you notice what “trust” actually means in engineering.&lt;/p&gt;</description><content>&lt;p&gt;Developers aren’t turning away from AI—they’re turning up the scrutiny. And that shift, paradoxical on the surface, is the healthiest sign the software industry could ask for: adoption is rising, trust is falling, and expectations are becoming more realistic.&lt;/p&gt;
&lt;h2 id="the-paradox-isnt-a-paradox"&gt;The “paradox” isn’t a paradox&lt;/h2&gt;
&lt;p&gt;You can summarize the current mood like this: more teams are using AI tooling, but fewer people believe the output should be treated as truth. That sounds like contradiction until you notice what “trust” actually means in engineering.&lt;/p&gt;
&lt;p&gt;In the early hype cycle, trust was often implied: if the model could generate plausible code, it must be correct. In practice, developers discovered a more useful operating system: AI is a drafting tool, not an oracle.&lt;/p&gt;
&lt;p&gt;So the adoption-versus-trust split is not regress. It’s a maturation signal. Teams are learning to separate &lt;em&gt;usefulness&lt;/em&gt; from &lt;em&gt;authority&lt;/em&gt;. They’re getting value without pretending the tool is infallible.&lt;/p&gt;
&lt;p&gt;A concrete example: a developer can use an AI assistant to scaffold a REST endpoint in seconds—then spend the next five minutes wiring validation, error handling, auth checks, and tests. The tool saves time. The human still owns correctness. That’s the new norm.&lt;/p&gt;
&lt;h2 id="adoption-goes-up-because-ai-helps-with-the-right-tasks"&gt;Adoption goes up because AI helps with the right tasks&lt;/h2&gt;
&lt;p&gt;Skepticism doesn’t happen in a vacuum. Developers keep using AI because it reduces friction in places where they feel the burn daily:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Boilerplate and scaffolding:&lt;/strong&gt; CRUD endpoints, serializers, basic UI components, configuration templates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explaining code and APIs:&lt;/strong&gt; turning a vague internal wiki into actionable steps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drafting tests and edge cases:&lt;/strong&gt; generating initial test structures and prompting follow-ups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactoring suggestions:&lt;/strong&gt; outlining how to restructure functions, rename variables, or simplify control flow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When AI supports these workflows, it earns its keep. People adopt tools that reduce cognitive load and cycle time—especially under tight deadlines. Even skeptical engineers will try something that saves them an afternoon.&lt;/p&gt;
&lt;p&gt;The important nuance is that adoption doesn’t require blind faith. Developers don’t need AI to be right; they need it to be &lt;em&gt;helpful enough&lt;/em&gt; to make review faster and outcomes better.&lt;/p&gt;
&lt;h2 id="trust-goes-down-because-developers-have-finally-learned-the-failure-modes"&gt;Trust goes down because developers have finally learned the failure modes&lt;/h2&gt;
&lt;p&gt;Where skepticism becomes “productive” is in how targeted it is. Developers aren’t just saying “AI is bad.” They’re saying, essentially: “I know how it breaks.”&lt;/p&gt;
&lt;p&gt;In real projects, AI output commonly fails in predictable categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Subtle logic bugs:&lt;/strong&gt; code that compiles but behaves incorrectly under edge conditions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incorrect assumptions:&lt;/strong&gt; using the wrong library function signature, mismatched API versions, or outdated conventions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security oversights:&lt;/strong&gt; missing input sanitization, naive auth logic, or accidental exposure of sensitive data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;False confidence:&lt;/strong&gt; output that &lt;em&gt;looks&lt;/em&gt; correct even when it’s missing required requirements or constraints.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why trust erodes over time. Early users treat AI like an autocomplete superpower. Later users treat it like a junior contributor: fast to produce, but needing verification.&lt;/p&gt;
&lt;p&gt;A good mental model: if AI is used as a &lt;em&gt;starting point&lt;/em&gt;, trust becomes rational. If it’s used as a &lt;em&gt;source of truth&lt;/em&gt;, trust collapses quickly. The decline in trust isn’t cynicism—it’s calibration.&lt;/p&gt;
&lt;h2 id="the-healthiest-teams-build-ai-into-a-quality-loop"&gt;The healthiest teams build AI into a quality loop&lt;/h2&gt;
&lt;p&gt;The difference between “AI skepticism” and “AI fear” is whether teams build guardrails. The healthiest trajectory is not less AI; it’s more engineering discipline around AI output.&lt;/p&gt;
&lt;p&gt;Here are practical patterns that turn skepticism into speed:&lt;/p&gt;
&lt;h3 id="treat-ai-output-like-a-diff-not-a-decision"&gt;Treat AI output like a diff, not a decision&lt;/h3&gt;
&lt;p&gt;Instead of asking, “Is this correct?” ask, “What changed, and do we understand why?”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Require AI-generated code to pass through the same review checklist as human-written code.&lt;/li&gt;
&lt;li&gt;In code review, ask for &lt;em&gt;rationale&lt;/em&gt;, not just acceptance: “Why does this handle nulls this way?” “What happens with malformed input?”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="add-tests-before-you-trust-the-behavior"&gt;Add tests before you trust the behavior&lt;/h3&gt;
&lt;p&gt;If AI drafts tests, great—use them—but don’t stop there. Make a habit of adding at least one “nasty” test per feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;empty inputs&lt;/li&gt;
&lt;li&gt;unexpected types&lt;/li&gt;
&lt;li&gt;boundary values&lt;/li&gt;
&lt;li&gt;permission failures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point isn’t to catch everything. It’s to ensure the model’s mistakes become visible early—before they become production bugs.&lt;/p&gt;
&lt;h3 id="make-the-tool-context-aware-not-just-prompt-aware"&gt;Make the tool context-aware, not just prompt-aware&lt;/h3&gt;
&lt;p&gt;A common trap is sending a generic prompt and trusting the result. Better approach: give the model the project’s constraints.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Link to relevant interfaces or type definitions.&lt;/li&gt;
&lt;li&gt;Provide the expected request/response schema.&lt;/li&gt;
&lt;li&gt;Tell it the conventions: error format, logging style, naming rules, timeouts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Skeptics don’t just ask for better answers—they ask for &lt;em&gt;better inputs&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="use-automated-checks-to-outvote-the-model"&gt;Use automated checks to “outvote” the model&lt;/h3&gt;
&lt;p&gt;Put friction where it matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static analysis&lt;/li&gt;
&lt;li&gt;linting&lt;/li&gt;
&lt;li&gt;formatting enforcement&lt;/li&gt;
&lt;li&gt;type checking&lt;/li&gt;
&lt;li&gt;dependency vulnerability scans&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let tools catch the easy-to-miss failures so humans can focus on the hard reasoning.&lt;/p&gt;
&lt;h2 id="skepticism-is-becoming-a-skill-not-a-stance"&gt;Skepticism is becoming a skill, not a stance&lt;/h2&gt;
&lt;p&gt;This shift matters: developers aren’t merely refusing AI. They’re developing a new competency—knowing when to trust, when to verify, and how to structure work so verification is cheap.&lt;/p&gt;
&lt;p&gt;Think of it like unit testing. At first, “test everything” sounds like overhead. Over time, it becomes a superpower because it changes how you work. You move faster because you’re not constantly guessing.&lt;/p&gt;
&lt;p&gt;AI skepticism is following the same path. Teams that learn to evaluate AI output quickly aren’t resisting progress; they’re upgrading their process.&lt;/p&gt;
&lt;p&gt;For example, a developer might adopt a rule like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI drafts&lt;/strong&gt;: generate candidate implementations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Humans verify&lt;/strong&gt;: confirm correctness, align with requirements, and review security implications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI enforces&lt;/strong&gt;: fail builds on style, type, and test regressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not anti-AI. It’s professionalized AI usage.&lt;/p&gt;
&lt;h2 id="why-this-maturity-beats-the-hype-cycle-every-time"&gt;Why this maturity beats the hype cycle every time&lt;/h2&gt;
&lt;p&gt;The industry has been here before: tools arrive promising miracles, everyone rushes to adopt, and then reality kicks in. The difference now is that the response is healthier.&lt;/p&gt;
&lt;p&gt;Instead of doubling down on magical thinking, developers are doing something better: they’re turning lessons into norms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Trust is earned through testing and review, not through plausibility.&lt;/li&gt;
&lt;li&gt;AI is evaluated like any other contributor: output quality varies by task and context.&lt;/li&gt;
&lt;li&gt;Feedback loops improve results over time, without pretending the first version was perfect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a single sentence summary of what’s happening: the tools are getting used more widely, and expectations are getting corrected faster than marketing can inflate them.&lt;/p&gt;
&lt;p&gt;That’s how an ecosystem becomes durable.&lt;/p&gt;
&lt;h2 id="conclusion-the-next-wave-of-ai-wont-be-built-on-blind-trust"&gt;Conclusion: The next wave of AI won’t be built on blind trust&lt;/h2&gt;
&lt;p&gt;The rise of developer skepticism isn’t a warning sign—it’s a maturity milestone. As AI becomes embedded in everyday workflows, trust naturally becomes conditional: useful for drafts, verified for decisions.&lt;/p&gt;
&lt;p&gt;The healthiest thing we can do—individually and as teams—is exactly what skeptical developers are doing now: use AI to move faster, then prove correctness with the engineering fundamentals that have always mattered. When adoption and evaluation grow up together, the hype cycle stops driving the roadmap—and craft finally does.&lt;/p&gt;</content></item><item><title>AI Coding Agents Need Guardrails, Not Cheerleaders</title><link>https://decastro.work/blog/ai-coding-agents-need-guardrails-not-cheerleaders/</link><pubDate>Tue, 24 Jun 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ai-coding-agents-need-guardrails-not-cheerleaders/</guid><description>&lt;p&gt;AI coding agents have moved from “impressive demo” to “just ship it.” And once teams adopt the unrestricted “let the agent do everything” mindset, the results stop being clever—and start looking like familiar technical debt with a faster keyboard. The pattern is now painfully predictable: untested code, direct commits to &lt;code&gt;main&lt;/code&gt;, and pull requests that get rubber-stamped because someone said, “The AI checked it.”&lt;/p&gt;
&lt;p&gt;That’s not agentic development. It’s automation without responsibility.&lt;/p&gt;</description><content>&lt;p&gt;AI coding agents have moved from “impressive demo” to “just ship it.” And once teams adopt the unrestricted “let the agent do everything” mindset, the results stop being clever—and start looking like familiar technical debt with a faster keyboard. The pattern is now painfully predictable: untested code, direct commits to &lt;code&gt;main&lt;/code&gt;, and pull requests that get rubber-stamped because someone said, “The AI checked it.”&lt;/p&gt;
&lt;p&gt;That’s not agentic development. It’s automation without responsibility.&lt;/p&gt;
&lt;h2 id="the-hype-cycles-hidden-failure-mode-speed-over-correctness"&gt;The hype cycle’s hidden failure mode: speed over correctness&lt;/h2&gt;
&lt;p&gt;The original promise of AI coding agents was simple: reduce toil. Generate code quickly, propose changes automatically, and help teams move faster. The failure mode appears when “faster” quietly becomes “always.”&lt;/p&gt;
&lt;p&gt;In the unrestricted approach, agents are often granted four freedoms at once:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Write code without strong constraints&lt;/strong&gt; (no enforced test expectations, no required coverage thresholds).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute changes without sandboxing&lt;/strong&gt; (shared environments, persistent credentials, broad filesystem access).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit directly to privileged branches&lt;/strong&gt; (&lt;code&gt;main&lt;/code&gt;, or equivalent).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Submit PRs with authority&lt;/strong&gt; (“LGTM—AI verified it”), which discourages real review.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notice how each freedom removes a human control point. Individually, that might feel efficient. Together, they create a new class of technical debt: changes that look structured, compile “sometimes,” and pass casual checks—yet fail under real usage, produce security exposure, or break downstream assumptions.&lt;/p&gt;
&lt;p&gt;The key editorial point: agents don’t create correctness. They create plausible code. Without guardrails, plausibility becomes policy.&lt;/p&gt;
&lt;h2 id="why-the-ai-checked-it-is-a-trap"&gt;Why “the AI checked it” is a trap&lt;/h2&gt;
&lt;p&gt;Teams already understand the gap between “code generated” and “code validated.” We’ve built entire engineering cultures around that gap: CI gates, staging environments, test suites, linters, code review, and incident response.&lt;/p&gt;
&lt;p&gt;“AI checked it” collapses that gap through social convenience. It’s a claim that substitutes process for proof.&lt;/p&gt;
&lt;p&gt;Consider what often happens in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The agent runs a thin local test set (or none), because it can’t reliably know what matters for your domain.&lt;/li&gt;
&lt;li&gt;The agent “updates tests” to make them pass, because it can optimize for the visible objective rather than the underlying requirement.&lt;/li&gt;
&lt;li&gt;The agent’s PR description is persuasive and specific—because language models are good at writing explanations—even when the changes are wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A sharp rule of thumb: &lt;strong&gt;if review criteria changed from “can we trust this?” to “the bot said so,” you’re past the point where technical debt is preventable.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="guardrails-arent-bureaucracytheyre-a-control-system"&gt;Guardrails aren’t bureaucracy—they’re a control system&lt;/h2&gt;
&lt;p&gt;A good agent workflow is not “permissionless coding.” It’s a bounded system that makes failure expensive and success measurable. The constraints aren’t there to slow you down; they’re there to keep your engineering pipeline honest.&lt;/p&gt;
&lt;p&gt;Here’s the guardrail stack I’d treat as non-negotiable for production-grade AI coding agents:&lt;/p&gt;
&lt;h3 id="sandbox-execution-and-least-privilege-by-default"&gt;Sandbox execution (and least privilege by default)&lt;/h3&gt;
&lt;p&gt;Run agent code generation and execution inside isolated environments. No broad network access. No long-lived secrets. No direct access to production-like credentials.&lt;/p&gt;
&lt;p&gt;Practical example: if the agent needs to run integration tests, give it a test-only service account with read-only access and scoped tokens, and route it through a test harness that cannot reach external systems.&lt;/p&gt;
&lt;h3 id="mandatory-test-expectations-not-optional-best-effort"&gt;Mandatory test expectations (not optional “best effort”)&lt;/h3&gt;
&lt;p&gt;Agents should not be allowed to merge code that doesn’t meet defined quality gates. Define:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimum unit test coverage for touched modules (even if lightweight)&lt;/li&gt;
&lt;li&gt;Required test categories (e.g., unit + integration for API changes)&lt;/li&gt;
&lt;li&gt;A policy for “no tests added” (usually: fail the pipeline)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Crucial detail: make the agent responsible for &lt;em&gt;writing the tests&lt;/em&gt; only inside the constraints you enforce. Your CI is the enforcement mechanism; the agent is just a contributor.&lt;/p&gt;
&lt;h3 id="human-review-gates-with-real-checklists"&gt;Human review gates (with real checklists)&lt;/h3&gt;
&lt;p&gt;Keep humans in the loop. But don’t leave review to mood or memory—encode review gates into tooling and PR templates.&lt;/p&gt;
&lt;p&gt;Example checklist for AI-generated changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Behavioral correctness:&lt;/strong&gt; does this change the system in intended ways only?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases:&lt;/strong&gt; are boundaries and error handling covered?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security posture:&lt;/strong&gt; are secrets handled correctly? Are inputs validated?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintainability:&lt;/strong&gt; are changes localized? Is the diff reasonable?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn’t to distrust the agent; it’s to make review systematic even when the code is generated quickly.&lt;/p&gt;
&lt;h3 id="scope-limitations-timebox-file-allowlists-and-change-budgets"&gt;Scope limitations (timebox, file allowlists, and change budgets)&lt;/h3&gt;
&lt;p&gt;An unrestricted agent is a chaos generator with a badge. Use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time budgets&lt;/strong&gt; (e.g., agent can iterate for 5–10 minutes per task)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File allowlists&lt;/strong&gt; (e.g., only under &lt;code&gt;src/&lt;/code&gt; and &lt;code&gt;tests/&lt;/code&gt; for a given objective)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change budgets&lt;/strong&gt; (e.g., cap the number of files or lines modified)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the agent can rewrite half your repo, you’re not using an assistant—you’re running a demolition crew with autocomplete.&lt;/p&gt;
&lt;h2 id="the-anti-pattern-committing-to-main-like-its-a-spreadsheet"&gt;The anti-pattern: committing to &lt;code&gt;main&lt;/code&gt; like it’s a spreadsheet&lt;/h2&gt;
&lt;p&gt;The most corrosive behavior in the current wave is “agent commits to `main because it’s confident.” Confidence is not a release criterion, and AI uncertainty does not translate cleanly into engineering risk.&lt;/p&gt;
&lt;p&gt;Instead, treat agent-generated changes like any other untrusted input: they must pass through the same gates as human-authored PRs—just faster and with more explicit evidence.&lt;/p&gt;
&lt;p&gt;A robust workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Agent creates a branch (not PR-less commits).&lt;/li&gt;
&lt;li&gt;CI runs the full relevant test suite.&lt;/li&gt;
&lt;li&gt;Required checks succeed (tests, lint, security scanning where applicable).&lt;/li&gt;
&lt;li&gt;Human review happens.&lt;/li&gt;
&lt;li&gt;Merge happens only after gates pass.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re tempted to bypass steps “because the bot is good,” ask what you’re optimizing for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Throughput&lt;/strong&gt; (fine)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;or system reliability&lt;/strong&gt; (better)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trick is to recognize that throughput without reliability is just creating the next incident faster.&lt;/p&gt;
&lt;h2 id="a-better-model-agent-as-implementer-team-as-validator"&gt;A better model: “agent as implementer,” “team as validator”&lt;/h2&gt;
&lt;p&gt;Agentic development shines when you separate responsibilities. An AI coding agent should be an implementer that proposes changes, but it shouldn’t be the final arbiter of correctness.&lt;/p&gt;
&lt;p&gt;Here’s a practical division of labor that works in real teams:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You define the contract:&lt;/strong&gt; issue requirements, acceptance criteria, APIs involved, and risk boundaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The agent drafts the implementation:&lt;/strong&gt; code changes and accompanying tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your pipeline validates everything:&lt;/strong&gt; deterministic checks, unit and integration tests, security tooling, and build verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Humans arbitrate ambiguity:&lt;/strong&gt; design tradeoffs, review context, and domain-specific correctness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When teams adopt this model, agents become useful without becoming dangerous. You get speed where it matters—drafting, refactoring, test generation under constraints—while preserving the human authority that final validation requires.&lt;/p&gt;
&lt;p&gt;Concrete example: say you want to add a new endpoint. A good agent workflow would:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generate the endpoint handler,&lt;/li&gt;
&lt;li&gt;update the routing,&lt;/li&gt;
&lt;li&gt;add tests for success and failure paths,&lt;/li&gt;
&lt;li&gt;and produce a PR that clearly links new tests to new behavior.
But it should not:&lt;/li&gt;
&lt;li&gt;guess at authorization rules without confirmation,&lt;/li&gt;
&lt;li&gt;skip integration tests because “unit tests passed,”&lt;/li&gt;
&lt;li&gt;or modify unrelated modules to “make things work.”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion-guardrails-are-how-you-keep-agent-speed-from-becoming-debt"&gt;Conclusion: Guardrails are how you keep agent speed from becoming debt&lt;/h2&gt;
&lt;p&gt;AI coding agents are not the enemy. Unrestricted adoption is.&lt;/p&gt;
&lt;p&gt;If you want agents to reduce technical debt, you need them inside a control system: sandboxed execution, mandatory tests, human review gates, and strict scope limits. Anything else turns “agentic development” into “automated regret”—a faster way to ship plausible code that your team will have to untangle later.&lt;/p&gt;
&lt;p&gt;Be the adult in the workflow: let the agent draft, but make correctness expensive to get wrong and easy to verify when it’s right.&lt;/p&gt;</content></item><item><title>Valkey Forked Redis and Nobody Panicked</title><link>https://decastro.work/blog/valkey-forked-redis-nobody-panicked/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/valkey-forked-redis-nobody-panicked/</guid><description>&lt;p&gt;There aren’t many technology stories where “corporate licensing changed” turns into “everyone quietly kept shipping.” The Valkey fork is one of them. When Redis Ltd. tightened its license, the community didn’t build a new database from scratch or demand users rewrite their world—it forked, kept the interface steady, and made the ecosystem resilient. The result wasn’t just a technical win. It was a cultural one: open-source infrastructure can survive corporate capture—without putting engineers on a two-week migration treadmill.&lt;/p&gt;</description><content>&lt;p&gt;There aren’t many technology stories where “corporate licensing changed” turns into “everyone quietly kept shipping.” The Valkey fork is one of them. When Redis Ltd. tightened its license, the community didn’t build a new database from scratch or demand users rewrite their world—it forked, kept the interface steady, and made the ecosystem resilient. The result wasn’t just a technical win. It was a cultural one: open-source infrastructure can survive corporate capture—without putting engineers on a two-week migration treadmill.&lt;/p&gt;
&lt;h2 id="what-happened-when-redis-got-restrictive"&gt;What Happened When Redis Got Restrictive&lt;/h2&gt;
&lt;p&gt;Redis didn’t suddenly become unusable. The alarm sounded because Redis’s licensing direction shifted toward restrictions that many organizations—especially those using Redis as infrastructure at scale—viewed as a risk. In practice, licensing changes can be as operationally disruptive as breaking API changes: procurement, legal review, vendor contracts, compliance workflows, and internal policy all have to catch up.&lt;/p&gt;
&lt;p&gt;And unlike a normal “upgrade,” licensing friction tends to propagate through an organization slowly—until it abruptly doesn’t. The moment a company’s leadership decides it wants certainty, the engineering plan becomes: ensure the dependency is stable, legally safe, and technically compatible.&lt;/p&gt;
&lt;p&gt;That’s where the fork mattered. A fork is often framed as a last resort. Here, it became a strategy: keep the system the same for users, while removing the license uncertainty at the source.&lt;/p&gt;
&lt;h2 id="valkey-same-shape-different-governance"&gt;Valkey: Same Shape, Different Governance&lt;/h2&gt;
&lt;p&gt;The Linux Foundation forked Redis into Valkey. The key point—one that engineers care about more than headlines—is that Valkey wasn’t designed to be “Redis, but different.” It was designed to be Redis, but with a governance and licensing story that didn’t trigger the same existential worry.&lt;/p&gt;
&lt;p&gt;In an ideal world, migrations are boring. Valkey’s success hinged on boring being possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API compatibility:&lt;/strong&gt; Applications that speak Redis commands didn’t need to learn a new dialect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance parity:&lt;/strong&gt; Systems that depended on Redis’s speed didn’t have to justify a sudden slowdown with benchmarking theater.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data format compatibility:&lt;/strong&gt; Stored data didn’t become a museum piece that required an elaborate conversion project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That combination is what makes “overnight migration” realistic. If the new system changed the wire protocol, the data model, or the behavior of common commands, users would have noticed immediately. Instead, users noticed something else: their infrastructure stopped being exposed to a licensing cliff.&lt;/p&gt;
&lt;h2 id="how-the-ecosystem-switched-without-users-feeling-it"&gt;How the Ecosystem Switched Without Users Feeling It&lt;/h2&gt;
&lt;p&gt;Fork stories fail when they stay trapped inside engineering circles. This one didn’t. Major cloud providers and vendors—AWS, Google, Oracle, and Ericsson among them—backed Valkey. That matters because most production Redis deployments aren’t “pets.” They’re services managed by platforms, tooling, and automation.&lt;/p&gt;
&lt;p&gt;When those layers adopt a compatible alternative, the migration path becomes less like a grand rewrite and more like a dependency swap.&lt;/p&gt;
&lt;p&gt;Here’s what “seamless” typically looks like in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Managed service adoption&lt;/strong&gt;&lt;br&gt;
Instead of every team compiling and operating a new Redis-like database, platform teams switch the backing implementation. Internal services keep connecting to the same endpoint style—often with the same client libraries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compatibility at the edges&lt;/strong&gt;&lt;br&gt;
Many production issues show up where assumptions leak: client versions, proxy behavior, command parsing, replication semantics. Because Valkey kept compatibility close to Redis, these edge cases were less likely to break in surprising ways.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No “big bang” deployments&lt;/strong&gt;&lt;br&gt;
Teams can roll changes gradually—cluster-by-cluster, region-by-region—using feature flags and deployment pipelines. When the interface is compatible, the risk profile drops dramatically.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A concrete example: consider a web service that uses Redis for session state and caching. If the application is already configured around standard Redis commands and the client library is compatible, the team can focus on validating performance and operational behavior—rather than rewriting business logic or rethinking serialization.&lt;/p&gt;
&lt;p&gt;The “nobody panicked” part wasn’t magic. It was engineering design aligned with operational reality.&lt;/p&gt;
&lt;h2 id="why-standard-protocols-beat-vendor-licensing"&gt;Why Standard Protocols Beat Vendor Licensing&lt;/h2&gt;
&lt;p&gt;Licensing decisions are corporate decisions. Infrastructure is an operational commitment. The tension between those two realities is where open-source resilience lives or dies.&lt;/p&gt;
&lt;p&gt;When a project is built on &lt;strong&gt;standard protocols&lt;/strong&gt; and stable interfaces, it becomes harder for a single vendor’s licensing policy to strand users. Engineers can swap implementations without forcing a rewrite of their entire stack.&lt;/p&gt;
&lt;p&gt;This is the deeper lesson: the Redis protocol isn’t just an implementation detail. It’s the contract that lets ecosystems evolve. As long as clients, tooling, and data expectations remain grounded in that contract, forks are viable responses to governance failures—not disruptive events.&lt;/p&gt;
&lt;p&gt;You can see this principle in other infrastructure categories, too:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you use SQL, moving between engines is painful but often manageable.&lt;/li&gt;
&lt;li&gt;If you use container interfaces, you can shift runtimes without breaking the world.&lt;/li&gt;
&lt;li&gt;If you standardize on HTTP semantics and clear API boundaries, you can replace backends while keeping clients stable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Valkey didn’t invalidate Redis. It demonstrated that a widely adopted protocol can outlive one company’s licensing choices—because users have options and ecosystems have inertia.&lt;/p&gt;
&lt;h2 id="practical-advice-how-to-build-for-this-kind-of-shock"&gt;Practical Advice: How to Build for This Kind of Shock&lt;/h2&gt;
&lt;p&gt;The Valkey story is inspiring, but you shouldn’t bet your production uptime on hope. If you run infrastructure that could face licensing or governance risk, you need a plan that assumes “the dependency may change.”&lt;/p&gt;
&lt;p&gt;Here are practical steps that reduce your blast radius:&lt;/p&gt;
&lt;h3 id="1-treat-compatibility-as-a-requirement-not-a-hope"&gt;1) Treat compatibility as a requirement, not a hope&lt;/h3&gt;
&lt;p&gt;Before you adopt any critical dependency, confirm what would break if the implementation changed. Is your application tied to one vendor’s extensions? Are you relying on undocumented behavior? Is your data model vendor-specific?&lt;/p&gt;
&lt;p&gt;If you’re using the “standard path” (well-defined commands, stable semantics), you’re already doing the right thing.&lt;/p&gt;
&lt;h3 id="2-keep-dependency-surfaces-small"&gt;2) Keep dependency surfaces small&lt;/h3&gt;
&lt;p&gt;If Redis is embedded everywhere—directly into app logic, analytics jobs, and custom tooling—you’ll feel migration pain. Prefer centralized access patterns: consistent clients, shared libraries, standardized command usage, and a small number of integration points.&lt;/p&gt;
&lt;h3 id="3-maintain-a-compatibility-test-harness"&gt;3) Maintain a compatibility test harness&lt;/h3&gt;
&lt;p&gt;When you can run the same test suite against multiple implementations, you turn migration from a philosophical debate into a technical exercise. Validate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;command behavior for your critical operations&lt;/li&gt;
&lt;li&gt;serialization/deserialization logic&lt;/li&gt;
&lt;li&gt;failure modes (timeouts, reconnect behavior)&lt;/li&gt;
&lt;li&gt;replication and persistence behaviors (as applicable)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-have-a-provider-swap-playbook"&gt;4) Have a “provider swap” playbook&lt;/h3&gt;
&lt;p&gt;Define what changes in your stack when the backing implementation changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;infrastructure configuration&lt;/li&gt;
&lt;li&gt;deployment pipeline steps&lt;/li&gt;
&lt;li&gt;monitoring and alerting assumptions&lt;/li&gt;
&lt;li&gt;rollback procedures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your goal isn’t to be ready for every scenario. It’s to be ready for the one that’s most common: “the provider changed the terms.”&lt;/p&gt;
&lt;h3 id="5-coordinate-with-platform-and-legal-early"&gt;5) Coordinate with platform and legal early&lt;/h3&gt;
&lt;p&gt;One lesson from licensing-driven transitions: engineering isn’t the only bottleneck. Procurement timelines, contract language, and internal policy reviews can drag out while systems keep running. Bring stakeholders into the process with clear technical compatibility evidence and an operational migration plan.&lt;/p&gt;
&lt;p&gt;If you wait until a contract deadline hits, you’ll end up doing emergency work with insufficient time to test.&lt;/p&gt;
&lt;h2 id="the-real-takeaway-forks-are-a-feature-of-open-infrastructure"&gt;The Real Takeaway: Forks Are a Feature of Open Infrastructure&lt;/h2&gt;
&lt;p&gt;Valkey’s success wasn’t just that the fork existed. It was that it was &lt;strong&gt;usable immediately&lt;/strong&gt;: compatible interfaces, compatible data, and broad backing. That’s how you keep momentum while others scramble.&lt;/p&gt;
&lt;p&gt;In the broader open-source ecosystem, forks are often portrayed as messy. But they can be orderly and constructive when the project’s core interface is stable and widely adopted. More importantly, the community demonstrated something executives and architects should notice:&lt;/p&gt;
&lt;p&gt;Open-source infrastructure with standard protocols isn’t fragile. It’s resilient by design.&lt;/p&gt;
&lt;p&gt;And that resilience matters now, in an era where corporate control can change overnight through licensing decisions, distribution terms, or “enterprise-only” constraints. The best defense isn’t panic. It’s architecture—and the quiet confidence that the ecosystem can carry users forward when one company’s incentives diverge.&lt;/p&gt;
&lt;h2 id="conclusion-redis-outlived-the-decision-not-the-protocol"&gt;Conclusion: Redis Outlived the Decision, Not the Protocol&lt;/h2&gt;
&lt;p&gt;Valkey proved that when an open-source project is anchored to a stable protocol and a broad ecosystem, users don’t have to choose between compliance and continuity. They can switch providers quickly, keep APIs steady, preserve data compatibility, and continue shipping—without turning every licensing change into a crisis.&lt;/p&gt;
&lt;p&gt;The Redis protocol will outlive any single company’s licensing decisions because the protocol is bigger than the company. That’s the kind of infrastructure story worth building for.&lt;/p&gt;</content></item><item><title>FastAPI's +5 Point Surge Signals Python's Async-First Future</title><link>https://decastro.work/blog/fastapi-5-point-surge-python-async-first/</link><pubDate>Thu, 12 Jun 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/fastapi-5-point-surge-python-async-first/</guid><description>&lt;p&gt;For years, Python web development lived with a quiet tradeoff: either you got speed and clarity, or you got “flexibility” that quickly turned into ambiguity. FastAPI didn’t just improve one API—it changed what developers expect from frameworks. The latest momentum—often described as a noticeable usage uptick—shouldn’t be read as a single team winning. It’s a signal that Python’s ecosystem is moving toward async-first behavior, type-driven correctness, and documentation that isn’t an afterthought.&lt;/p&gt;</description><content>&lt;p&gt;For years, Python web development lived with a quiet tradeoff: either you got speed and clarity, or you got “flexibility” that quickly turned into ambiguity. FastAPI didn’t just improve one API—it changed what developers expect from frameworks. The latest momentum—often described as a noticeable usage uptick—shouldn’t be read as a single team winning. It’s a signal that Python’s ecosystem is moving toward async-first behavior, type-driven correctness, and documentation that isn’t an afterthought.&lt;/p&gt;
&lt;p&gt;This is how FastAPI forced the conversation, and why competitors like Litestar—and even heavyweight incumbents like Django—are now responding with the same underlying message: guardrails beat guesswork.&lt;/p&gt;
&lt;h2 id="fastapis-real-innovation-wasnt-speedit-was-intent"&gt;FastAPI’s Real Innovation Wasn’t Speed—It Was Intent&lt;/h2&gt;
&lt;p&gt;Yes, FastAPI can be fast. But speed isn’t why teams adopt it and stay. The real innovation is that it turns developer intent into machine-checkable structure—then uses that structure to automate the boring parts: validation, serialization, and documentation.&lt;/p&gt;
&lt;p&gt;The signature move was leveraging Python type hints as a first-class specification. Instead of writing parallel schemas, validation logic, and OpenAPI descriptions by hand, you express your data model once:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; typing &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Optional
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; FastAPI
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pydantic &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BaseModel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserIn&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name: str
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email: str
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; age: Optional[int] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/users&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;create_user&lt;/span&gt;(payload: UserIn):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;: payload}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;UserIn&lt;/code&gt; type isn’t “just typing.” In practice, it becomes input validation, shapes the JSON schema, and drives the generated API docs. This is the opposite of the old Flask-era vibe: “figure it out yourself.”&lt;/p&gt;
&lt;p&gt;Opinionated frameworks don’t remove freedom—they remove uncertainty.&lt;/p&gt;
&lt;h3 id="the-guardrails-developers-actually-feel"&gt;The guardrails developers actually feel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Validation happens automatically&lt;/strong&gt; based on your declared types.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Errors become consistent&lt;/strong&gt; and easier to troubleshoot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docs are accurate by construction&lt;/strong&gt; because they’re derived from your types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When your API is a living product, “accurate by construction” is a competitive advantage.&lt;/p&gt;
&lt;h2 id="async-first-doesnt-mean-always-asyncit-means-designed-for-concurrency"&gt;Async-First Doesn’t Mean “Always Async”—It Means Designed for Concurrency&lt;/h2&gt;
&lt;p&gt;FastAPI made async feel natural rather than bolted on. But the more important shift is conceptual: the framework is built around non-blocking request handling, which aligns with how modern Python systems behave—event loops, concurrent I/O, and streaming responses.&lt;/p&gt;
&lt;p&gt;You can write sync endpoints, but the default path encourages async where it matters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; FastAPI
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; httpx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/weather&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;weather&lt;/span&gt;(city: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; httpx&lt;span style="color:#f92672"&gt;.&lt;/span&gt;AsyncClient() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; client:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; r &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;https://example.com/weather?city=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;city&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; r&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isn’t just about “use &lt;code&gt;async def&lt;/code&gt;.” It’s about building a framework where concurrency is a first-order concern: middleware, dependency injection, background tasks, and streaming can all cooperate with the async model instead of fighting it.&lt;/p&gt;
&lt;p&gt;Practical advice: treat async as an I/O contract. If your endpoint touches the network, the database, or the filesystem, async is often worth it. If you’re doing CPU-heavy work, keep it out of the event loop—use worker processes or task queues.&lt;/p&gt;
&lt;h2 id="type-hints-became-a-product-feature-not-developer-ornamentation"&gt;Type Hints Became a Product Feature, Not Developer Ornamentation&lt;/h2&gt;
&lt;p&gt;Python’s typing story has matured for years, but FastAPI turned type hints into a customer-facing feature. And that’s a subtle but decisive change.&lt;/p&gt;
&lt;p&gt;In classic setups, types are for IDEs and static analysis; runtime behavior still depends on handwritten validation and serialization. FastAPI collapses that separation. When types are enforced at runtime, they become part of your API’s contract.&lt;/p&gt;
&lt;p&gt;This is also why FastAPI-style ecosystems tend to feel “self-documenting.” When your endpoint signature is declarative, the system can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;generate accurate OpenAPI schemas,&lt;/li&gt;
&lt;li&gt;enforce constraints like &lt;code&gt;Optional&lt;/code&gt;, lists, enums, and nested models,&lt;/li&gt;
&lt;li&gt;document parameter metadata without duplicating work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical example: constrained inputs save you from an entire category of bugs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pydantic &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BaseModel, EmailStr, Field
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; FastAPI
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Signup&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email: EmailStr
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; password: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Field(min_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you’ve built enforcement into the contract. Your users get clear validation feedback, and your logs stop filling up with predictable “bad request” chaos.&lt;/p&gt;
&lt;p&gt;If you’ve ever supported a production API with loosely validated payloads, you already know why teams gravitate toward type-driven frameworks.&lt;/p&gt;
&lt;h2 id="litestars-challenge-same-philosophy-different-edges"&gt;Litestar’s Challenge: Same Philosophy, Different Edges&lt;/h2&gt;
&lt;p&gt;FastAPI didn’t create the desire for type-driven APIs, but it proved it could become mainstream. That’s why Litestar’s rise matters. It’s not “yet another web framework.” It’s evidence that the philosophy—typed contracts, async capability, and reduced boilerplate—is now competitive territory.&lt;/p&gt;
&lt;p&gt;Litestar positions itself as modern and developer-experience focused, aiming to deliver a similar promise: write clean Python, get structured request/response behavior, and keep the API documentation aligned with reality.&lt;/p&gt;
&lt;p&gt;The practical question for teams isn’t “Is Litestar better than FastAPI?” It’s “Do we want the async-first, type-driven ergonomics—and are we willing to bet on the ecosystem around it?” If your team cares about long-term maintainability, that philosophy is increasingly the default expectation.&lt;/p&gt;
&lt;p&gt;A simple migration mindset helps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start new services in the framework whose development ergonomics your team already trusts.&lt;/li&gt;
&lt;li&gt;When migrating legacy code, focus on one vertical slice: request models + validation + docs + one database-backed endpoint.&lt;/li&gt;
&lt;li&gt;Keep endpoint logic pure and isolate side effects so async execution and testing stay manageable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Framework choice matters less than the architectural pattern: typed boundaries + explicit I/O + predictable errors.&lt;/p&gt;
&lt;h2 id="django-too-the-incumbent-is-moving-toward-async"&gt;Django, Too: The Incumbent Is Moving Toward Async&lt;/h2&gt;
&lt;p&gt;The most telling signal is that even Django—synonymous with batteries included and “just works”—has invested in async support. That doesn’t mean Django is suddenly “an async-only framework.” It means the ecosystem can’t ignore async anymore.&lt;/p&gt;
&lt;p&gt;Why this matters: Django’s adoption pattern is conservative. Teams use it because it’s stable, familiar, and supported. When Django commits effort toward async, it sends a clear message: the market expects concurrency-ready web stacks as a baseline, not an optional add-on.&lt;/p&gt;
&lt;p&gt;The broader point is cultural. “Figure it out yourself” is increasingly unacceptable when teams need predictable behavior under load, consistent request validation, and reliable documentation for integrators.&lt;/p&gt;
&lt;p&gt;FastAPI helped shift expectations. Now the whole ecosystem is converging toward the same outcomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;async-first capability,&lt;/li&gt;
&lt;li&gt;type-driven contracts,&lt;/li&gt;
&lt;li&gt;automated documentation,&lt;/li&gt;
&lt;li&gt;guardrails that scale with teams and complexity.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-this-means-for-your-next-python-service"&gt;What This Means for Your Next Python Service&lt;/h2&gt;
&lt;p&gt;If you’re building a new API—or rewriting one that’s stuck in documentation drift and validation chaos—here’s the opinionated guidance that falls out of the FastAPI-led wave.&lt;/p&gt;
&lt;h3 id="choose-a-framework-that-treats-types-as-runtime-contracts"&gt;Choose a framework that treats types as runtime contracts&lt;/h3&gt;
&lt;p&gt;Your endpoints should describe your data clearly, and your system should enforce it automatically. This reduces bugs and support burden. It also makes your API easier to integrate with, because consumers can trust the schema.&lt;/p&gt;
&lt;h3 id="make-async-decisions-based-on-io-boundaries"&gt;Make async decisions based on I/O boundaries&lt;/h3&gt;
&lt;p&gt;Async is not a religious requirement. But if your service spends most of its time waiting—databases, HTTP calls, message queues—async-first design reduces waste and improves throughput. If you do heavy CPU work, offload it.&lt;/p&gt;
&lt;h3 id="dont-confuse-documentation-with-truth"&gt;Don’t confuse documentation with truth&lt;/h3&gt;
&lt;p&gt;If your docs are hand-maintained, they will drift. Prefer frameworks where your declared models generate the OpenAPI schema. This is how you keep contracts stable even as code evolves.&lt;/p&gt;
&lt;h3 id="treat-validation-as-part-of-your-public-api"&gt;Treat validation as part of your public API&lt;/h3&gt;
&lt;p&gt;Validation isn’t only about protecting your database. It’s about giving clients predictable error shapes and clear constraints. Typed models help you get this right without writing a custom validator for every field.&lt;/p&gt;
&lt;h2 id="conclusion-fastapis-surge-is-a-forecast-not-a-trend"&gt;Conclusion: FastAPI’s Surge Is a Forecast, Not a Trend&lt;/h2&gt;
&lt;p&gt;FastAPI’s momentum isn’t merely a single framework’s success story. It’s a forecast of where Python web development is headed: async-first concurrency built into the foundations, and type hints elevated into real runtime contracts that generate accurate behavior and documentation automatically.&lt;/p&gt;
&lt;p&gt;The era of “flexibility over guarantees” is ending—not because Python lost its soul, but because teams found something better: guardrails that don’t slow you down, and APIs that explain themselves while staying correct.&lt;/p&gt;</content></item><item><title>Why PostgreSQL's Community Is Its Biggest Competitive Advantage</title><link>https://decastro.work/blog/postgresql-community-biggest-competitive-advantage/</link><pubDate>Thu, 05 Jun 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgresql-community-biggest-competitive-advantage/</guid><description>&lt;p&gt;Most databases try to win on features—better indexing, faster joins, nicer tooling. PostgreSQL wins on something harder to copy: a community that turns upgrades into ecosystems. Extensions expand Postgres without bloating the core. Conferences spread hard-won lessons faster than any marketing cycle. And a contributor culture that’s distributed across companies—not owned by one—keeps the project resilient.&lt;/p&gt;
&lt;p&gt;If you’ve ever felt that proprietary platforms grow “only when the vendor wants,” PostgreSQL’s community model will feel like a breath of fresh air. Here’s why that matters, and how to think about it when choosing a database strategy.&lt;/p&gt;</description><content>&lt;p&gt;Most databases try to win on features—better indexing, faster joins, nicer tooling. PostgreSQL wins on something harder to copy: a community that turns upgrades into ecosystems. Extensions expand Postgres without bloating the core. Conferences spread hard-won lessons faster than any marketing cycle. And a contributor culture that’s distributed across companies—not owned by one—keeps the project resilient.&lt;/p&gt;
&lt;p&gt;If you’ve ever felt that proprietary platforms grow “only when the vendor wants,” PostgreSQL’s community model will feel like a breath of fresh air. Here’s why that matters, and how to think about it when choosing a database strategy.&lt;/p&gt;
&lt;h2 id="the-real-moat-isnt-coreits-the-extension-ecosystem"&gt;The real moat isn’t core—it&amp;rsquo;s the extension ecosystem&lt;/h2&gt;
&lt;p&gt;It’s easy to look at PostgreSQL feature checklists and conclude “other databases can do that too.” The more interesting question is: &lt;em&gt;How does the system evolve without turning into a kitchen sink?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;PostgreSQL’s extension model is the answer. Extensions let the database grow capabilities as opt-in add-ons, usually in separate packages with clear boundaries. You can adopt what you need without dragging the entire world into a monolith of features.&lt;/p&gt;
&lt;p&gt;Think about workflows that typically belong outside the database core:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auth and identity integration&lt;/strong&gt;: You can add what you need—without forcing every installation to carry the same assumptions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom indexing strategies&lt;/strong&gt;: When a team needs something specific (for example, a new way to index a domain type), extension authors can build it without waiting for a global release cycle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Specialized data features&lt;/strong&gt;: Geospatial, time-series helpers, vector tooling, and operational utilities often arrive through extensions and community libraries rather than kernel-level rewrites.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practically, this changes how you plan upgrades. In many proprietary ecosystems, “new capability” often means “new licensed version,” sometimes with migration pain. With PostgreSQL, new capability frequently means “install an extension” and move on. Your roadmap becomes modular.&lt;/p&gt;
&lt;p&gt;And that modularity compounds. Once an extension has users, it tends to attract maintenance contributors, bug fixes, performance tuning, documentation, and integration work. The ecosystem doesn’t just add features—it improves them in the open.&lt;/p&gt;
&lt;h2 id="community-contributions-beat-vendor-roadmaps"&gt;Community contributions beat vendor roadmaps&lt;/h2&gt;
&lt;p&gt;Proprietary databases operate on a simple premise: the vendor decides what matters, when it ships, and how it’s supported. That’s not inherently bad—but it does create a predictable dynamic: innovation tends to lag behind community experimentation because the experimental work has to pass through a gatekeeper.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s model is fundamentally different. It treats contributions as a first-class development pipeline. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ideas originate everywhere&lt;/strong&gt;: from startups, from enterprise teams, from researchers, from individuals who simply care.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The best solutions have to earn their place&lt;/strong&gt;: they go through review, testing, and iteration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The project can absorb innovation without adopting everyone’s worldview&lt;/strong&gt;: extensions help keep the core clean while still enabling growth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is not that PostgreSQL is “more innovative than any vendor.” It’s that the incentives are aligned with long-term improvement. When contributors upstream changes, the benefits aren’t trapped behind a product SKU. They become part of the common foundation.&lt;/p&gt;
&lt;p&gt;That’s the competitive difference. Features are easy to clone. A living contribution culture is not.&lt;/p&gt;
&lt;h2 id="conferences-create-a-knowledge-network-not-just-hype"&gt;Conferences create a knowledge network, not just hype&lt;/h2&gt;
&lt;p&gt;Software communities don’t scale with Git commits alone. They scale with shared context—what works, what breaks, and what’s actually maintainable in production.&lt;/p&gt;
&lt;p&gt;This is where PostgreSQL conferences matter. PGConf events across regions do more than entertain. They turn practical experience into reusable patterns. You get sessions on extension design, replication trade-offs, performance investigations, migrations, and operational playbooks—not just “look at our demo.”&lt;/p&gt;
&lt;p&gt;Here’s what that means for teams making real decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architectural clarity&lt;/strong&gt;: You hear how people structure workloads, partitioning strategies, and indexing approaches under constraints similar to yours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster troubleshooting&lt;/strong&gt;: When a bug or behavior shows up, there’s often precedent in community discussions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better integration instincts&lt;/strong&gt;: You learn where extension boundaries make sense and where they don’t.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The strongest conferences share one trait: they reward depth. They aren’t only about features; they’re about how to make those features reliable. That operational knowledge is what many teams struggle to acquire quickly—especially when they’re trying to build on a database that isn’t “their team’s database,” but rather a dependency they can’t afford to misunderstand.&lt;/p&gt;
&lt;h2 id="distributed-companies-shared-ownership"&gt;Distributed companies, shared ownership&lt;/h2&gt;
&lt;p&gt;One of the most underappreciated advantages of PostgreSQL is that no single company controls the project. That doesn’t mean there’s no corporate involvement—it means corporate involvement is accountable to the project and the community, not to a private roadmap.&lt;/p&gt;
&lt;p&gt;This distributed ownership shows up in the services ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Neon&lt;/strong&gt; focuses on Postgres-based cloud offerings with modern scaling approaches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supabase&lt;/strong&gt; builds developer-centric platforms on top of Postgres for application teams.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crunchy Data&lt;/strong&gt; supports production-grade deployments, operational tooling, and lifecycle management.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tembo&lt;/strong&gt; offers managed capabilities around Postgres and extensions, helping teams move faster without giving up the flexibility of the underlying database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These companies compete in the market, but many also contribute upstream—whether that’s improving extensions, sharing integration learnings, or helping stabilize the ecosystem.&lt;/p&gt;
&lt;p&gt;The practical consequence is trust. When your database is backed by a community where multiple organizations have skin in the game, your risk profile changes. You’re less likely to face sudden product pivots, deprecations without meaningful alternatives, or “platform lock-in” that’s really just a business decision.&lt;/p&gt;
&lt;p&gt;In other words: community isn’t only a technical advantage. It’s a governance advantage.&lt;/p&gt;
&lt;h2 id="sustainable-open-source-looks-like-a-feedback-loop"&gt;Sustainable open source looks like a feedback loop&lt;/h2&gt;
&lt;p&gt;A lot of open source projects begin as code and end as abandoned repositories. PostgreSQL is different because it has a feedback loop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;People use Postgres in real systems.&lt;/li&gt;
&lt;li&gt;They build extensions, tools, and integrations to solve practical problems.&lt;/li&gt;
&lt;li&gt;Those solutions accumulate users.&lt;/li&gt;
&lt;li&gt;Community maintainers and contributors refine and upstream the best ideas.&lt;/li&gt;
&lt;li&gt;The improved ecosystem enables broader adoption and more contributors.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This creates momentum that doesn’t depend on a single funding cycle or a single product manager’s priorities.&lt;/p&gt;
&lt;p&gt;You can see the loop in the way extension ecosystems often mature: initial implementations solve a problem, then the community gradually improves performance, documentation, compatibility, and operational tooling. The “shape” of the system stays flexible because growth is compartmentalized.&lt;/p&gt;
&lt;p&gt;And because the core remains stable, you avoid the “big-bang evolution” problem. Teams can adopt new capabilities without constantly rewriting everything around the database. That’s a major reason Postgres continues to be a sensible default for organizations that expect to evolve their applications for years.&lt;/p&gt;
&lt;h2 id="how-to-capitalize-on-postgresqls-community-advantage-not-just-admire-it"&gt;How to capitalize on PostgreSQL’s community advantage (not just admire it)&lt;/h2&gt;
&lt;p&gt;The smartest teams don’t just pick PostgreSQL—they leverage the ecosystem consciously. Here are practical ways to do that:&lt;/p&gt;
&lt;h3 id="1-start-with-the-extension-model-but-dont-overextend"&gt;1) Start with the extension model, but don’t overextend&lt;/h3&gt;
&lt;p&gt;Before you bolt on ten extensions, map your requirements to clear boundaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What belongs in the database (data integrity, core query needs)?&lt;/li&gt;
&lt;li&gt;What belongs in the application (business logic, orchestration)?&lt;/li&gt;
&lt;li&gt;What belongs in the ecosystem (indexing helpers, operational tooling, specialized types)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A smaller, well-chosen extension set is easier to maintain and upgrade.&lt;/p&gt;
&lt;h3 id="2-treat-community-adoption-as-an-engineering-discipline"&gt;2) Treat community adoption as an engineering discipline&lt;/h3&gt;
&lt;p&gt;When you adopt an extension, do the same due diligence you would for internal libraries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Verify compatibility with your PostgreSQL version.&lt;/li&gt;
&lt;li&gt;Review update cadence and maintainers.&lt;/li&gt;
&lt;li&gt;Test performance under realistic workloads.&lt;/li&gt;
&lt;li&gt;Document rollback paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Community support is strong, but you still need internal operational rigor.&lt;/p&gt;
&lt;h3 id="3-build-a-habit-of-reading-and-participating"&gt;3) Build a habit of reading and participating&lt;/h3&gt;
&lt;p&gt;If you maintain anything around Postgres—migrations, operational scripts, custom extensions, query tooling—contribute back when you can. Even small contributions (bug fixes, documentation improvements, example recipes) reduce future maintenance burden for everyone, including you.&lt;/p&gt;
&lt;p&gt;You don’t need to be a core committer. You need to be part of the feedback loop.&lt;/p&gt;
&lt;h3 id="4-use-conferences-to-accelerate-your-internal-playbook"&gt;4) Use conferences to accelerate your internal playbook&lt;/h3&gt;
&lt;p&gt;Send engineers who own production concerns. Ask them to capture learnings immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what patterns are recommended,&lt;/li&gt;
&lt;li&gt;what trade-offs are common,&lt;/li&gt;
&lt;li&gt;what pitfalls showed up in real deployments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then turn that knowledge into internal documentation and decision templates. The value of conferences isn’t the talk—it’s what you operationalize afterward.&lt;/p&gt;
&lt;h2 id="conclusion-postgres-wins-because-the-community-keeps-compounding"&gt;Conclusion: Postgres wins because the community keeps compounding&lt;/h2&gt;
&lt;p&gt;PostgreSQL’s competitive advantage isn’t that it has every feature under the sun today. Plenty of databases can match capabilities in isolation. PostgreSQL’s lead comes from compounding: an extension ecosystem that scales without core bloat, a contribution culture that isn’t trapped behind a single vendor roadmap, and a global set of conferences that turn hard-won production lessons into shared knowledge.&lt;/p&gt;
&lt;p&gt;In the long run, sustainable advantage belongs to communities that keep improving the platform &lt;em&gt;after&lt;/em&gt; you choose it. PostgreSQL’s community doesn’t just support users—it continuously upgrades the system’s future. That’s why its momentum isn’t a trend. It’s a trajectory.&lt;/p&gt;</content></item><item><title>The State of Rust in 2025: Beyond Systems Programming</title><link>https://decastro.work/blog/state-of-rust-2025-beyond-systems-programming/</link><pubDate>Sat, 31 May 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/state-of-rust-2025-beyond-systems-programming/</guid><description>&lt;p&gt;Rust’s reputation used to be a narrow one: “systems programming, but safer.” In 2025, that framing feels like an old map. Yes, Rust is still excellent at low-level work—but its ecosystem has broadened so aggressively that “beyond systems” isn’t a side quest anymore. It’s the main plot.&lt;/p&gt;
&lt;p&gt;What changed isn’t that Rust suddenly became something it isn’t. It’s that the supporting cast—frameworks, tooling, deployment paths, and libraries—matured enough to make full products practical. If you’re still treating Rust as a specialty language, you’re leaving capability on the table.&lt;/p&gt;</description><content>&lt;p&gt;Rust’s reputation used to be a narrow one: “systems programming, but safer.” In 2025, that framing feels like an old map. Yes, Rust is still excellent at low-level work—but its ecosystem has broadened so aggressively that “beyond systems” isn’t a side quest anymore. It’s the main plot.&lt;/p&gt;
&lt;p&gt;What changed isn’t that Rust suddenly became something it isn’t. It’s that the supporting cast—frameworks, tooling, deployment paths, and libraries—matured enough to make full products practical. If you’re still treating Rust as a specialty language, you’re leaving capability on the table.&lt;/p&gt;
&lt;h2 id="web-development-rust-is-no-longer-a-thought-experiment"&gt;Web development: Rust is no longer a thought experiment&lt;/h2&gt;
&lt;p&gt;For years, the argument against Rust web development was less about performance and more about ergonomics and ecosystem breadth. That conversation has shifted. You don’t have to assemble a web stack from scraps anymore, and you don’t have to compromise on maintainability to get “Rust-level correctness.”&lt;/p&gt;
&lt;p&gt;On the server side, Axum and Actix-web have become the kinds of frameworks people actually build businesses with. Axum’s routing and extraction model makes request handling feel clean and composable—useful when your API surface grows beyond simple CRUD. Actix-web, meanwhile, shines when you want a mature, battle-tested event-driven approach and fine control over concurrency patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; imagine building an API gateway that routes to internal services, applies auth, logs structured events, and enforces rate limiting. In Axum, you can structure this as layers/middleware and request extractors so the core handlers stay readable. In Actix-web, the ecosystem supports a straightforward decomposition into services, middleware, and async handlers—without forcing you into a framework style that doesn’t match your team.&lt;/p&gt;
&lt;p&gt;On the frontend side, the ecosystem has also moved past “hello world.” Leptos targets a reactive programming model that pairs well with Rust’s strengths: explicit state, predictable data flow, and tight integration between backend and UI logic. Dioxus brings a component-based mental model familiar to React users, but with Rust at the core. The result is a path to full-stack development where your server and UI can share types and build on the same language semantics.&lt;/p&gt;
&lt;p&gt;If your organization has a TypeScript-heavy web stack, adopting Rust for the frontend can be incremental: start with a small internal tool, admin dashboard, or performance-sensitive UI, and expand only when the developer experience holds up.&lt;/p&gt;
&lt;h2 id="cli-and-tooling-rusts-sweet-spot-is-productivity-now"&gt;CLI and tooling: Rust’s sweet spot is productivity now&lt;/h2&gt;
&lt;p&gt;Rust has always been good at shipping reliable binaries. In 2025, that reliability comes packaged with ergonomics: argument parsing libraries that fit well into real codebases, solid patterns for logging and configuration, and build tooling that integrates cleanly with CI/CD.&lt;/p&gt;
&lt;p&gt;For CLI apps, the story is straightforward: Rust is fantastic when you want predictable behavior, fast startup, stable dependency management, and binaries that you can distribute without turning your infrastructure into a science project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; you’re building a deployment helper that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;validates environment configuration,&lt;/li&gt;
&lt;li&gt;packages a build artifact,&lt;/li&gt;
&lt;li&gt;uploads it to storage,&lt;/li&gt;
&lt;li&gt;triggers a remote deploy,&lt;/li&gt;
&lt;li&gt;prints a human-readable summary plus machine-readable logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust lets you model the workflow as a set of explicit steps with strong error handling. The “important” part is not the syntax—it’s how errors remain structured instead of dissolving into strings. When something breaks (an auth token is missing, a path is wrong, a network call fails), your CLI can exit with useful codes and actionable messages.&lt;/p&gt;
&lt;p&gt;Opinionated takeaway: if you’re still shipping operational tools in loosely typed scripts, Rust CLIs are the upgrade path that tends to pay back quickly—in fewer support tickets, faster debugging, and fewer “works on my machine” incidents.&lt;/p&gt;
&lt;h2 id="wasm-the-performance-story-is-real-but-the-constraints-are-clearer"&gt;WASM: the performance story is real, but the constraints are clearer&lt;/h2&gt;
&lt;p&gt;WebAssembly in 2025 is not “Rust’s magic trick.” It’s an engineering tool with constraints you need to respect: sandboxing rules, interop boundaries, bundle size considerations, and the reality that not every workload benefits from moving to WASM.&lt;/p&gt;
&lt;p&gt;But Rust remains one of the cleanest ways to produce WASM modules that are memory-safe and maintainable—especially when you’re compiling predictable computational workloads. Think:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;image processing kernels,&lt;/li&gt;
&lt;li&gt;parsers and validators,&lt;/li&gt;
&lt;li&gt;encryption/decryption helpers,&lt;/li&gt;
&lt;li&gt;deterministic transformation pipelines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; suppose you need to validate and normalize user-provided files before sending them to your backend. A WASM module can run locally in the browser for responsive UX—while your server repeats the same logic for correctness. Rust’s type system and explicit error handling make it far easier to keep the behavior consistent across environments.&lt;/p&gt;
&lt;p&gt;The “gotcha” to plan for is interface design. Keep the surface area between JS and WASM narrow: avoid chatty calls, batch work, and design clear data formats. If you treat WASM as a low-level compute engine with a tight API, you’ll get a system that feels fast and reliable rather than fragile.&lt;/p&gt;
&lt;h2 id="desktop-apps-tauri-turns-rust-for-ui-into-a-deployment-advantage"&gt;Desktop apps: Tauri turns “Rust for UI” into a deployment advantage&lt;/h2&gt;
&lt;p&gt;Desktop apps used to mean heavyweight frameworks. Electron made sense when iteration speed dominated everything else. But as teams matured, the pain points became obvious: memory footprint, slower cold starts, and bundling overhead. Tauri changes the equation by letting Rust own the core and wrapping a lightweight webview for UI.&lt;/p&gt;
&lt;p&gt;In practice, this affects more than performance. It affects how you ship updates, how you manage resource usage on lower-end machines, and how you build apps that behave consistently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; build a cross-platform desktop companion for a developer tool—syncing state, managing local configuration, and providing an audit-style UI with a web tech stack. With Tauri, the Rust side can handle the “real work”: filesystem access (where appropriate), process orchestration, encryption, and communication with your backend. The web UI can remain familiar for frontend developers, while the system-level functionality stays in Rust’s safer domain.&lt;/p&gt;
&lt;p&gt;The key point: Tauri isn’t just “Electron but smaller.” It’s a different architecture. Your developers stop treating the backend and desktop core as a JavaScript problem and start treating it like a product-grade systems problem—with Rust in the driver’s seat.&lt;/p&gt;
&lt;h2 id="embedded-rust-is-no-longer-a-research-project"&gt;Embedded: Rust is no longer a research project&lt;/h2&gt;
&lt;p&gt;Embedded isn’t just “Rust as a better C.” In many cases it’s Rust as the language that makes codebases sustainable—because ownership and types reduce entire categories of bugs that are expensive to diagnose when you can’t replicate hardware states on your desk.&lt;/p&gt;
&lt;p&gt;The embedded Rust ecosystem is mature enough that projects are serious, not experimental. You’ll see libraries and tooling aimed at microcontrollers, drivers, and runtime support designed for production realities: constrained memory, tight timing, and the need for predictable behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical example:&lt;/strong&gt; you’re implementing firmware for a sensor node that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reads data on an interrupt-driven schedule,&lt;/li&gt;
&lt;li&gt;buffers samples safely,&lt;/li&gt;
&lt;li&gt;encodes packets for transmission,&lt;/li&gt;
&lt;li&gt;handles power state transitions,&lt;/li&gt;
&lt;li&gt;reports errors in a way you can interpret in the field.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust’s value is not that it’s “safe” in a marketing sense—it’s that it forces you to make resource ownership explicit. Concurrency bugs and memory corruption are still possible in embedded systems, but Rust reduces the surface area drastically and encourages design patterns that stay correct as features expand.&lt;/p&gt;
&lt;p&gt;In embedded, you also learn quickly what to avoid: complicated abstractions where determinism matters, overly dynamic behavior, and designs that create unclear ownership boundaries. The upside is that these constraints push teams toward cleaner architecture from day one.&lt;/p&gt;
&lt;h2 id="the-meta-story-tooling-and-ecosystem-maturity"&gt;The meta-story: tooling and ecosystem maturity&lt;/h2&gt;
&lt;p&gt;It’s tempting to reduce Rust’s 2025 state to frameworks and examples. But the real shift is operational: the ecosystem behaves like it’s built for teams.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency management that works predictably across projects,&lt;/li&gt;
&lt;li&gt;consistent async patterns and integration points,&lt;/li&gt;
&lt;li&gt;build and test workflows that don’t require ritual,&lt;/li&gt;
&lt;li&gt;clear pathways from prototype to deployable artifacts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The “Rust is only for systems programming” era was never about Rust’s capability. It was about confidence—confidence that you could build a complete product without fighting your tools every week. In 2025, that confidence has matured because the ecosystem learned what product teams need: stable libraries, good documentation habits, ergonomic APIs, and straightforward deployment stories.&lt;/p&gt;
&lt;p&gt;And this is where Rust’s advantage compounds. When you can reuse Rust patterns and types across server, CLI, desktop core, and WASM compute modules, your team stops context-switching. You ship fewer rewrites, you get more shared understanding, and you reduce the friction between frontend and backend decisions.&lt;/p&gt;
&lt;h2 id="conclusion-rust-is-a-product-platform-now"&gt;Conclusion: Rust is a product platform now&lt;/h2&gt;
&lt;p&gt;Rust in 2025 is no longer a niche language with a strong pitch. It’s a platform with breadth: web servers that scale, reactive UI approaches that ship, CLI tooling that’s actually maintainable, WASM modules that behave like serious components, desktop apps that don’t punish your users’ machines, and embedded codebases that can last.&lt;/p&gt;
&lt;p&gt;If you’re evaluating Rust for a new project today, don’t ask whether it can do the job—ask whether your team wants the benefits Rust unlocks: correctness, predictable behavior, and a coherent ecosystem that supports real applications end to end.&lt;/p&gt;</content></item><item><title>TypeScript Just Overtook Python and JavaScript on GitHub</title><link>https://decastro.work/blog/typescript-overtook-python-javascript-github/</link><pubDate>Mon, 19 May 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/typescript-overtook-python-javascript-github/</guid><description>&lt;p&gt;For years, TypeScript’s pitch was simple: add types to JavaScript, get safer code, catch bugs earlier. But the real story now is different. TypeScript has quietly become the language most teams want in 2026—not only because it’s more reliable, but because it’s more &lt;em&gt;understandable&lt;/em&gt; by the AI tools that increasingly write, refactor, and autocomplete our code. The typed superset didn’t just win the type-safety debate. It won the AI-context debate, and that’s reshaping developer behavior.&lt;/p&gt;</description><content>&lt;p&gt;For years, TypeScript’s pitch was simple: add types to JavaScript, get safer code, catch bugs earlier. But the real story now is different. TypeScript has quietly become the language most teams want in 2026—not only because it’s more reliable, but because it’s more &lt;em&gt;understandable&lt;/em&gt; by the AI tools that increasingly write, refactor, and autocomplete our code. The typed superset didn’t just win the type-safety debate. It won the AI-context debate, and that’s reshaping developer behavior.&lt;/p&gt;
&lt;h2 id="the-tipping-point-a-superset-that-behaves-like-a-destination"&gt;The tipping point: a “superset” that behaves like a destination&lt;/h2&gt;
&lt;p&gt;TypeScript started as a complement: a way to bring structure to the chaos of JavaScript. Yet the ecosystem evolved in a way that flipped the relationship. Modern TypeScript isn’t merely “JavaScript with types.” It’s a full development language, complete with patterns, tooling conventions, and a massive library surface built around its type system.&lt;/p&gt;
&lt;p&gt;Look at what teams actually do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They don’t just annotate a few variables—they define interfaces and types as the core of their design.&lt;/li&gt;
&lt;li&gt;They don’t just compile—they rely on type-driven APIs, typed schemas, and generics that encode intent.&lt;/li&gt;
&lt;li&gt;They don’t treat the type checker as optional—they treat it as part of the review process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That shift matters when you consider who your codebase is now written for. TypeScript was designed to help humans reason about software. But it also helps machines—especially LLMs—reason about it with far fewer guesses.&lt;/p&gt;
&lt;p&gt;Meanwhile, JavaScript remains flexible and productive, but it leaves both humans and AI tools to infer structure from runtime behavior, naming conventions, and convention-over-configuration patterns that can vary widely across teams. Python, similarly, is expressive and fast for scripting and data work, but its dynamic nature means AI tools must reconstruct likely types from usage patterns and docstrings rather than from an explicit contract.&lt;/p&gt;
&lt;p&gt;TypeScript became the “default language of intent.” And once that becomes the default, popularity follows.&lt;/p&gt;
&lt;h2 id="why-githubs-language-rankings-started-reflecting-reality"&gt;Why GitHub’s language rankings started reflecting reality&lt;/h2&gt;
&lt;p&gt;GitHub language rankings aren’t a perfect measure of “best language,” and they certainly aren’t a scientific study of developer satisfaction. But they do reflect a consistent behavior: where new code gets created and where projects invest.&lt;/p&gt;
&lt;p&gt;When TypeScript rises above both Python and JavaScript in those rankings, it signals something more than taste. It indicates that TypeScript is winning at the stage where language choice hardens into habit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New web applications are more often scaffolded with TypeScript.&lt;/li&gt;
&lt;li&gt;Libraries increasingly ship type definitions (or are written in TypeScript from the start).&lt;/li&gt;
&lt;li&gt;Tooling defaults—linters, IDE integrations, code generators—reinforce typed code as the path of least resistance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key point is this: TypeScript’s momentum is no longer just “JavaScript but safer.” It’s “a complete authoring environment” that fits how modern teams build: editors that provide rich context, CI pipelines that enforce contracts, and automation that depends on static knowledge.&lt;/p&gt;
&lt;h2 id="the-ai-argument-typescript-gives-llms-a-map-not-a-maze"&gt;The AI argument: TypeScript gives LLMs a map, not a maze&lt;/h2&gt;
&lt;p&gt;Here’s the blunt truth about how LLM coding assistance works in practice: it performs best when the model can see stable, explicit structure.&lt;/p&gt;
&lt;p&gt;Dynamic languages force the model to infer types from usage. That inference can work, but it’s fragile. It also makes the model’s job harder during generation and refactoring—exactly when mistakes are most costly.&lt;/p&gt;
&lt;p&gt;TypeScript changes the game by providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Explicit types&lt;/strong&gt; (e.g., &lt;code&gt;User&lt;/code&gt;, &lt;code&gt;OrderStatus&lt;/code&gt;, &lt;code&gt;ApiResponse&amp;lt;T&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function signatures&lt;/strong&gt; that constrain inputs and outputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generics&lt;/strong&gt; that encode relationships (e.g., “this function transforms &lt;code&gt;T&lt;/code&gt; into &lt;code&gt;U&lt;/code&gt;”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discriminated unions&lt;/strong&gt; that represent real-world state machines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Module boundaries&lt;/strong&gt; that help tools locate the right abstractions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An LLM-assisted workflow becomes dramatically more effective when the codebase contains these contracts in plain sight.&lt;/p&gt;
&lt;h3 id="concrete-example-endpoint-generation"&gt;Concrete example: endpoint generation&lt;/h3&gt;
&lt;p&gt;Imagine generating a frontend client for an API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In JavaScript&lt;/strong&gt;, an AI assistant might produce something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users&amp;#34;&lt;/span&gt;, { &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;) });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But “data” could be any shape, and “res.json()” could return anything. The assistant must guess. If your API returns &lt;code&gt;{ userId, email }&lt;/code&gt; in one case and &lt;code&gt;{ error }&lt;/code&gt; in another, the assistant has to infer behavior from prior calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In TypeScript&lt;/strong&gt;, the code can be explicit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateUserRequest&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateUserResponse&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;userId&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;CreateUserRequest&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;CreateUserResponse&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users&amp;#34;&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;JSON.stringify&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;headers&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#e6db74"&gt;&amp;#34;content-type&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;ok&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Error(&lt;span style="color:#e6db74"&gt;&amp;#34;Failed to create user&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;CreateUserResponse&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the AI assistant can generate the right request shape, understand what the response must contain, and propagate those types into downstream UI and state management without guessing. The model isn’t only writing code—it’s aligning with a contract.&lt;/p&gt;
&lt;h3 id="why-this-feels-like-typescript-isnt-a-superset-anymore"&gt;Why this feels like “TypeScript isn’t a superset anymore”&lt;/h3&gt;
&lt;p&gt;In the AI era, contracts aren’t just for compile-time correctness. They’re for &lt;em&gt;reasoning-time correctness&lt;/em&gt;. When TypeScript types permeate your codebase, the assistant has fewer degrees of freedom, which means fewer wrong branches, fewer missing fields, and less rewriting.&lt;/p&gt;
&lt;p&gt;In other words: TypeScript doesn’t merely improve correctness for humans. It improves correctness for the automation humans increasingly rely on.&lt;/p&gt;
&lt;h2 id="the-practical-benefits-teams-feel-immediately"&gt;The practical benefits teams feel immediately&lt;/h2&gt;
&lt;p&gt;The AI advantage is real, but it’s not abstract. It shows up as day-to-day friction reduction.&lt;/p&gt;
&lt;h3 id="1-fewer-mystery-objects-and-cleaner-refactors"&gt;1) Fewer “mystery objects” and cleaner refactors&lt;/h3&gt;
&lt;p&gt;Consider a refactor that changes a user model. With TypeScript, you can update a single type definition and let the compiler (and IDE) highlight every impacted usage. With AI assistance, the assistant also learns the new shape from types rather than from scattered runtime checks.&lt;/p&gt;
&lt;h3 id="2-better-autocomplete-and-safer-code-suggestions"&gt;2) Better autocomplete and safer code suggestions&lt;/h3&gt;
&lt;p&gt;Typed projects tend to have richer language server responses. That improves suggestions before you even reach for an AI assistant, and it reduces the chance that AI-generated code diverges from your project’s established patterns.&lt;/p&gt;
&lt;h3 id="3-stronger-boundaries-for-generated-code"&gt;3) Stronger boundaries for generated code&lt;/h3&gt;
&lt;p&gt;AI tools often generate code “locally” (a function here, a component there). TypeScript’s interfaces and type exports act like guardrails, so generated code plugs into the rest of the system with fewer integration bugs.&lt;/p&gt;
&lt;p&gt;A practical workflow many teams adopt looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define the types first (request/response, domain entities, state shapes).&lt;/li&gt;
&lt;li&gt;Generate or implement functions around those types.&lt;/li&gt;
&lt;li&gt;Let TypeScript drive the cleanup through compile errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not glamorous, but it’s effective—and it pairs extremely well with AI-assisted development.&lt;/p&gt;
&lt;h2 id="what-about-python-and-javascriptare-they-losing"&gt;What about Python and JavaScript—are they “losing”?&lt;/h2&gt;
&lt;p&gt;No. JavaScript and Python remain strong, especially where their ecosystems shine. But the selection pressure is shifting.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JavaScript&lt;/strong&gt; is still the language of the web, and many teams keep it for prototypes, scripts, and cases where overhead matters. However, as teams grow, typed contracts reduce ambiguity, and AI tooling makes that ambiguity more expensive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python&lt;/strong&gt; remains a powerhouse for data, automation, and backend services. But the move toward AI-assisted coding favors static structure. Python can add type hints, of course—but many Python codebases still rely on runtime dynamics and patterns that aren’t as consistently enforced as TypeScript’s end-to-end tooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important distinction is not “TypeScript is better than Python.” It’s “TypeScript has become the most efficient substrate for modern development tooling, including AI.”&lt;/p&gt;
&lt;p&gt;And that efficiency is compelling when the cost of a mistake is higher and the volume of changes is larger—exactly what happens when AI speeds up iteration.&lt;/p&gt;
&lt;h2 id="how-to-adopt-the-ai-friendly-typescript-mindset-without-rewriting-everything"&gt;How to adopt the “AI-friendly TypeScript” mindset (without rewriting everything)&lt;/h2&gt;
&lt;p&gt;If you’re thinking about TypeScript now—or already using it and want to get the most from AI tooling—focus on these high-leverage moves.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Make types part of your domain model&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Don’t only type variables. Type the concepts: &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Money&lt;/code&gt;, &lt;code&gt;UserSession&lt;/code&gt;, &lt;code&gt;FeatureFlag&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use discriminated unions for state&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Model UI and workflow states explicitly, instead of relying on boolean flags scattered across components.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer typed interfaces at module boundaries&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;The fastest wins come from typing request/response shapes, props, and public APIs between layers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Let the type checker lead&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;When integrating AI-generated code, treat type errors as a checklist. Don’t manually patch around them; fix them by aligning with the types.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid “type any” as a habit&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If you must cast, contain it. Otherwise, you erase the very contract AI tools need.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This isn’t a mandate to migrate overnight. It’s a mandate to converge your project around explicit contracts—because AI assistance performs best when the codebase is legible.&lt;/p&gt;
&lt;h2 id="conclusion-typescript-won-by-becoming-easier-to-build-withand-to-build-through"&gt;Conclusion: TypeScript won by becoming easier to build with—and to build through&lt;/h2&gt;
&lt;p&gt;TypeScript overtaking Python and JavaScript on GitHub isn’t just a popularity swing. It’s the visible result of a deeper shift: software development is increasingly mediated by AI tools, and those tools work far better when code carries explicit structure.&lt;/p&gt;
&lt;p&gt;Types used to be a compile-time safety net. Now they’re also an AI-context advantage. TypeScript didn’t merely complement JavaScript—it evolved into the language teams trust to express intent clearly, refactor confidently, and automate reliably. In the age of AI-assisted coding, that matters more than slogans about safety ever did.&lt;/p&gt;</content></item><item><title>The Three AI Skills Every Developer Needs by End of Year</title><link>https://decastro.work/blog/three-ai-skills-every-developer-needs/</link><pubDate>Tue, 13 May 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/three-ai-skills-every-developer-needs/</guid><description>&lt;p&gt;AI in software isn’t “optional tooling” anymore—it’s becoming a core capability. The same way every developer had to eventually learn version control, observability, and cloud basics, the AI era now demands a practical skill stack. And after watching too many teams stumble, there’s a clear answer: by the end of the year, you should be building competency in three areas—prompt engineering, RAG architecture, and agent orchestration.&lt;/p&gt;
&lt;p&gt;Not because you’ll memorize buzzwords. Because these three skills map directly to what production systems actually need: reliable instructions, grounded knowledge, and safe action.&lt;/p&gt;</description><content>&lt;p&gt;AI in software isn’t “optional tooling” anymore—it’s becoming a core capability. The same way every developer had to eventually learn version control, observability, and cloud basics, the AI era now demands a practical skill stack. And after watching too many teams stumble, there’s a clear answer: by the end of the year, you should be building competency in three areas—prompt engineering, RAG architecture, and agent orchestration.&lt;/p&gt;
&lt;p&gt;Not because you’ll memorize buzzwords. Because these three skills map directly to what production systems actually need: reliable instructions, grounded knowledge, and safe action.&lt;/p&gt;
&lt;h2 id="1-prompt-engineering-turn-chat-into-a-controllable-interface"&gt;1) Prompt engineering: turn “chat” into a controllable interface&lt;/h2&gt;
&lt;p&gt;Prompt engineering used to be a party trick. Now it’s the control layer. If your application depends on an LLM, you need prompts that behave like interfaces—predictable inputs, constrained outputs, and explicit assumptions.&lt;/p&gt;
&lt;p&gt;Start with &lt;strong&gt;system prompts&lt;/strong&gt; that define role, boundaries, and output contract. A good system prompt doesn’t just say “You are helpful.” It tells the model what to do when information is missing, how to format responses, and what not to do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example (system prompt for a support bot):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“You are a support agent for Acme Cloud.”&lt;/li&gt;
&lt;li&gt;“If the user asks for account-specific details, request verification steps rather than guessing.”&lt;/li&gt;
&lt;li&gt;“Output JSON with fields: &lt;code&gt;intent&lt;/code&gt;, &lt;code&gt;summary&lt;/code&gt;, &lt;code&gt;next_action&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last line—an output contract—changes everything. It reduces downstream parsing headaches and makes testing realistic.&lt;/p&gt;
&lt;p&gt;Next, use &lt;strong&gt;few-shot examples&lt;/strong&gt; to teach your model the patterns your product cares about. The key isn’t adding lots of examples; it’s picking the ones that represent your real edge cases: ambiguous requests, conflicting requirements, and “unknown” scenarios.&lt;/p&gt;
&lt;p&gt;For instance, if your system routes tickets by intent, include examples where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the user’s request is underspecified,&lt;/li&gt;
&lt;li&gt;the user’s request conflicts with policy,&lt;/li&gt;
&lt;li&gt;the user asks for something the bot cannot do.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, teams should stop treating “chain-of-thought” as magic. You don’t want to expose internal reasoning; you want &lt;strong&gt;reasoning behavior&lt;/strong&gt; you can validate. In practice, this means eliciting structured intermediate signals in a way that you can test—like “list key factors” or “produce a brief checklist before the final answer.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt;&lt;br&gt;
Build a small prompt test suite now. Keep 20–50 representative prompts in a repo, run them in CI, and record whether outputs still match your contract. Prompt quality degrades when models update, your product changes, or you tweak instructions—testing is how you catch it before users do.&lt;/p&gt;
&lt;h2 id="2-rag-architecture-retrieve-the-right-context-not-just-more-text"&gt;2) RAG architecture: retrieve the right context, not just more text&lt;/h2&gt;
&lt;p&gt;If prompt engineering is your interface, &lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt; is your truth layer. The core idea is simple: don’t rely on the model’s memory for enterprise knowledge. Instead, retrieve relevant documents and feed them into the model.&lt;/p&gt;
&lt;p&gt;But RAG only works when the architecture is designed, not when you “turn on embeddings.” In enterprise settings, most failures come down to retrieval quality, not generation quality.&lt;/p&gt;
&lt;h3 id="the-moving-parts-you-must-understand"&gt;The moving parts you must understand&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Embedding models&lt;/strong&gt;: Convert text into vectors so similarity search can work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vector stores&lt;/strong&gt;: Persist embeddings and enable retrieval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrieval strategy&lt;/strong&gt;: Decide how you fetch candidates (e.g., top-k, hybrid search).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reranking&lt;/strong&gt;: Re-order retrieved candidates with a stronger model to improve relevance.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="a-realistic-example-policy-qa"&gt;A realistic example: policy Q&amp;amp;A&lt;/h3&gt;
&lt;p&gt;Imagine an internal tool that answers: “What’s our retention policy for customer logs?”&lt;/p&gt;
&lt;p&gt;A naive RAG setup might retrieve a few chunks that vaguely relate to “security,” and the LLM will happily synthesize a plausible answer—wrong, but fluent.&lt;/p&gt;
&lt;p&gt;A better approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use chunking that matches document structure (headings, sections, tables where appropriate).&lt;/li&gt;
&lt;li&gt;Retrieve with a strategy tuned to your data (hybrid retrieval often helps when keywords matter).&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;reranking&lt;/strong&gt; so the final context is the most relevant, not merely the most similar.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-guidance-make-retrieval-observable"&gt;Practical guidance: make retrieval observable&lt;/h3&gt;
&lt;p&gt;Treat retrieval like a subsystem you can debug. Log:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the retrieved chunk IDs,&lt;/li&gt;
&lt;li&gt;similarity scores,&lt;/li&gt;
&lt;li&gt;the final set of context sent to the model,&lt;/li&gt;
&lt;li&gt;the model’s stated confidence (or your rubric-based confidence).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then add a rubric: “Did the answer cite the correct policy section?” Without this, you’ll never know whether you’re improving retrieval or just getting lucky.&lt;/p&gt;
&lt;p&gt;Also, don’t ignore &lt;strong&gt;data hygiene&lt;/strong&gt;. If your documents are duplicated, outdated, or poorly chunked, embeddings will faithfully preserve that mess. RAG amplifies quality problems—so you need versioning, deletion policies, and update workflows.&lt;/p&gt;
&lt;h2 id="3-agent-orchestration-stop-building-one-off-prompts-and-start-building-systems"&gt;3) Agent orchestration: stop building one-off prompts and start building systems&lt;/h2&gt;
&lt;p&gt;Most AI projects stall not because the model can’t do the work, but because the workflow is bigger than a single generation call. That’s where &lt;strong&gt;agent orchestration&lt;/strong&gt; comes in: coordinating tool use, managing state, and integrating human review when risk is non-trivial.&lt;/p&gt;
&lt;p&gt;In 2025, the winning teams won’t just “prompt the model.” They’ll ship reliable systems that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;decide when to call tools,&lt;/li&gt;
&lt;li&gt;track progress across steps,&lt;/li&gt;
&lt;li&gt;recover from failure,&lt;/li&gt;
&lt;li&gt;and escalate to a human when needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="tool-use-needs-guardrails"&gt;Tool use needs guardrails&lt;/h3&gt;
&lt;p&gt;Suppose your agent can create Jira tickets after reading a bug report. If it mistakes a label, you might spam a queue. If it invents fields, automation breaks.&lt;/p&gt;
&lt;p&gt;So design orchestration with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;explicit tool schemas&lt;/strong&gt; (inputs validated),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;preconditions&lt;/strong&gt; (“don’t create until severity is known”),&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;postconditions&lt;/strong&gt; (“confirm the created ticket ID and required links”).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Agent reads user request.&lt;/li&gt;
&lt;li&gt;Agent drafts a structured “plan” (what to do and what info is missing).&lt;/li&gt;
&lt;li&gt;Agent calls tools only for validated steps.&lt;/li&gt;
&lt;li&gt;Agent reports results with citations to tool outputs.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="state-management-is-what-makes-it-production-grade"&gt;State management is what makes it production-grade&lt;/h3&gt;
&lt;p&gt;Agents aren’t just prompts—they’re workflows. You need a state model: conversation context, retrieved documents, tool outputs, and intermediate decisions. If you don’t persist state, you’ll get loops, contradictions, and inconsistent outcomes.&lt;/p&gt;
&lt;p&gt;Implement state explicitly in your code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;messages[]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;retrieval_context[]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool_calls[]&lt;/code&gt; + results&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pending_clarifications[]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final_response&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="human-in-the-loop-isnt-a-weaknessits-how-you-scale-safely"&gt;Human-in-the-loop isn’t a weakness—it’s how you scale safely&lt;/h3&gt;
&lt;p&gt;Every serious agent should have escalation paths. Not every action requires human approval, but high-risk operations should.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For “draft a support reply,” you can let the model draft and humans optionally review.&lt;/li&gt;
&lt;li&gt;For “refund money,” you should require human confirmation after the agent proposes an action and the supporting evidence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best orchestration designs make escalation deterministic: “If confidence &amp;lt; threshold OR policy category == restricted, escalate.”&lt;/p&gt;
&lt;h2 id="4-how-these-three-skills-fit-together-in-a-real-product"&gt;4) How these three skills fit together in a real product&lt;/h2&gt;
&lt;p&gt;It’s tempting to learn these skills in isolation. Don’t. Build a mental model for how they cooperate.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prompt engineering&lt;/strong&gt; defines the behavior of each model call: format, boundaries, and decision style.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG&lt;/strong&gt; supplies grounded context: it reduces hallucination by forcing the model to operate over retrieved sources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent orchestration&lt;/strong&gt; connects those capabilities to actions and workflows: it decides what to retrieve, when to call tools, and when to ask humans.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A concrete architecture example: “AI ops copilot”&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User asks: “Why did our deployment fail last night?”&lt;/li&gt;
&lt;li&gt;Orchestrator:
&lt;ul&gt;
&lt;li&gt;retrieves relevant incident logs and deployment runbooks (RAG),&lt;/li&gt;
&lt;li&gt;asks the model to produce a root-cause hypothesis with references,&lt;/li&gt;
&lt;li&gt;optionally calls a “log search” tool for missing details,&lt;/li&gt;
&lt;li&gt;and generates a short remediation plan.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Prompt layer ensures the output includes: hypothesis, evidence, affected services, and recommended next steps.&lt;/li&gt;
&lt;li&gt;Human-in-the-loop triggers when the agent proposes changes to production configurations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the end state developers should aim for: an AI feature that behaves like software, not like a demo.&lt;/p&gt;
&lt;h2 id="5-a-practical-learning-plan-you-can-execute-this-quarter"&gt;5) A practical learning plan you can execute this quarter&lt;/h2&gt;
&lt;p&gt;You don’t need a month-long course. You need a portfolio of working features and a repeatable process.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Week 1–2: Prompt engineering with contracts&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build 3 small prompting tasks with strict JSON outputs.&lt;/li&gt;
&lt;li&gt;Add a test suite with edge cases.&lt;/li&gt;
&lt;li&gt;Integrate output validation and fallback strategies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Week 3–4: RAG that you can debug&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a mini knowledge base (policies, docs, or internal markdown).&lt;/li&gt;
&lt;li&gt;Implement embeddings + vector store + retrieval.&lt;/li&gt;
&lt;li&gt;Add reranking and log what context was used.&lt;/li&gt;
&lt;li&gt;Evaluate with a rubric: “correctness with citations.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Week 5–6: Agent orchestration for one real workflow&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick a workflow with tools (e.g., ticket creation, document drafting, or code search + change proposal).&lt;/li&gt;
&lt;li&gt;Implement state, tool schemas, and escalation.&lt;/li&gt;
&lt;li&gt;Add “plan then act” to reduce messy actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;End of month:&lt;/strong&gt; ship a small internal tool. The goal isn’t impressing anyone—it’s proving you can operate AI reliably with engineering discipline.&lt;/p&gt;
&lt;h2 id="conclusion-the-skill-gap-is-now-a-production-gap"&gt;Conclusion: the skill gap is now a production gap&lt;/h2&gt;
&lt;p&gt;The AI skill gap isn’t about knowing that LLMs exist. It’s about building systems that behave correctly under real constraints. By end of year, prompt engineering, RAG architecture, and agent orchestration become the baseline differentiators: they’re what turns model output into dependable software.&lt;/p&gt;
&lt;p&gt;Learn them together, test everything, and treat AI like production engineering—not experimentation. That’s how you stop being “AI-curious” and start being AI-competent.&lt;/p&gt;</content></item><item><title>Kamal: The Deployment Tool That Made Me Question Everything About Kubernetes</title><link>https://decastro.work/blog/kamal-deployment-tool-question-kubernetes/</link><pubDate>Wed, 07 May 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/kamal-deployment-tool-question-kubernetes/</guid><description>&lt;p&gt;For years, I treated Kubernetes like the grown-up version of deployment: inevitable, powerful, and—if you ignored the operational pain—almost magical. Then I tried Kamal. The premise is so straightforward it feels suspicious: build a deployable container, point at a few servers, and roll out with near-zero downtime… without an orchestrator. After using it, I stopped assuming that “serious companies” need Kubernetes. I started asking a more honest question: do you need orchestration, or do you need a deploy process that doesn’t hate you?&lt;/p&gt;</description><content>&lt;p&gt;For years, I treated Kubernetes like the grown-up version of deployment: inevitable, powerful, and—if you ignored the operational pain—almost magical. Then I tried Kamal. The premise is so straightforward it feels suspicious: build a deployable container, point at a few servers, and roll out with near-zero downtime… without an orchestrator. After using it, I stopped assuming that “serious companies” need Kubernetes. I started asking a more honest question: do you need orchestration, or do you need a deploy process that doesn’t hate you?&lt;/p&gt;
&lt;h2 id="the-problem-kubernetes-quietly-teaches-you-to-ignore"&gt;The problem Kubernetes quietly teaches you to ignore&lt;/h2&gt;
&lt;p&gt;Kubernetes is often sold as a platform for running workloads. In practice, most teams adopt it to solve a different problem: they want consistent, repeatable deployments across environments—dev, staging, prod, plus a cloud of edge cases in between.&lt;/p&gt;
&lt;p&gt;But Kubernetes doesn’t eliminate deployment complexity; it relocates it. You trade “write a deployment script” for “design a deployment strategy across controllers,” “choose rollout parameters,” “understand service discovery and readiness gates,” and “learn how to debug the moment your cluster is healthy but your app isn’t.”&lt;/p&gt;
&lt;p&gt;Even the people who love Kubernetes eventually end up writing glue: pipelines that generate manifests, conventions for image tags, scripts for migrations, and tribal knowledge about rollbacks. The more you rely on the cluster for everything, the more deployment becomes an exercise in understanding the platform’s behavior rather than the app’s behavior.&lt;/p&gt;
&lt;p&gt;Kamal’s appeal is that it refuses to make deployment a rite of passage.&lt;/p&gt;
&lt;h2 id="what-kamal-actually-is-and-what-it-isnt"&gt;What Kamal actually is (and what it isn’t)&lt;/h2&gt;
&lt;p&gt;Kamal is a deployment tool for containerized applications to bare servers. No orchestrator required. No Kubernetes. No Helm. No controllers. The conceptual model is closer to Docker Compose than it is to K8s:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You define what to run.&lt;/li&gt;
&lt;li&gt;You build images.&lt;/li&gt;
&lt;li&gt;You deploy them to one or more servers.&lt;/li&gt;
&lt;li&gt;You roll out changes using a strategy that aims for no downtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Kubernetes is “describe the desired state of your entire system,” Kamal is “ship this release to these machines, safely.”&lt;/p&gt;
&lt;p&gt;It also bakes in a few things teams always end up bolting on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero-downtime rolling deploys&lt;/strong&gt; (the practical kind: instances come up, traffic shifts, old ones drain)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in SSL&lt;/strong&gt; (so you’re not reinventing HTTPS automation)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A deployment workflow designed around real apps&lt;/strong&gt;, not abstract workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most telling part is the positioning. Kamal doesn’t ask you to run your whole world. It asks you to deploy your app. That means your mental model stays attached to your software instead of drifting into platform mechanics.&lt;/p&gt;
&lt;h2 id="the-its-just-a-script-philosophy-that-hits-a-nerve"&gt;The “it’s just a script” philosophy that hits a nerve&lt;/h2&gt;
&lt;p&gt;The first thing you notice about Kamal is how calm it feels. Kubernetes deployments can be thrilling—until you start worrying about resource requests, readiness probes, rollout controllers, and the difference between “pod running” and “app actually serving traffic.”&lt;/p&gt;
&lt;p&gt;Kamal is suspiciously simple. And that’s the point.&lt;/p&gt;
&lt;p&gt;Think about what you truly need for most production deployments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build a new artifact&lt;/strong&gt; (container image).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start the new version somewhere&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify it’s healthy&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift traffic&lt;/strong&gt; to it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop the old version&lt;/strong&gt; without breaking users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make rollback predictable&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s basically it. Everything else—ingress controllers, service meshes, autoscalers—may be useful, but it’s not required for safe, repeatable rollouts of a typical web app.&lt;/p&gt;
&lt;p&gt;Kamal leans into that reality. It’s what Docker Compose would become if it grew up just enough to deploy across multiple machines with disciplined rollout behavior.&lt;/p&gt;
&lt;p&gt;And yes, it can feel offensive if you’ve spent years mastering Kubernetes. You built expertise. You paid the tax. Seeing a tool bypass most of that can look like a shortcut.&lt;/p&gt;
&lt;p&gt;But shortcuts aren’t the issue. Abstractions that don’t match your problem are.&lt;/p&gt;
&lt;h2 id="a-concrete-rollout-example-the-sane-kind-of-zero-downtime"&gt;A concrete rollout example: the sane kind of “zero downtime”&lt;/h2&gt;
&lt;p&gt;Let’s say you run a Rails app in containers across three servers behind a load balancer. With Kubernetes, your rollout story might involve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;updating a Deployment spec,&lt;/li&gt;
&lt;li&gt;letting the controller create pods,&lt;/li&gt;
&lt;li&gt;ensuring readiness probes are correct,&lt;/li&gt;
&lt;li&gt;waiting for the rollout strategy to complete,&lt;/li&gt;
&lt;li&gt;and hoping your traffic routing behaves exactly as expected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Kamal, the story is narrower. The deploy is centered on application instances:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deploy to a subset of servers&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Kamal brings up the new release alongside the old one.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health checks gate progression&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;If the new container isn’t serving properly, the rollout won’t just “pretend” it’s fine.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traffic shifts cleanly&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Clients start hitting the new instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Old instances drain and stop&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;You minimize disruption without requiring the entire system to be re-platformed.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This model is not about theoretical correctness; it’s about keeping the website usable during updates. If your app is mostly stateless and your dependencies are stable, that’s often exactly what you need.&lt;/p&gt;
&lt;p&gt;And the best part? When things go wrong, you can reason about them like a deploy issue, not like a distributed systems seminar.&lt;/p&gt;
&lt;h2 id="why-dhhs-comfort-matters-and-why-it-shouldnt-be-a-surprise"&gt;Why DHH’s comfort matters (and why it shouldn’t be a surprise)&lt;/h2&gt;
&lt;p&gt;Kamal isn’t just a toy for hobbyists. The 37signals team reportedly runs &lt;strong&gt;Hey&lt;/strong&gt; and &lt;strong&gt;Basecamp&lt;/strong&gt; using Kamal without Kubernetes. That detail matters less as a status symbol and more as a validation of the underlying premise: you can run serious production workloads with a deployment workflow designed to do the job, not to showcase architecture.&lt;/p&gt;
&lt;p&gt;But here’s the part I find most useful as a lesson: &lt;strong&gt;their choice suggests they didn’t chase complexity for its own sake&lt;/strong&gt;. They likely asked the same question many teams eventually reach:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Are we actually deploying our application, or are we operating our infrastructure?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kubernetes can be a great platform, but it’s also a commitment. It’s not merely “a tool”; it’s a way of structuring work, skills, debugging, and cost. If your workload doesn’t require the benefits of orchestration, then Kubernetes becomes an elegant answer to a question you didn’t ask.&lt;/p&gt;
&lt;p&gt;Kamal answers the question you did ask: “How do we roll out changes without breaking production?”&lt;/p&gt;
&lt;h2 id="when-you-should-choose-kamaland-when-you-shouldnt"&gt;When you should choose Kamal—and when you shouldn’t&lt;/h2&gt;
&lt;p&gt;Here’s my opinionated rule of thumb:&lt;/p&gt;
&lt;h3 id="kamal-fits-best-when"&gt;Kamal fits best when…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Your app is &lt;strong&gt;web-facing&lt;/strong&gt; (or otherwise straightforward to route)&lt;/li&gt;
&lt;li&gt;Your deployment unit is essentially &lt;strong&gt;one release → one container image&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;You want &lt;strong&gt;multi-server deployments&lt;/strong&gt; without building a platform team&lt;/li&gt;
&lt;li&gt;You prefer strong deploy guarantees (rolling updates, HTTPS, predictable rollbacks) over deep cluster-level control&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Examples: Rails apps, Node services, background workers with manageable routing, and most “normal” containerized web systems.&lt;/p&gt;
&lt;h3 id="you-might-still-want-kubernetes-when"&gt;You might still want Kubernetes when…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You need &lt;strong&gt;complex scheduling&lt;/strong&gt; across heterogeneous resources&lt;/li&gt;
&lt;li&gt;You run workloads that demand &lt;strong&gt;fine-grained autoscaling&lt;/strong&gt; and orchestration semantics&lt;/li&gt;
&lt;li&gt;You’re building a platform used by multiple teams with diverse requirements&lt;/li&gt;
&lt;li&gt;You want cluster-native primitives as first-class features (for example, advanced networking models)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is “bad.” It’s just different. Kubernetes is incredible when you genuinely want a distributed runtime and you’re willing to operate it.&lt;/p&gt;
&lt;p&gt;But if your primary pain is “we can’t deploy safely,” Kamal targets that directly.&lt;/p&gt;
&lt;h2 id="the-practical-takeaway-stop-making-deploys-a-platform-problem"&gt;The practical takeaway: stop making deploys a platform problem&lt;/h2&gt;
&lt;p&gt;After using Kamal, the biggest mindset shift I had wasn’t technical—it was cultural.&lt;/p&gt;
&lt;p&gt;Stop asking, “Do we have Kubernetes?” Start asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do we have a release workflow that makes rollback boring?&lt;/li&gt;
&lt;li&gt;Can we deploy across servers without a fragile web of custom scripts?&lt;/li&gt;
&lt;li&gt;Do we have health checks and traffic shifting that match how users experience the system?&lt;/li&gt;
&lt;li&gt;Are we building abstractions we can’t explain to a new teammate?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kamal’s strength is that it keeps deployment close to the app’s reality. You get a disciplined rollout mechanism, not an ecosystem you have to learn to operate.&lt;/p&gt;
&lt;p&gt;And in many teams, that’s the difference between shipping confidently and constantly fearing the next production change.&lt;/p&gt;
&lt;h2 id="conclusion-maybe-the-orchestrator-isnt-the-point"&gt;Conclusion: Maybe the orchestrator isn’t the point&lt;/h2&gt;
&lt;p&gt;Kubernetes is powerful, but it isn’t automatically the answer to “safe deployments.” Kamal is a reminder that many production problems reduce to a well-designed release process: roll forward without downtime, roll back without drama, and keep HTTPS from becoming a permanent project.&lt;/p&gt;
&lt;p&gt;If your infrastructure goal is “run apps,” Kamal will feel almost refreshingly honest. It’s not trying to be your platform. It’s trying to get your code into production—cleanly—and let you get back to building.&lt;/p&gt;</content></item><item><title>The Quiet Death of the SPA for Content Websites</title><link>https://decastro.work/blog/quiet-death-spa-content-websites/</link><pubDate>Thu, 01 May 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/quiet-death-spa-content-websites/</guid><description>&lt;p&gt;For years, the industry treated SPAs like a universal solvent—drop one into every project and everything gets “modern.” But if you’ve built or maintained a content-heavy site, you already know the bill came due: brittle performance, fragile SEO, and accessibility compromises that took longer to fix than they did to ship.&lt;/p&gt;
&lt;p&gt;The good news is that course-correction is no longer theoretical. Multi-page apps (MPAs), delivered as HTML and progressively enhanced, are back—and for most content websites, they never should have left.&lt;/p&gt;</description><content>&lt;p&gt;For years, the industry treated SPAs like a universal solvent—drop one into every project and everything gets “modern.” But if you’ve built or maintained a content-heavy site, you already know the bill came due: brittle performance, fragile SEO, and accessibility compromises that took longer to fix than they did to ship.&lt;/p&gt;
&lt;p&gt;The good news is that course-correction is no longer theoretical. Multi-page apps (MPAs), delivered as HTML and progressively enhanced, are back—and for most content websites, they never should have left.&lt;/p&gt;
&lt;h2 id="why-the-spa-model-broke-content-sites-not-just-seo"&gt;Why the SPA model broke content sites (not just “SEO”)&lt;/h2&gt;
&lt;p&gt;An SPA optimizes for one primary goal: an app-like experience after the initial load, typically with most data and UI transitions handled client-side. That’s a great fit when the product is the UI—think Figma-style collaboration or Google Docs-like document editing—where the user continuously interacts with stateful interfaces.&lt;/p&gt;
&lt;p&gt;Content websites are different. A blog, a landing page, a documentation portal, or a storefront is primarily about information retrieval: render content quickly, let users skim, make pages discoverable, and ensure assistive technologies can follow structure.&lt;/p&gt;
&lt;p&gt;When you force an SPA architecture onto that job, you usually inherit several cascading problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;First meaningful paint suffers.&lt;/strong&gt; SPAs often ship a JavaScript bundle first, then render content later once the client hydrates and runs UI logic. Even if the final interaction is smooth, the initial “I can read this” moment can lag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Indexing becomes a moving target.&lt;/strong&gt; You can make SPAs work with server-side rendering or pre-rendering, but it turns a simple content pipeline into a complicated system with multiple render paths and edge cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility gets harder than it needs to be.&lt;/strong&gt; SPA routing and dynamic content require careful focus management, live region announcements, history handling, and robust semantic structure. You can do it—but it’s not “free,” and it’s easy to regress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complexity creeps upward.&lt;/strong&gt; A content site shouldn’t need a full application framework, client routers, state managers, and build pipelines to publish articles. Yet many teams ended up with exactly that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short: the SPA made content sites behave like applications. But content sites aren’t applications—they’re documents plus navigation.&lt;/p&gt;
&lt;h2 id="the-real-reason-mpas-are-winning-again-they-ship-html-first"&gt;The real reason MPAs are winning again: they ship HTML first&lt;/h2&gt;
&lt;p&gt;The winning pattern is straightforward: render HTML on the server, send it over the wire, and enhance it progressively for interactions that actually need JavaScript. This approach is less about nostalgia and more about engineering leverage.&lt;/p&gt;
&lt;p&gt;When you deliver meaningful HTML immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Users see real content quickly, even on slow devices or spotty networks.&lt;/li&gt;
&lt;li&gt;Search engines receive structured, crawlable pages without guessing what the client might render later.&lt;/li&gt;
&lt;li&gt;Assistive technologies get semantic markup by default—headings, links, tables, lists—rather than reconstructed DOM trees.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Concrete example: a documentation page.&lt;/p&gt;
&lt;p&gt;With an SPA, you often pay for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A client-side router&lt;/li&gt;
&lt;li&gt;A hydration step&lt;/li&gt;
&lt;li&gt;A “loading” UI that replaces the server markup&lt;/li&gt;
&lt;li&gt;Extra client logic to build headings, anchor links, and code blocks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With an MPA + progressive enhancement, you simply render the documentation page as HTML. If you want “copy code” buttons, collapsible sections, or client-side search, you add that on top—without turning the entire page into a JavaScript application.&lt;/p&gt;
&lt;p&gt;The key is not “no JavaScript.” The key is “JavaScript when it helps,” not “JavaScript as the delivery mechanism for core content.”&lt;/p&gt;
&lt;h2 id="astro-server-rendered-frameworks-and-htmx-the-modern-toolkit-for-html-first"&gt;Astro, server-rendered frameworks, and htmx: the modern toolkit for HTML-first&lt;/h2&gt;
&lt;p&gt;The industry didn’t just “go back.” It improved the mechanics of shipping HTML-first while keeping developer experience sane.&lt;/p&gt;
&lt;h3 id="astro-and-component-driven-static-generation"&gt;Astro and component-driven static generation&lt;/h3&gt;
&lt;p&gt;Astro’s core idea—ship less JavaScript by default and keep components composable—fits content sites like a glove. You can build a site with pages that are generated as HTML, then selectively hydrate interactive components only where necessary. The result is a clean separation between “content delivery” and “interactive behavior.”&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer Islands of interactivity: buttons, modals, forms, widgets—hydrate only those.&lt;/li&gt;
&lt;li&gt;Keep your routing and page structure server-rendered so deep links and sharing behave predictably.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="server-rendered-frameworks-reactvuesvelte-on-the-server"&gt;Server-rendered frameworks (React/Vue/Svelte on the server)&lt;/h3&gt;
&lt;p&gt;Server-rendered frameworks have matured: streaming, routing, data loading conventions, and tooling that make “render HTML on the server” a first-class default. If you need a component model, you can still have one—just don’t force the browser to become the primary renderer.&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treat hydration as an enhancement, not a requirement for correctness.&lt;/li&gt;
&lt;li&gt;Avoid client-side-only “blank pages.” If a page can’t render meaningfully without JavaScript, you’re back to SPA territory.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="htmx-for-incremental-interactivity"&gt;htmx for incremental interactivity&lt;/h3&gt;
&lt;p&gt;If you want a particularly pragmatic approach, htmx is hard to ignore. It lets you sprinkle interactivity into server-rendered pages using declarative attributes—fetch partial HTML, swap DOM fragments, and keep the browser in charge of only the interaction.&lt;/p&gt;
&lt;p&gt;A classic example: a product list with filtering.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The server renders the initial catalog as HTML.&lt;/li&gt;
&lt;li&gt;When users filter, htmx requests the filtered HTML from the server and swaps the relevant section.&lt;/li&gt;
&lt;li&gt;No SPA routing. No client-side state gymnastics. No hydration dependency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not “less modern.” It’s just closer to how the web already works.&lt;/p&gt;
&lt;h2 id="progressive-enhancement-isnt-a-vibeits-a-discipline"&gt;Progressive enhancement isn’t a vibe—it’s a discipline&lt;/h2&gt;
&lt;p&gt;Progressive enhancement is often described as a principle, but in practice it’s a set of decisions. If you apply it consistently, it becomes a competitive advantage.&lt;/p&gt;
&lt;p&gt;Here’s what it looks like in a content website build:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Core content is server-rendered HTML.&lt;/strong&gt;&lt;br&gt;
Your page must make sense with JavaScript turned off.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Navigation works without a SPA router.&lt;/strong&gt;&lt;br&gt;
Standard links should load new pages. If you want transitions, layer them on later.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Forms work reliably.&lt;/strong&gt;&lt;br&gt;
Use server-side validation and render form errors in HTML. Enhance with client-side validation only to improve usability, not to provide the only validation path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Interactive components are isolated.&lt;/strong&gt;&lt;br&gt;
If a widget needs JavaScript, make it a self-contained enhancement. Don’t let one widget decide the rendering architecture for the entire page.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Accessibility is part of the contract.&lt;/strong&gt;&lt;br&gt;
Don’t treat focus states and announcements as afterthoughts. With HTML-first rendering, you get much of the semantic foundation for free.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Opinionated takeaway: if your “content site” behaves like an SPA core, you’re likely violating at least two of these disciplines—usually #1 and #2.&lt;/p&gt;
&lt;h2 id="performance-isnt-just-fasterits-more-predictable"&gt;Performance isn’t just faster—it’s more predictable&lt;/h2&gt;
&lt;p&gt;SPA defenders often argue that once the initial bundle loads, navigation becomes fast. That can be true in ideal conditions. But content websites live in real conditions: slow networks, low-power phones, distracted users, and frequent back/forward navigation.&lt;/p&gt;
&lt;p&gt;MPA + HTML-first designs have a different performance profile:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The browser has something meaningful to display immediately.&lt;/li&gt;
&lt;li&gt;Cache behavior is simpler.&lt;/li&gt;
&lt;li&gt;Page loads are stateless and consistent across sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical guidance when reviewing an existing SPA content site:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Measure &lt;strong&gt;time to first contentful paint&lt;/strong&gt; and &lt;strong&gt;time to interactive&lt;/strong&gt;, not just route transitions.&lt;/li&gt;
&lt;li&gt;Check whether content is available in the initial HTML response or only after hydration.&lt;/li&gt;
&lt;li&gt;Validate that your pages have meaningful metadata and structured headings before client code runs.&lt;/li&gt;
&lt;li&gt;Test with JavaScript disabled. If the page collapses into an error state, you’ve built a fragile system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A sharp rule of thumb: if the page is primarily consumed as a document, make the server deliver the document.&lt;/p&gt;
&lt;h2 id="the-cost-of-the-spa-eraand-why-the-default-is-changing"&gt;The cost of the SPA era—and why the “default” is changing&lt;/h2&gt;
&lt;p&gt;The SPA wasn’t a mistake because SPAs are “bad.” They’re bad as a default architecture for everything.&lt;/p&gt;
&lt;p&gt;The industry conflated “single-page feel” with “better user experience,” then standardized tooling around client-first rendering. Over time, teams accumulated complexity to fix the shortcomings: SSR, pre-rendering, client routing workarounds, SEO adapters, and accessibility patches. You could keep going, but you were treating symptoms instead of questioning the underlying assumption.&lt;/p&gt;
&lt;p&gt;Now the assumptions are being challenged:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Content should be delivered as HTML first.&lt;/li&gt;
&lt;li&gt;Interactivity should be added where it provides value.&lt;/li&gt;
&lt;li&gt;Framework choice should follow product needs, not fashion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And crucially, we have better options than we did a decade ago. Astro, server-rendered frameworks, and htmx prove that you can build modern experiences without forcing every page into an SPA mold.&lt;/p&gt;
&lt;h2 id="conclusion-build-like-the-web-still-matters"&gt;Conclusion: Build like the web still matters&lt;/h2&gt;
&lt;p&gt;The SPA for content websites didn’t fail because developers lacked talent—it failed because the architecture was misaligned with what content websites actually are. The web is at its best when it ships documents efficiently, lets users navigate naturally, and makes accessibility and SEO the default outcome rather than the result of heroic engineering.&lt;/p&gt;
&lt;p&gt;Multi-page apps with progressive enhancement aren’t a trend. They’re the correction: HTML-first delivery, interactive enhancements on demand, and a cleaner, more resilient foundation for every blog, doc site, marketing page, and storefront that wants to be fast and readable everywhere.&lt;/p&gt;</content></item><item><title>Redis Jumped 8% Because Microservices Won (Whether You Like It or Not)</title><link>https://decastro.work/blog/redis-jumped-8-percent-microservices-won/</link><pubDate>Fri, 25 Apr 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/redis-jumped-8-percent-microservices-won/</guid><description>&lt;p&gt;Microservices didn’t just change how teams ship software—they changed what software needs to survive the chaos: caches that stay coherent, sessions that don’t vanish, queues that don’t turn into spreadsheets, and coordination that doesn’t become a deadlock festival. Redis is the glue that keeps all of that from collapsing. If you’ve noticed Redis usage trending upward, it’s not because teams suddenly got nostalgic for an in-memory datastore—it’s because microservices made Redis unavoidable.&lt;/p&gt;</description><content>&lt;p&gt;Microservices didn’t just change how teams ship software—they changed what software needs to survive the chaos: caches that stay coherent, sessions that don’t vanish, queues that don’t turn into spreadsheets, and coordination that doesn’t become a deadlock festival. Redis is the glue that keeps all of that from collapsing. If you’ve noticed Redis usage trending upward, it’s not because teams suddenly got nostalgic for an in-memory datastore—it’s because microservices made Redis unavoidable.&lt;/p&gt;
&lt;h2 id="the-microservices-default-why-one-tool-per-concern-stops-working"&gt;The microservices default: why “one tool per concern” stops working&lt;/h2&gt;
&lt;p&gt;At small scale, it’s tempting to treat each microservice concern as a separate tool: a cache here, a queue there, rate limiting somewhere else, sessions “handled by the platform,” and pub/sub whenever you feel brave. That strategy dies as soon as you run multiple services in production long enough to learn the cost of integration.&lt;/p&gt;
&lt;p&gt;Here’s what microservices force you to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep latency tolerable&lt;/strong&gt; across network hops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Share state&lt;/strong&gt; without introducing a distributed-system meltdown.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coordinate&lt;/strong&gt; actions (locks, leader election, idempotency).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handle traffic spikes&lt;/strong&gt; without turning your database into a punchline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recover gracefully&lt;/strong&gt; when services restart, deploy, and fail.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis earns its place because it’s a single, well-understood infrastructure primitive that can support multiple patterns. Not all at once—nothing good is that simple—but enough that teams can standardize quickly and move faster.&lt;/p&gt;
&lt;p&gt;Practical example: imagine an e-commerce checkout system split into services like &lt;code&gt;catalog&lt;/code&gt;, &lt;code&gt;pricing&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, and &lt;code&gt;orders&lt;/code&gt;. Each service might need caching, each needs rate limiting, sessions need somewhere durable to live, and events need to flow between services. When Redis is available as a shared component, the team stops reinventing the wheel per microservice and starts enforcing consistent operational practices.&lt;/p&gt;
&lt;h2 id="caching-that-doesnt-turn-into-a-reliability-problem"&gt;Caching that doesn’t turn into a reliability problem&lt;/h2&gt;
&lt;p&gt;Caching is the classic reason Redis gets adopted, but the real value is &lt;em&gt;how&lt;/em&gt; caching becomes a system, not a shortcut.&lt;/p&gt;
&lt;p&gt;A few patterns teams use successfully:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-through caching&lt;/strong&gt;: service checks Redis first; if missing, fetches from the database and writes back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-through or write-back&lt;/strong&gt;: updates Redis in step with writes, or queues cache invalidation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache-aside with TTL discipline&lt;/strong&gt;: every cached value has a sensible expiration and a plan for misses.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difference between “Redis as a feature” and “Redis as infrastructure” is TTL strategy and invalidation behavior. You don’t need exotic techniques on day one—you need consistency.&lt;/p&gt;
&lt;p&gt;Concrete advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;short TTLs&lt;/strong&gt; for highly dynamic data (e.g., “current availability”).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;versioned keys&lt;/strong&gt; for items where invalidation is awkward (e.g., &lt;code&gt;product:{id}:v{version}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;bounded memory patterns&lt;/strong&gt;: set eviction policies intentionally instead of assuming “in-memory means infinite.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In microservices, this matters because cache misses amplify downstream load. A single poorly scoped cache can turn a deployment into a thundering herd.&lt;/p&gt;
&lt;h2 id="session-storage-the-boring-piece-that-quietly-matters"&gt;Session storage: the boring piece that quietly matters&lt;/h2&gt;
&lt;p&gt;Most teams don’t realize they’re fighting distributed systems until sessions become a problem. Load balancers send requests to different instances; instances restart; autoscaling changes the topology; and suddenly your “stateless” application isn’t as stateless as you thought.&lt;/p&gt;
&lt;p&gt;Redis session storage solves the operational mismatch: web and API instances can stay ephemeral while session state lives in a shared store.&lt;/p&gt;
&lt;p&gt;A typical implementation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store session data keyed by session ID.&lt;/li&gt;
&lt;li&gt;Set TTL aligned with session expiration.&lt;/li&gt;
&lt;li&gt;Use atomic updates when multiple requests mutate the same session (e.g., cart updates and authentication refresh).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical example: a mobile client might call &lt;code&gt;/api/cart&lt;/code&gt; and &lt;code&gt;/api/checkout&lt;/code&gt; concurrently. Without careful handling, you can race session updates and end up with “ghost carts” or stale authentication state. Redis gives you the primitives to make those updates consistent without forcing the web tier to become sticky in a fragile way.&lt;/p&gt;
&lt;h2 id="pubsub-and-eventing-decoupling-services-without-losing-control"&gt;Pub/sub and eventing: decoupling services without losing control&lt;/h2&gt;
&lt;p&gt;Once microservices multiply, you hit the “who informs whom?” question. Polling works until it doesn’t—stale data, wasted calls, delayed reactions. Pub/sub and streaming patterns help decouple services so that publishers don’t need to know who consumes.&lt;/p&gt;
&lt;p&gt;Redis-based patterns commonly look like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broadcasting domain events (e.g., &lt;code&gt;order.created&lt;/code&gt;, &lt;code&gt;user.verified&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Building lightweight notification channels (e.g., “cache invalidation” messages).&lt;/li&gt;
&lt;li&gt;Driving fan-out workflows without tight service-to-service coupling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the editorial truth: pub/sub is easy to wire and easy to misuse. It’s not a magic message bus. Teams succeed when they treat it as part of a broader design that includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotent consumers&lt;/strong&gt; (events can be delivered more than once).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry and dead-letter logic&lt;/strong&gt; where appropriate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear ownership of state&lt;/strong&gt; (events announce changes; state lives elsewhere).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis helps because it keeps the plumbing simple while still being fast enough for high-throughput internal messaging.&lt;/p&gt;
&lt;h2 id="distributed-locks-and-coordination-use-them-carefully-but-dont-pretend-you-dont-need-them"&gt;Distributed locks and coordination: use them carefully, but don’t pretend you don’t need them&lt;/h2&gt;
&lt;p&gt;Microservices are distributed by definition, which means coordination is unavoidable. You need mutual exclusion sometimes, leader election sometimes, and “only one worker does the expensive thing” behavior often.&lt;/p&gt;
&lt;p&gt;Redis makes these coordination patterns achievable without turning your stack into a research project. Distributed locks, for example, are a tool—powerful, but sharp.&lt;/p&gt;
&lt;p&gt;Practical guidance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Redis locks for &lt;strong&gt;short critical sections&lt;/strong&gt; and time-bounded operations.&lt;/li&gt;
&lt;li&gt;Prefer &lt;strong&gt;single-purpose locks&lt;/strong&gt; (one lock per resource or keyspace) instead of a global “everything lock.”&lt;/li&gt;
&lt;li&gt;Make operations &lt;strong&gt;idempotent&lt;/strong&gt; so failures don’t cause double work.&lt;/li&gt;
&lt;li&gt;Set lock expirations to avoid permanent contention after crashes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you think you “don’t need locks,” check the real world: retries, concurrent requests, background jobs, and deployment rollouts are exactly where coordination bugs breed.&lt;/p&gt;
&lt;h2 id="rate-limiting-and-backpressure-protecting-systems-from-themselves"&gt;Rate limiting and backpressure: protecting systems from themselves&lt;/h2&gt;
&lt;p&gt;Rate limiting is where Redis often becomes non-negotiable. Microservices don’t just receive traffic—they receive &lt;em&gt;traffic multiplied by internal calls&lt;/em&gt;. One slow downstream dependency can trigger a cascade of retries and timeouts. Rate limiting buys time.&lt;/p&gt;
&lt;p&gt;Teams use Redis counters and token-bucket-like strategies to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Throttle by user, API key, or IP.&lt;/li&gt;
&lt;li&gt;Apply burst control for expensive endpoints.&lt;/li&gt;
&lt;li&gt;Provide consistent behavior across multiple service instances.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical example: if you expose an endpoint that triggers a heavyweight report generation, you can throttle requests per user. Even if one instance fails mid-flight, Redis ensures the overall system’s decision remains consistent across the fleet.&lt;/p&gt;
&lt;p&gt;The key is operational realism:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose a rate limiting window that matches your user expectations.&lt;/li&gt;
&lt;li&gt;Store only what you need (and consider key cardinality carefully).&lt;/li&gt;
&lt;li&gt;Monitor denial rates; if legitimate users get throttled, you’ll need a feedback loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-valkey-moment-licensing-drama-but-protocol-grade-infrastructure-still-wins"&gt;The Valkey moment: licensing drama, but protocol-grade infrastructure still wins&lt;/h2&gt;
&lt;p&gt;There’s real momentum around Redis alternatives, including Valkey, driven in part by licensing and governance debates. That shift is not imaginary—teams re-evaluate dependencies whenever legal uncertainty appears.&lt;/p&gt;
&lt;p&gt;But two things stay true in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Redis patterns are portable knowledge.&lt;/strong&gt; Teams built their caching, session, pub/sub, and locking layers around the Redis programming model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Redis protocol is infrastructure-grade.&lt;/strong&gt; Even when a backend changes, application-level fluency and operational muscle memory don’t vanish.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What this means for builders: you can reduce lock-in pressure by choosing implementations thoughtfully, but you shouldn’t treat Redis fluency as optional. If you understand the patterns, the migration path gets easier, not harder.&lt;/p&gt;
&lt;p&gt;If your team is hiring or training, make Redis competence a baseline skill: not just “how to run Redis,” but how to design with TTLs, failure modes, atomicity, and workload characteristics.&lt;/p&gt;
&lt;h2 id="conclusion-redis-didnt-get-popularmicroservices-made-it-necessary"&gt;Conclusion: Redis didn’t “get popular”—microservices made it necessary&lt;/h2&gt;
&lt;p&gt;Redis usage rises for a reason: microservices created a steady demand for fast shared state, coordination, and communication. Caching keeps latency sane, session storage keeps web tiers clean, pub/sub enables decoupling, distributed locks prevent duplicate work, and rate limiting protects the system from cascading failure.&lt;/p&gt;
&lt;p&gt;Whether you’re strictly on Redis today or exploring protocol-compatible alternatives, the takeaway is the same: if you build distributed systems, Redis fluency isn’t a nice-to-have. It’s the difference between stitching services together and actually running them.&lt;/p&gt;</content></item><item><title>Your Kubernetes Cluster Is Probably Overkill (Still)</title><link>https://decastro.work/blog/kubernetes-cluster-probably-overkill-still/</link><pubDate>Sun, 13 Apr 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/kubernetes-cluster-probably-overkill-still/</guid><description>&lt;p&gt;Kubernetes is a marvel—so is a CNC machine. The trouble isn’t that it’s powerful. The trouble is that most teams keep buying power tools when they actually need a bench vise. Three years after I made this argument, the replies are still furious. That tells me the real issue isn’t Kubernetes. It’s what people &lt;em&gt;do&lt;/em&gt; with it.&lt;/p&gt;
&lt;p&gt;Kubernetes is great for running complex, multi-team systems at scale. But for the majority of teams running it today—especially those under ~30 engineers—it’s usually not the best default. You’re paying an operational complexity tax for capabilities you may never use.&lt;/p&gt;</description><content>&lt;p&gt;Kubernetes is a marvel—so is a CNC machine. The trouble isn’t that it’s powerful. The trouble is that most teams keep buying power tools when they actually need a bench vise. Three years after I made this argument, the replies are still furious. That tells me the real issue isn’t Kubernetes. It’s what people &lt;em&gt;do&lt;/em&gt; with it.&lt;/p&gt;
&lt;p&gt;Kubernetes is great for running complex, multi-team systems at scale. But for the majority of teams running it today—especially those under ~30 engineers—it’s usually not the best default. You’re paying an operational complexity tax for capabilities you may never use.&lt;/p&gt;
&lt;p&gt;This is not a hit piece. It’s a reality check, with practical alternatives you can adopt without lighting your roadmap on fire.&lt;/p&gt;
&lt;h2 id="why-kubernetes-becomes-invisible-infrastructureand-why-thats-costly"&gt;Why Kubernetes becomes “invisible infrastructure”—and why that’s costly&lt;/h2&gt;
&lt;p&gt;The biggest lie about Kubernetes is that it’s just “infrastructure as code,” like Terraform. In practice, Kubernetes introduces a new category of work that doesn’t feel like feature development but still consumes engineering energy: continual operational alignment.&lt;/p&gt;
&lt;p&gt;You rarely “set up Kubernetes once.” Instead, you maintain an ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Identity and access&lt;/strong&gt; (RBAC roles, service accounts, least privilege)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Networking&lt;/strong&gt; (ingress controllers, service types, network policies, DNS quirks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt; (storage classes, volume lifecycle, reclaim policies)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security plumbing&lt;/strong&gt; (certificates, secrets management patterns, image scanning integration)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment mechanics&lt;/strong&gt; (rollouts, readiness/liveness, autoscaling rules, dashboards/observability hooks)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if your team uses sensible defaults, the work doesn’t disappear. It just moves into “tribal knowledge” and recurring operational tickets.&lt;/p&gt;
&lt;p&gt;A common pattern looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You ship features in sprint cadence, but you also spend cycles every week debugging environment drift—why one pod behaves differently under the new ingress, why a migration script fails only in staging, why TLS renewals stalled, why a network policy blocked something you forgot to label.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That time doesn’t always show up as “Kubernetes time” on a timesheet. But it shows up in your delivery speed.&lt;/p&gt;
&lt;p&gt;And if you’re not explicitly using Kubernetes primitives—multi-tenancy boundaries, advanced scheduling, complex stateful workloads, multi-region failover—then a lot of that overhead is mostly theater.&lt;/p&gt;
&lt;h2 id="the-capability-mismatch-problem-youre-building-the-wrong-platform"&gt;The “capability mismatch” problem: you’re building the wrong platform&lt;/h2&gt;
&lt;p&gt;Kubernetes rewards teams that need &lt;em&gt;platform behaviors&lt;/em&gt;. The platform behaviors people often cite—self-healing, rolling updates, scaling—are real. But they’re not exclusive to K8s. What matters is whether your application portfolio demands the orchestration features K8s makes easy.&lt;/p&gt;
&lt;p&gt;Consider a typical team of ~10–25 engineers building a handful of services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A web app&lt;/li&gt;
&lt;li&gt;One or two background workers&lt;/li&gt;
&lt;li&gt;A few domain services (maybe a queue consumer, maybe a scheduled job)&lt;/li&gt;
&lt;li&gt;A database and cache&lt;/li&gt;
&lt;li&gt;Some integrations and an admin UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a perfect use case for simpler deployment models. You can still get reliability: retries, health checks, rollouts, environment separation. You just don’t need to bring your own cluster-building discipline.&lt;/p&gt;
&lt;p&gt;Here’s the litmus test I use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you run &lt;strong&gt;multiple deployable services per team&lt;/strong&gt;, with strict ownership boundaries and heavy automation demands?&lt;/li&gt;
&lt;li&gt;Do you need &lt;strong&gt;fine-grained traffic routing&lt;/strong&gt; across many versions (beyond basic blue/green)?&lt;/li&gt;
&lt;li&gt;Do you manage &lt;strong&gt;stateful workloads&lt;/strong&gt; with custom storage behavior frequently?&lt;/li&gt;
&lt;li&gt;Do you require &lt;strong&gt;complex autoscaling&lt;/strong&gt; based on app-level metrics?&lt;/li&gt;
&lt;li&gt;Do you operate across &lt;strong&gt;multiple regions&lt;/strong&gt; or have a real multi-cluster story?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re answering “mostly no,” then Kubernetes becomes a capability mismatch. You’re paying to orchestrate complexity you don’t need, which slows down everything else.&lt;/p&gt;
&lt;h2 id="what-those-engineers-are-really-doing-and-what-to-stop-doing"&gt;What those engineers are really doing (and what to stop doing)&lt;/h2&gt;
&lt;p&gt;Let’s talk about the day-to-day work that tends to consume Kubernetes teams. This isn’t a list of “bad practices.” It’s a list of the recurring categories of tasks that Kubernetes introduces.&lt;/p&gt;
&lt;h3 id="1-rbac-and-permissions-drift"&gt;1) RBAC and permissions drift&lt;/h3&gt;
&lt;p&gt;When you add a new job, service, or endpoint, you often have to update RBAC bindings. Even with automation, the process adds friction and review surface area. It’s not hard—it’s just constant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical move:&lt;/strong&gt; if you must use Kubernetes, invest early in a clean permission model with templates. But if you don’t need Kubernetes, don’t recreate this treadmill elsewhere.&lt;/p&gt;
&lt;h3 id="2-ingress-tls-and-why-is-staging-different"&gt;2) Ingress, TLS, and “why is staging different?”&lt;/h3&gt;
&lt;p&gt;Ingress controllers and certificate management sound solved—until you have five environments, two domains, and one legacy wildcard certificate strategy. Then you get a steady drip of “it works locally / fails in staging” issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical move:&lt;/strong&gt; fewer moving parts wins. Managed PaaS and simpler container platforms typically reduce this entire class of work.&lt;/p&gt;
&lt;h3 id="3-storage-class-complexity-and-volume-lifecycle-surprises"&gt;3) Storage class complexity and volume lifecycle surprises&lt;/h3&gt;
&lt;p&gt;Stateful workloads require careful thinking: provisioning behavior, reclaim policies, migration impact, backups, and incident recovery. If your services are mostly stateless (common for modern web stacks), you can keep your state on managed services and reduce Kubernetes storage complexity dramatically. But when teams don’t do this, they end up spending time in storage configuration instead of product.&lt;/p&gt;
&lt;h3 id="4-observability-wiring-that-never-fully-stabilizes"&gt;4) Observability wiring that never fully stabilizes&lt;/h3&gt;
&lt;p&gt;Kubernetes monitoring stacks are powerful, but the integration path can be endless: metrics, logs, traces, pod identity, dashboards, alert tuning, SLO definitions. You’ll still need observability with any platform—but Kubernetes makes it more configurable, and configuration invites drift.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical move:&lt;/strong&gt; choose an ecosystem with sensible defaults and fewer integration seams unless you truly need the control.&lt;/p&gt;
&lt;h2 id="faster-paths-docker-compose-kamal-and-managed-paas"&gt;Faster paths: Docker Compose, Kamal, and managed PaaS&lt;/h2&gt;
&lt;p&gt;Here are three alternatives I genuinely think most teams should consider first. The point isn’t ideology. The point is shipping faster with fewer operational surprises.&lt;/p&gt;
&lt;h3 id="docker-compose-the-serious-local--simple-deploy-baseline"&gt;Docker Compose: the “serious local + simple deploy” baseline&lt;/h3&gt;
&lt;p&gt;Docker Compose is underrated because it’s not glamorous. But if your architecture is straightforward, Compose gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repeatable local development&lt;/li&gt;
&lt;li&gt;Clear service boundaries&lt;/li&gt;
&lt;li&gt;A clean way to mirror staging&lt;/li&gt;
&lt;li&gt;Straightforward CI builds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can run Compose in production with the right tooling around it, but even if you don’t, Compose still pays off by removing environment differences. Less drift means fewer “Kubernetes-only” bugs and fewer emergency rollbacks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; If your team currently builds Docker images and then struggles with dev/staging parity, start by making Compose the canonical way to run the whole stack locally. When you later deploy via another method, keep the service definitions aligned.&lt;/p&gt;
&lt;h3 id="kamal-or-similar-deploy-via-ssh--containers-approaches-pragmatic-deployment"&gt;Kamal (or similar “deploy via SSH + containers” approaches): pragmatic deployment&lt;/h3&gt;
&lt;p&gt;Tools in the “run containers on servers” family let teams focus on what matters: build artifacts, run them reliably, and roll forward/back without cluster semantics.&lt;/p&gt;
&lt;p&gt;Kamal-style workflows shine for teams that don’t want to learn the full operational Kubernetes surface area:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No RBAC learning curve&lt;/li&gt;
&lt;li&gt;Less networking plumbing&lt;/li&gt;
&lt;li&gt;Fewer controllers to manage&lt;/li&gt;
&lt;li&gt;Predictable runtime behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your deployment targets are a small set of VMs (or a limited fleet), this approach can be shockingly productive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; A two-service Rails + worker setup can deploy via a single command, with database migrations handled in a controlled step. When something fails, you can inspect the running container set directly rather than spelunking through pod states, events, and controller reconciliation.&lt;/p&gt;
&lt;h3 id="managed-paas-let-someone-else-babysit-the-platform"&gt;Managed PaaS: let someone else babysit the platform&lt;/h3&gt;
&lt;p&gt;If you’re not actively developing a platform, using a managed PaaS can be the fastest path to “production without the platform tax.”&lt;/p&gt;
&lt;p&gt;Managed offerings typically handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ingress routing&lt;/li&gt;
&lt;li&gt;TLS automation&lt;/li&gt;
&lt;li&gt;Build pipelines and rollouts&lt;/li&gt;
&lt;li&gt;Environment configuration&lt;/li&gt;
&lt;li&gt;Some level of scaling (often good enough)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t about avoiding performance constraints forever. It’s about delaying complexity until you truly need it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; If your team’s bottleneck is feature throughput, the move is to adopt a PaaS that supports your deployment model and database integration well—then use the saved engineering time to improve reliability at the app layer (idempotent jobs, safe migrations, better caching strategies, and robust retry policies).&lt;/p&gt;
&lt;h2 id="when-kubernetes-actually-earns-its-keep"&gt;When Kubernetes actually earns its keep&lt;/h2&gt;
&lt;p&gt;To be fair, Kubernetes earns its keep in specific situations. You shouldn’t feel guilty for using it when you need what it provides.&lt;/p&gt;
&lt;p&gt;Kubernetes is a strong choice when you have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A large portfolio of services&lt;/strong&gt; with frequent deployments across many teams&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strong multi-tenancy or isolation requirements&lt;/strong&gt; (not just “we should be careful,” but enforceable boundaries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced scheduling and placement needs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex stateful workloads&lt;/strong&gt; that genuinely benefit from Kubernetes-native patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-cluster or multi-region architectures&lt;/strong&gt; with orchestrated failover strategies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A platform team&lt;/strong&gt; that can maintain the ecosystem as a product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is organizational. If you have the headcount and mandate to operate Kubernetes well, the complexity tax is an investment, not a leak.&lt;/p&gt;
&lt;p&gt;But if you’re a small team building product—Kubernetes can become your unwilling second job.&lt;/p&gt;
&lt;h2 id="conclusion-choose-the-simplest-system-that-meets-your-real-requirements"&gt;Conclusion: choose the simplest system that meets your real requirements&lt;/h2&gt;
&lt;p&gt;Kubernetes isn’t wrong. It’s just often misapplied.&lt;/p&gt;
&lt;p&gt;If your team is under 30 engineers and you don’t have multi-region needs, complex autoscaling, canary-heavy rollout strategies, or a platform team dedicated to cluster operations, you’re probably paying a complexity tax you could eliminate. Start with Docker Compose for parity, consider Kamal-style pragmatic deployments for speed, or go managed PaaS to reclaim engineering time.&lt;/p&gt;
&lt;p&gt;Ship faster. Learn fewer moving parts. Spend your energy on the product—not on keeping controllers and certificates reconciled.&lt;/p&gt;</content></item><item><title>Anthropic Claude Code Is the AI Coding Tool I Wanted Since Copilot Launched</title><link>https://decastro.work/blog/claude-code-ai-coding-tool-wanted/</link><pubDate>Mon, 07 Apr 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/claude-code-ai-coding-tool-wanted/</guid><description>&lt;p&gt;Copilot made coding feel faster—but it still mostly treated you like a keyboard. Anthropic’s Claude Code flips the model: instead of spitting out text for you to paste, it operates like a teammate in your repo. It reads your files, writes code, runs commands, checks results, and iterates until the work is done—right inside the terminal.&lt;/p&gt;
&lt;p&gt;If you’ve ever thought, &lt;em&gt;“I don’t need better autocomplete; I need an agent that can actually take a task from start to finish,”&lt;/em&gt; Claude Code is the first tool I’ve used that feels close.&lt;/p&gt;</description><content>&lt;p&gt;Copilot made coding feel faster—but it still mostly treated you like a keyboard. Anthropic’s Claude Code flips the model: instead of spitting out text for you to paste, it operates like a teammate in your repo. It reads your files, writes code, runs commands, checks results, and iterates until the work is done—right inside the terminal.&lt;/p&gt;
&lt;p&gt;If you’ve ever thought, &lt;em&gt;“I don’t need better autocomplete; I need an agent that can actually take a task from start to finish,”&lt;/em&gt; Claude Code is the first tool I’ve used that feels close.&lt;/p&gt;
&lt;h2 id="agentic-coding-that-behaves-like-pair-programming"&gt;Agentic coding that behaves like pair programming&lt;/h2&gt;
&lt;p&gt;Most AI coding tools today are great at one thing: predicting the next token. Even when they wrap that prediction in chat, the workflow remains “generate something and hope it compiles.” You still do the expensive part—understanding the context, wiring the change into the right place, running tests, debugging failures, and iterating.&lt;/p&gt;
&lt;p&gt;Claude Code changes that rhythm. The core promise isn’t that it can write code; it’s that it can &lt;strong&gt;work on a problem&lt;/strong&gt; with the repository as its working memory.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You give it a task (often via a GitHub issue).&lt;/li&gt;
&lt;li&gt;It reads the relevant files and the surrounding code patterns.&lt;/li&gt;
&lt;li&gt;It proposes and applies changes.&lt;/li&gt;
&lt;li&gt;It runs the commands you’d normally run (tests, linters, build steps).&lt;/li&gt;
&lt;li&gt;It checks what happened, fixes what broke, and repeats.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last part matters. Pair programming isn’t “here’s a patch—good luck.” It’s “let’s make it pass,” with feedback loops built into the workflow. Claude Code behaves that way.&lt;/p&gt;
&lt;h2 id="from-issue-to-pr-the-workflow-shift-you-can-feel"&gt;From issue to PR: the workflow shift you can feel&lt;/h2&gt;
&lt;p&gt;The biggest difference is how Claude Code frames collaboration. With Copilot-style tools, you’re often the operator: you request code, then you integrate it, run it, and debug the results. With Claude Code, you’re more like a reviewer overseeing an execution loop.&lt;/p&gt;
&lt;p&gt;Give it a GitHub issue and it can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read the codebase&lt;/strong&gt; to understand where the fix belongs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the changes&lt;/strong&gt; in the right files and style conventions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run tests&lt;/strong&gt; and other checks that reveal whether the change is correct.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterate&lt;/strong&gt; when something fails—without you manually stitching the next attempt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open a PR&lt;/strong&gt; so you can review with the full diff and rationale context.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The practical outcome: you spend less time shepherding small generated snippets into a coherent change. Instead, you spend time on what humans are good at—reviewing approach, catching edge cases, and approving the direction.&lt;/p&gt;
&lt;h3 id="a-concrete-example-make-tests-pass-isnt-enoughuntil-it-is"&gt;A concrete example: “Make tests pass” isn’t enough—until it is&lt;/h3&gt;
&lt;p&gt;Imagine an issue like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Fix flakiness in &lt;code&gt;orders&lt;/code&gt; integration tests. Failures occur intermittently in CI.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With a token-predicting tool, you’d likely get a suggestion—maybe “add a retry” or “sleep before asserting”—and then you’d test it and probably spend an hour tuning until it behaves. You’d iterate manually, because the tool can’t reliably carry the loop across your project.&lt;/p&gt;
&lt;p&gt;With Claude Code, you can hand it the same issue and let it do the loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It finds the flaky tests and their dependencies.&lt;/li&gt;
&lt;li&gt;It traces the timing assumptions and where they break.&lt;/li&gt;
&lt;li&gt;It changes the code (or test strategy) in targeted places.&lt;/li&gt;
&lt;li&gt;It runs the test suite, observes the failure mode, and tries again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when it doesn’t “know” the best fix instantly, the agentic behavior makes it &lt;em&gt;safe&lt;/em&gt; to explore. You’re not stuck between guess-and-paste attempts—you’re watching an automated effort converge.&lt;/p&gt;
&lt;h2 id="terminal-native-means-fewer-context-switches"&gt;Terminal-native means fewer context switches&lt;/h2&gt;
&lt;p&gt;AI coding tools usually fail developers not because they’re dumb, but because they create friction. If you have to copy prompts in one place, paste patches elsewhere, and keep reloading mental context, you lose momentum.&lt;/p&gt;
&lt;p&gt;Claude Code being &lt;strong&gt;terminal-native&lt;/strong&gt; is a quality-of-life decision that reads like a small detail but changes everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It sees your filesystem and your project structure directly.&lt;/li&gt;
&lt;li&gt;It can run commands in the same environment you use.&lt;/li&gt;
&lt;li&gt;It can inspect outputs and adjust based on real error messages.&lt;/li&gt;
&lt;li&gt;It’s naturally aligned with how developers already work: edit → run → read logs → repeat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what makes it feel like pair programming. In real pairing, you don’t ask your teammate to describe a patch and then you do the integration manually. You collaborate around an active session: they make changes, they run things, they show you what broke and why.&lt;/p&gt;
&lt;p&gt;Claude Code’s workflow matches that.&lt;/p&gt;
&lt;h2 id="work-on-this-problem-while-i-review-beats-generate-for-me"&gt;“Work on this problem while I review” beats “generate for me”&lt;/h2&gt;
&lt;p&gt;Here’s my firm opinion: the best AI coding tools shouldn’t replace your judgment. They should &lt;strong&gt;compress the busywork&lt;/strong&gt; between your judgment calls.&lt;/p&gt;
&lt;p&gt;The difference between “generate code for me to paste” and “work on this problem while I review your approach” is not just semantics—it changes how often you get interrupted.&lt;/p&gt;
&lt;p&gt;When the tool generates code for you, you’re responsible for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;choosing where to apply it,&lt;/li&gt;
&lt;li&gt;adapting it to your architecture,&lt;/li&gt;
&lt;li&gt;running tests,&lt;/li&gt;
&lt;li&gt;interpreting failures,&lt;/li&gt;
&lt;li&gt;and iterating.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the tool works on the problem, that burden shifts into an execution loop the tool can handle reliably. Your job becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;review the diff,&lt;/li&gt;
&lt;li&gt;sanity-check the strategy,&lt;/li&gt;
&lt;li&gt;confirm edge cases,&lt;/li&gt;
&lt;li&gt;and approve merge readiness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s the kind of productivity gain you actually feel in a real sprint. You don’t just write less code; you make fewer “dead end” attempts.&lt;/p&gt;
&lt;h3 id="how-to-get-better-results-immediately"&gt;How to get better results immediately&lt;/h3&gt;
&lt;p&gt;Claude Code performs best when you give it a crisp task boundary. Instead of vague prompts, use issue-style specificity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What’s broken (error message, failing test name, repro steps)?&lt;/li&gt;
&lt;li&gt;What “done” looks like (tests passing, performance constraint, API behavior)?&lt;/li&gt;
&lt;li&gt;Any constraints (don’t change public API, keep backward compatibility, follow existing patterns).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your issue description includes the right breadcrumbs, the agent can spend its time on implementation and verification rather than reverse-engineering your intent.&lt;/p&gt;
&lt;h2 id="practical-patterns-how-to-use-it-like-a-pro"&gt;Practical patterns: how to use it like a pro&lt;/h2&gt;
&lt;p&gt;Claude Code isn’t magic, and you shouldn’t treat it like you can delegate design. But you can structure work so the agent contributes at its strongest.&lt;/p&gt;
&lt;h3 id="1-start-with-a-well-scoped-github-issue"&gt;1) Start with a well-scoped GitHub issue&lt;/h3&gt;
&lt;p&gt;Great issues include the failure signal and expected behavior. Even if you’re not sure of the fix, tell it what’s wrong.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“&lt;code&gt;GET /v1/users&lt;/code&gt; returns 500 in staging”&lt;/li&gt;
&lt;li&gt;“Integration test &lt;code&gt;users-list&lt;/code&gt; fails with timeout”&lt;/li&gt;
&lt;li&gt;“Expected: returns 200 and includes &lt;code&gt;cursor&lt;/code&gt; field”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-let-it-run-the-same-checks-you-trust"&gt;2) Let it run the same checks you trust&lt;/h3&gt;
&lt;p&gt;If your repo has &lt;code&gt;make test&lt;/code&gt;, &lt;code&gt;pnpm test&lt;/code&gt;, or &lt;code&gt;pytest -q&lt;/code&gt;, make sure Claude Code uses those commands. The point isn’t “run something”; it’s “run the things that represent correctness.”&lt;/p&gt;
&lt;p&gt;When an agent runs your real suite, you get a meaningful signal rather than a superficial “it compiles” state.&lt;/p&gt;
&lt;h3 id="3-review-approach-not-just-the-diff"&gt;3) Review approach, not just the diff&lt;/h3&gt;
&lt;p&gt;The agentic loop produces more complete changes, which means your review should focus on the strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does it handle the failure mode it discovered?&lt;/li&gt;
&lt;li&gt;Did it introduce broader refactors than needed?&lt;/li&gt;
&lt;li&gt;Are new behaviors covered by tests?&lt;/li&gt;
&lt;li&gt;Did it respect existing conventions?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A quick scan of how it fixed the immediate issue—and whether that fix seems robust—will save you from merging subtle regressions.&lt;/p&gt;
&lt;h3 id="4-use-it-iteratively-on-hard-middle-problems"&gt;4) Use it iteratively on “hard middle” problems&lt;/h3&gt;
&lt;p&gt;Claude Code shines when the work isn’t just writing new code; it’s &lt;strong&gt;changing code under constraints&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;refactors that must preserve behavior,&lt;/li&gt;
&lt;li&gt;bug fixes that require understanding test failures,&lt;/li&gt;
&lt;li&gt;integration changes across multiple modules,&lt;/li&gt;
&lt;li&gt;cleanup of technical debt without breaking the build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s where a feedback loop is everything.&lt;/p&gt;
&lt;h2 id="the-real-promise-fewer-ai-driven-bugs-more-engineering"&gt;The real promise: fewer “AI-driven” bugs, more engineering&lt;/h2&gt;
&lt;p&gt;The best part about this category shift is psychological. Developers are cautious for a reason: AI-generated code can be syntactically correct and still wrong in ways that only show up during execution. Claude Code’s approach reduces that gap by staying close to execution—reading, writing, running, and iterating.&lt;/p&gt;
&lt;p&gt;It’s not that it never makes mistakes. It’s that mistakes are handled inside the workflow, not after you paste something into your repo and discover it fails. When the agent can observe failures and respond, the tool becomes less of a suggestion engine and more of a task worker.&lt;/p&gt;
&lt;p&gt;That’s why it feels like pair programming. Your attention moves from babysitting outputs to guiding outcomes.&lt;/p&gt;
&lt;h2 id="conclusion-the-first-ai-coding-tool-that-matches-how-teams-actually-build"&gt;Conclusion: the first AI coding tool that matches how teams actually build&lt;/h2&gt;
&lt;p&gt;Copilot was a breakthrough, but it mostly optimized for completion. Claude Code feels like a bigger idea: &lt;strong&gt;agentic coding in the terminal that treats your repo as the workspace&lt;/strong&gt; and treats your review as the final checkpoint.&lt;/p&gt;
&lt;p&gt;If you want AI that can truly help you ship—read the context, implement the fix, run the tests, and iterate until it works—Claude Code is the tool I’ve wanted since Copilot landed.&lt;/p&gt;</content></item><item><title>Gleam and Zig: New Languages That Actually Deserve the Hype</title><link>https://decastro.work/blog/gleam-zig-new-languages-deserve-hype/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/gleam-zig-new-languages-deserve-hype/</guid><description>&lt;p&gt;Every few years, a new language arrives with the same promise: “faster,” “safer,” “cleaner.” Most fade into the background noise. Gleam and Zig are different—not because they’re magic, but because their design decisions are unusually aligned with a very practical goal: respect the developer’s time. If you care about building real systems without drowning in toolchain surprises or debugging mysteries, these two deserve a serious look.&lt;/p&gt;
&lt;h2 id="why-hype-is-cheapand-good-design-isnt"&gt;Why hype is cheap—and good design isn’t&lt;/h2&gt;
&lt;p&gt;Language hype usually comes from marketing, not engineering. It’s easy to say “best-in-class” and hard to deliver on the boring parts: predictable compilation errors, readable code, tooling that doesn’t fight you, and performance you can reason about.&lt;/p&gt;</description><content>&lt;p&gt;Every few years, a new language arrives with the same promise: “faster,” “safer,” “cleaner.” Most fade into the background noise. Gleam and Zig are different—not because they’re magic, but because their design decisions are unusually aligned with a very practical goal: respect the developer’s time. If you care about building real systems without drowning in toolchain surprises or debugging mysteries, these two deserve a serious look.&lt;/p&gt;
&lt;h2 id="why-hype-is-cheapand-good-design-isnt"&gt;Why hype is cheap—and good design isn’t&lt;/h2&gt;
&lt;p&gt;Language hype usually comes from marketing, not engineering. It’s easy to say “best-in-class” and hard to deliver on the boring parts: predictable compilation errors, readable code, tooling that doesn’t fight you, and performance you can reason about.&lt;/p&gt;
&lt;p&gt;Gleam and Zig share an almost contrarian philosophy: the language should make the common path smooth, and the uncommon path survivable.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gleam&lt;/strong&gt; targets the &lt;strong&gt;BEAM VM&lt;/strong&gt;, bringing &lt;strong&gt;typed, functional programming&lt;/strong&gt; with a syntax people often find familiar if they’ve used TypeScript or modern ML-style languages—without giving up the BEAM’s resilience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zig&lt;/strong&gt; targets systems work with a tone that’s refreshingly explicit: &lt;strong&gt;manual control without hidden complexity&lt;/strong&gt;, and error handling that’s visible rather than magical.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Neither is positioned as “replace everything tomorrow.” Instead, each is a bet on something more durable: making developers faster without making correctness a suffering contest.&lt;/p&gt;
&lt;h2 id="gleam-typed-functional-programming-without-the-move-fast-tax"&gt;Gleam: typed functional programming, without the “move fast” tax&lt;/h2&gt;
&lt;p&gt;Gleam is a functional language that compiles to the &lt;strong&gt;BEAM&lt;/strong&gt;, the same runtime behind Erlang and Elixir. That’s not a trivia detail—it matters. On BEAM, the system is built for concurrency, distribution, and fault tolerance. The language gives you a typed experience that feels designed to reduce the classic functional pain point: unclear invariants.&lt;/p&gt;
&lt;h3 id="what-erlang-style-fault-tolerance-looks-like-in-practice"&gt;What “Erlang-style fault tolerance” looks like in practice&lt;/h3&gt;
&lt;p&gt;In BEAM land, crashes are often treated as a normal event. Processes isolate failures, supervisors restart what needs restarting, and the system keeps moving.&lt;/p&gt;
&lt;p&gt;Gleam builds on that mental model. You write code with types that push errors earlier, then rely on BEAM’s runtime behavior for resilience. The combination is especially attractive for real-world services: APIs, background jobs, messaging, and anything that benefits from long-running reliability.&lt;/p&gt;
&lt;h3 id="a-taste-of-the-developer-experience"&gt;A taste of the developer experience&lt;/h3&gt;
&lt;p&gt;You don’t need to become a Lisp acolyte to start shipping with Gleam. The syntax is intentionally approachable. Where many functional languages feel like a tax you must pay before you can think, Gleam tries to keep you close to the logic you care about.&lt;/p&gt;
&lt;p&gt;For example, instead of fighting type inference you don’t understand, you can structure functions and types so the compiler becomes a helpful reviewer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use explicit types for boundaries (public functions, data entering/exiting the system).&lt;/li&gt;
&lt;li&gt;Let the compiler guide refactors by tightening types as you change logic.&lt;/li&gt;
&lt;li&gt;Prefer small composable functions—BEAM’s concurrency model pairs naturally with that style.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve used TypeScript, you’ll probably recognize the instinct: “make invalid states unrepresentable,” but without the same runtime guesswork.&lt;/p&gt;
&lt;h3 id="practical-use-cases-that-justify-the-time-investment"&gt;Practical use cases that justify the time investment&lt;/h3&gt;
&lt;p&gt;Gleam is a strong candidate when you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;typed functional core&lt;/strong&gt; inside a distributed service.&lt;/li&gt;
&lt;li&gt;BEAM’s &lt;strong&gt;fault tolerance&lt;/strong&gt; for long-lived processes.&lt;/li&gt;
&lt;li&gt;A language that doesn’t require you to abandon modern expectations around tooling and readability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example: imagine building a notification service. You’ll likely handle queue consumers, retries, and transient failures. Gleam’s combination of types and runtime fault tolerance maps cleanly to that reality.&lt;/p&gt;
&lt;h2 id="zig-c-level-control-with-explicit-error-handling-and-fewer-surprises"&gt;Zig: C-level control with explicit error handling (and fewer surprises)&lt;/h2&gt;
&lt;p&gt;Zig’s appeal is straightforward: you want the performance and low-level control of C, but you don’t want to inherit C’s accident-prone ergonomics. Zig is opinionated about what “explicit” should mean.&lt;/p&gt;
&lt;h3 id="explicit-error-handling-isnt-academicit-changes-how-you-debug"&gt;“Explicit error handling” isn’t academic—it changes how you debug&lt;/h3&gt;
&lt;p&gt;In many languages, failures are either exceptions, panics, or “return codes you forget to check.” Zig makes error paths part of the type system and control flow.&lt;/p&gt;
&lt;p&gt;That means when something goes wrong, the program’s structure already told you how errors are supposed to propagate. Less time is wasted hunting for whether a function can fail and—if it does—how.&lt;/p&gt;
&lt;p&gt;A practical pattern looks like this (conceptually): functions that can fail return an error union type, and callers handle the outcome explicitly. The compiler won’t let you pretend failure can’t happen.&lt;/p&gt;
&lt;h3 id="cross-compilation-that-feels-trivial"&gt;Cross-compilation that feels trivial&lt;/h3&gt;
&lt;p&gt;One of Zig’s strengths is that cross-compilation is designed into the workflow rather than bolted on. If you’ve ever wrestled with CMake toolchains, environment variables, and mysterious linker errors, you know how much time a build system can eat.&lt;/p&gt;
&lt;p&gt;Zig’s approach aims to make the “target matrix” less painful. Even if you only ship to one platform today, a smoother path to additional platforms becomes valuable fast—especially for tools, embedded-ish use cases, or performance-sensitive services.&lt;/p&gt;
&lt;h3 id="practical-use-cases-that-justify-the-time-investment-1"&gt;Practical use cases that justify the time investment&lt;/h3&gt;
&lt;p&gt;Zig shines when you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FFI-friendly&lt;/strong&gt; code that can integrate with C ecosystems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable resource management&lt;/strong&gt; without the overhead of a heavy runtime.&lt;/li&gt;
&lt;li&gt;A build workflow that doesn’t turn portability into a recurring tax.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think: writing a high-performance library used by multiple services, building developer tooling, or implementing system components where you want the constraints to be obvious.&lt;/p&gt;
&lt;h2 id="shared-philosophy-they-both-respect-your-time"&gt;Shared philosophy: they both respect your time&lt;/h2&gt;
&lt;p&gt;It’s tempting to compare languages on headline features. But the real differentiator is how often the language interrupts your work with avoidable friction.&lt;/p&gt;
&lt;h3 id="gleam-respects-time-by-catching-mistakes-earlythen-leaning-on-beam-for-resilience"&gt;Gleam respects time by catching mistakes early—then leaning on BEAM for resilience&lt;/h3&gt;
&lt;p&gt;Gleam’s “typed and pragmatic” vibe means the compiler helps you avoid entire categories of mistakes. And when something still goes wrong at runtime—because reality is messy—the BEAM ecosystem expects failures and provides a recovery model.&lt;/p&gt;
&lt;p&gt;That’s not a guarantee of perfection. It’s a guarantee that your system is designed to survive imperfect conditions.&lt;/p&gt;
&lt;h3 id="zig-respects-time-by-making-control-flow-and-failure-modes-visible"&gt;Zig respects time by making control flow and failure modes visible&lt;/h3&gt;
&lt;p&gt;Zig’s explicit approach reduces the mental overhead of “what could happen here?” and “did we check the error?” You don’t need to rely on conventions or code reviews to keep failure handling correct.&lt;/p&gt;
&lt;p&gt;Even better: the language’s explicitness tends to make debugging less of a detective novel. You can reason about the program because the program tells you what it’s doing.&lt;/p&gt;
&lt;h2 id="choosing-where-to-start-a-practical-learning-plan"&gt;Choosing where to start: a practical learning plan&lt;/h2&gt;
&lt;p&gt;If you’re deciding whether to spend time on Gleam or Zig, don’t start with an abstract language preference. Start with a project shape.&lt;/p&gt;
&lt;h3 id="if-your-work-is-backend-heavy-start-with-gleam"&gt;If your work is backend-heavy: start with Gleam&lt;/h3&gt;
&lt;p&gt;Pick one subsystem you already maintain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;queue consumer&lt;/li&gt;
&lt;li&gt;background job worker&lt;/li&gt;
&lt;li&gt;websocket handler&lt;/li&gt;
&lt;li&gt;event processing pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your goal isn’t to rewrite everything. It’s to validate that typed Gleam can model your domain without turning everyday refactors into a type-system battle.&lt;/p&gt;
&lt;p&gt;Practical approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write a small module with clear input/output types.&lt;/li&gt;
&lt;li&gt;Add concurrency boundaries where BEAM shines.&lt;/li&gt;
&lt;li&gt;Let the compiler drive refactors while you keep runtime behavior understandable.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="if-your-work-is-systems-heavy-start-with-zig"&gt;If your work is systems-heavy: start with Zig&lt;/h3&gt;
&lt;p&gt;Choose a component where performance or portability matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a parsing library&lt;/li&gt;
&lt;li&gt;a CLI tool that must be fast and portable&lt;/li&gt;
&lt;li&gt;a low-level integration layer (FFI)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with a function or module that returns explicit error unions.&lt;/li&gt;
&lt;li&gt;Build with cross-compilation from day one (even if you test locally).&lt;/li&gt;
&lt;li&gt;Refactor only when the error model and interfaces stay coherent.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="a-smart-middle-path-learn-both-but-for-different-reasons"&gt;A smart middle path: learn both, but for different reasons&lt;/h3&gt;
&lt;p&gt;You don’t have to become a polyglot collector. Learn each language for the problems it actually solves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gleam&lt;/strong&gt;: typed reliability on a resilient runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zig&lt;/strong&gt;: explicit control and predictable performance with a friendlier build experience than the C baseline.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion-invest-time-where-the-design-is-doing-real-work"&gt;Conclusion: invest time where the design is doing real work&lt;/h2&gt;
&lt;p&gt;Gleam and Zig won’t replace your stack overnight. But they’re not hype artifacts—they’re deliberate attempts to solve long-standing developer pain.&lt;/p&gt;
&lt;p&gt;Gleam pairs a typed, approachable syntax with BEAM’s fault-tolerant runtime philosophy, making real services easier to build and maintain. Zig brings C-level control while insisting that errors and failure paths are first-class, and it treats cross-compilation as a normal workflow rather than a torment.&lt;/p&gt;
&lt;p&gt;If you want languages that earn time, not beg for attention, these two are worth putting on your short list—and starting small this week.&lt;/p&gt;</content></item><item><title>The Honest Guide to AI-Assisted Development in 2025</title><link>https://decastro.work/blog/honest-guide-ai-assisted-development-2025/</link><pubDate>Wed, 19 Mar 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/honest-guide-ai-assisted-development-2025/</guid><description>&lt;p&gt;AI-assisted development in 2025 isn’t a magic button—it’s a workflow. After two years of daily tool usage, the most useful thing I can tell you is this: AI is best at helping you think faster and write cleaner, not at making final decisions on your behalf. Treat it like a sharp junior teammate with great recall and unreliable judgment, and you’ll get real leverage. Treat it like an autopilot, and you’ll ship confident bugs.&lt;/p&gt;</description><content>&lt;p&gt;AI-assisted development in 2025 isn’t a magic button—it’s a workflow. After two years of daily tool usage, the most useful thing I can tell you is this: AI is best at helping you think faster and write cleaner, not at making final decisions on your behalf. Treat it like a sharp junior teammate with great recall and unreliable judgment, and you’ll get real leverage. Treat it like an autopilot, and you’ll ship confident bugs.&lt;/p&gt;
&lt;h2 id="what-works-and-why-it-works"&gt;What Works (and Why It Works)&lt;/h2&gt;
&lt;p&gt;There’s a pattern behind the best AI use cases: tasks where the input already contains the relevant context, the output is bounded, and correctness is checkable. In 2025, these are the areas where AI consistently shines.&lt;/p&gt;
&lt;h3 id="1-generating-tests-from-specifications"&gt;1) Generating tests from specifications&lt;/h3&gt;
&lt;p&gt;When you can describe expected behavior in plain language (or in existing acceptance criteria), AI can produce a strong first draft of tests—especially for edge cases developers forget.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; You have an API endpoint: &lt;code&gt;POST /refunds&lt;/code&gt; that should reject refunds over the remaining balance and require idempotency keys. You can prompt AI with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input: a short spec of validation rules&lt;/li&gt;
&lt;li&gt;Output format: “Write unit tests for validation and idempotency behavior”&lt;/li&gt;
&lt;li&gt;Tech details: your test framework, mocking approach, and naming conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI will often generate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parameterized tests for boundary conditions&lt;/li&gt;
&lt;li&gt;tests for error response shapes&lt;/li&gt;
&lt;li&gt;idempotency tests that ensure the second request doesn’t re-apply the refund&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; immediately tighten the tests. AI will usually get the “what” right, but not always the “how strict.” Use your existing test suite style as the template, and run everything on CI before you trust it.&lt;/p&gt;
&lt;h3 id="2-explaining-unfamiliar-codebases"&gt;2) Explaining unfamiliar codebases&lt;/h3&gt;
&lt;p&gt;AI is surprisingly effective at turning “mystery code” into a navigable map—especially when you provide the file(s) and describe what you’re trying to change.&lt;/p&gt;
&lt;p&gt;A good prompt looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Here’s &lt;code&gt;authMiddleware.ts&lt;/code&gt; and how the router uses it. Explain the request flow and where errors are handled.”&lt;/li&gt;
&lt;li&gt;“What invariants does this code assume?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not clairvoyance; it’s summarization plus pattern matching. If you feed it the relevant modules, it can explain how they interact and where the risks are.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; ask for &lt;em&gt;implications&lt;/em&gt;, not just descriptions. For example: “Where would a null value cause a crash?” or “What happens if the token is expired?”&lt;/p&gt;
&lt;h3 id="3-rubber-duck-debugging-with-better-recall"&gt;3) Rubber-duck debugging (with better recall)&lt;/h3&gt;
&lt;p&gt;Rubber-duck debugging works because you force your brain to articulate the system. AI can accelerate that by acting as the duck—but with a better ability to keep track of details.&lt;/p&gt;
&lt;p&gt;Use it like a structured interrogation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;“Here’s what I expected.”&lt;/li&gt;
&lt;li&gt;“Here’s what actually happened.”&lt;/li&gt;
&lt;li&gt;“Here’s the relevant code and logs.”&lt;/li&gt;
&lt;li&gt;“List 5 plausible root causes, ranked by likelihood, and tell me what single test would confirm each.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You’ll get clearer hypotheses faster than you would alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; don’t ask “why is this failing?” Ask for experiments. The best AI outputs are testable.&lt;/p&gt;
&lt;h3 id="4-writing-boilerplate-and-glue-code"&gt;4) Writing boilerplate and glue code&lt;/h3&gt;
&lt;p&gt;Boilerplate is the sweet spot because it’s mechanical and easy to validate.&lt;/p&gt;
&lt;p&gt;Think:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mapping DTOs&lt;/li&gt;
&lt;li&gt;wiring dependencies&lt;/li&gt;
&lt;li&gt;request/response shaping&lt;/li&gt;
&lt;li&gt;repetitive CRUD handlers&lt;/li&gt;
&lt;li&gt;translating between languages or frameworks (“Write a TypeScript version of this Java logic,” or vice versa)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI is excellent at producing an initial draft of these pieces that you can then review and adapt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; insist on style consistency. If your team prefers explicit error types or specific naming conventions, bake them into the prompt.&lt;/p&gt;
&lt;h3 id="5-translating-between-languages-with-attention-to-idioms"&gt;5) Translating between languages (with attention to idioms)&lt;/h3&gt;
&lt;p&gt;Translation is more than syntax. AI helps most when you care about idioms, not just compilation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Port a Python function to Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide the Python version and the required behavior.&lt;/li&gt;
&lt;li&gt;Ask for the Go version using the idiomatic error handling (&lt;code&gt;error&lt;/code&gt; returns, explicit checks).&lt;/li&gt;
&lt;li&gt;Request a small set of tests that confirm parity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI often nails the intent, then you fix edge behaviors and formatting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; don’t skip equivalence tests. Translation is a place where subtle semantics (null vs empty, timezone parsing, overflow behavior) can quietly diverge.&lt;/p&gt;
&lt;h2 id="what-doesnt-work-or-works-poorly"&gt;What Doesn’t Work (or Works Poorly)&lt;/h2&gt;
&lt;p&gt;AI is still weak where the “context radius” explodes—where decisions depend on system-level knowledge, threat models, performance constraints, and real-world operational tradeoffs. In these areas, AI can easily provide plausible-sounding wrong answers.&lt;/p&gt;
&lt;h3 id="1-architecture-decisions"&gt;1) Architecture decisions&lt;/h3&gt;
&lt;p&gt;Architecture is not just code—it’s tradeoffs over time. AI can generate diagrams and high-level components, but it can’t responsibly choose constraints you haven’t supplied.&lt;/p&gt;
&lt;p&gt;If you ask for “the best architecture for X,” you’ll likely get a polished proposal that ignores:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your existing deployment reality&lt;/li&gt;
&lt;li&gt;team ownership boundaries&lt;/li&gt;
&lt;li&gt;operational costs&lt;/li&gt;
&lt;li&gt;failure modes and recovery strategies&lt;/li&gt;
&lt;li&gt;product constraints (latency vs throughput, compliance, rollout strategy)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What to do instead:&lt;/strong&gt; use AI to enumerate options and risks, not to decide. Ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“List three architecture options and the specific risks each introduces.”&lt;/li&gt;
&lt;li&gt;“What questions should I ask stakeholders before choosing?”
Then decide with human judgment and, ideally, hard requirements.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-security-sensitive-code"&gt;2) Security-sensitive code&lt;/h3&gt;
&lt;p&gt;This is the hard line. AI can help, but it shouldn’t be the authority for security properties. For auth flows, crypto usage, sanitization, and authorization logic, “almost correct” can be catastrophic.&lt;/p&gt;
&lt;p&gt;Common failure modes include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;incorrect assumptions about threat models&lt;/li&gt;
&lt;li&gt;incomplete validation&lt;/li&gt;
&lt;li&gt;insecure defaults&lt;/li&gt;
&lt;li&gt;misuse of cryptographic primitives&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; if AI drafts security-related code, treat it as a suspect contribution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;require code review by someone who owns security&lt;/li&gt;
&lt;li&gt;add tests for adversarial inputs&lt;/li&gt;
&lt;li&gt;verify with established libraries and patterns you already trust&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-performance-optimization"&gt;3) Performance optimization&lt;/h3&gt;
&lt;p&gt;Performance work requires measurement, profiling context, and a model of the workload. AI can suggest optimizations, but it can’t observe your production bottlenecks.&lt;/p&gt;
&lt;p&gt;Even when AI identifies plausible hot paths, it may:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;optimize the wrong layer&lt;/li&gt;
&lt;li&gt;recommend complexity where a simple cache would do&lt;/li&gt;
&lt;li&gt;propose micro-optimizations that don’t matter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What to do instead:&lt;/strong&gt; let AI help you design experiments. Use prompts like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Given this endpoint, what metrics would you profile first?”&lt;/li&gt;
&lt;li&gt;“Suggest profiling steps and what you’d expect to see if X is the bottleneck.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use the tool’s reasoning to guide measurement, then let reality decide.&lt;/p&gt;
&lt;h3 id="4-anything-requiring-system-level-context"&gt;4) Anything requiring system-level context&lt;/h3&gt;
&lt;p&gt;If the correctness depends on infrastructure, data distribution, concurrency assumptions, or operational constraints, AI is operating blind.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;queue semantics and retry behavior across services&lt;/li&gt;
&lt;li&gt;eventual consistency guarantees in your actual data stores&lt;/li&gt;
&lt;li&gt;resource limits in Kubernetes and autoscaling behavior&lt;/li&gt;
&lt;li&gt;how your CI, feature flags, and rollout process interact&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI can help you reason about what context is missing—but it can’t substitute for it.&lt;/p&gt;
&lt;h2 id="the-two-biggest-mistakes-developers-make"&gt;The Two Biggest Mistakes Developers Make&lt;/h2&gt;
&lt;h3 id="mistake-1-using-ai-as-a-code-generator-instead-of-a-thinking-partner"&gt;Mistake #1: Using AI as a code generator instead of a thinking partner&lt;/h3&gt;
&lt;p&gt;This is the most common failure I see: developers paste a prompt, receive a “working” implementation, and move on. The mental work disappears. So does verification.&lt;/p&gt;
&lt;p&gt;When you use AI as a thinking partner, you’re not asking for “the code.” You’re asking for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;options&lt;/li&gt;
&lt;li&gt;tradeoffs&lt;/li&gt;
&lt;li&gt;hypotheses&lt;/li&gt;
&lt;li&gt;explanations of assumptions&lt;/li&gt;
&lt;li&gt;test plans&lt;/li&gt;
&lt;li&gt;refactoring strategies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;A better workflow:&lt;/strong&gt; have AI propose, then you interrogate.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Draft a solution, but also list 10 assumptions you’re making.”&lt;/li&gt;
&lt;li&gt;“What edge cases does this miss?”&lt;/li&gt;
&lt;li&gt;“How would you prove it’s correct with tests?”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mistake-2-not-verifying-ai-output-with-the-same-rigor-as-a-junior-devs-pr"&gt;Mistake #2: Not verifying AI output with the same rigor as a junior dev’s PR&lt;/h3&gt;
&lt;p&gt;If AI output is treated as authoritative, you’ll ship errors that look like mistakes nobody should make. The fix is simple in principle: verify.&lt;/p&gt;
&lt;p&gt;At minimum:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run unit tests and lint&lt;/li&gt;
&lt;li&gt;add regression tests for any changed behavior&lt;/li&gt;
&lt;li&gt;perform code review for readability and invariants&lt;/li&gt;
&lt;li&gt;check for security-relevant patterns (input validation, authz/authn flow correctness, secrets handling)&lt;/li&gt;
&lt;li&gt;validate performance assumptions with benchmarks or profiler output when relevant&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical rule:&lt;/strong&gt; the AI response must earn trust, not receive it.&lt;/p&gt;
&lt;h2 id="a-practical-ai-assisted-workflow-that-holds-up"&gt;A Practical “AI-Assisted” Workflow That Holds Up&lt;/h2&gt;
&lt;p&gt;Here’s a pattern that consistently produces quality without turning your team into prompt engineers.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Provide constraints and examples.&lt;/strong&gt;&lt;br&gt;
“Here’s the function signature, here’s how errors are represented, here’s what a valid request looks like.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request reasoning artifacts, not just code.&lt;/strong&gt;&lt;br&gt;
Ask for test cases, edge cases, assumptions, and failure modes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate drafts, then revise with human ownership.&lt;/strong&gt;&lt;br&gt;
Treat AI as a starting point. You remain accountable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify with the same discipline as any PR.&lt;/strong&gt;&lt;br&gt;
Tests, review, and CI gates are non-negotiable—especially for security and correctness-critical logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure before optimizing.&lt;/strong&gt;&lt;br&gt;
Let AI propose experiments; let data decide.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; Suppose you’re adding a new language integration. Use AI to translate and scaffold, but require parity tests. Then run a small suite that compares outputs across edge cases (timezones, encoding, whitespace normalization). That single move prevents the most common “translation drift” bugs.&lt;/p&gt;
&lt;h2 id="conclusion-use-ai-for-speed-keep-humans-for-judgment"&gt;Conclusion: Use AI for Speed, Keep Humans for Judgment&lt;/h2&gt;
&lt;p&gt;In 2025, AI-assisted development works best when you align it with bounded, testable tasks: generating tests, explaining code, accelerating boilerplate, and translating logic between languages. It struggles—and can genuinely mislead—when you’re doing architecture, security-sensitive engineering, performance tuning, or anything that depends on system-level context.&lt;/p&gt;
&lt;p&gt;If there’s one mindset to adopt, it’s this: AI is your thinking partner and draft generator, not your decision-maker. The moment you require the same verification rigor you’d demand from a junior dev’s PR, AI stops being a gamble and starts being an advantage.&lt;/p&gt;</content></item><item><title>Your Monolith Is Fine. Your Monorepo Is Fine. Stop Apologizing.</title><link>https://decastro.work/blog/monolith-fine-monorepo-fine-stop-apologizing/</link><pubDate>Thu, 13 Mar 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/monolith-fine-monorepo-fine-stop-apologizing/</guid><description>&lt;p&gt;There’s a particular kind of tech insecurity that shows up in pull requests, architecture docs, and sprint planning: the urge to preface your choices with a justification. &lt;em&gt;“We know it’s a monolith, but…”&lt;/em&gt; or &lt;em&gt;“We know this deployment setup is intense, but…”&lt;/em&gt; Stop. The goal isn’t to be impressive at architecture debates. The goal is to ship software that users can actually benefit from.&lt;/p&gt;
&lt;p&gt;Both monoliths and microservices can be great. Both monorepos and polyrepos can work. What doesn’t work is treating architecture like a personality test—or freezing your roadmap in the name of “future-proofing.” If your team can deliver, recover from incidents, and evolve the system without heroic effort, your architecture is already doing its job.&lt;/p&gt;</description><content>&lt;p&gt;There’s a particular kind of tech insecurity that shows up in pull requests, architecture docs, and sprint planning: the urge to preface your choices with a justification. &lt;em&gt;“We know it’s a monolith, but…”&lt;/em&gt; or &lt;em&gt;“We know this deployment setup is intense, but…”&lt;/em&gt; Stop. The goal isn’t to be impressive at architecture debates. The goal is to ship software that users can actually benefit from.&lt;/p&gt;
&lt;p&gt;Both monoliths and microservices can be great. Both monorepos and polyrepos can work. What doesn’t work is treating architecture like a personality test—or freezing your roadmap in the name of “future-proofing.” If your team can deliver, recover from incidents, and evolve the system without heroic effort, your architecture is already doing its job.&lt;/p&gt;
&lt;h2 id="the-real-question-isnt-what-architecture-its-what-friction"&gt;The real question isn’t “what architecture,” it’s “what friction?”&lt;/h2&gt;
&lt;p&gt;Frameworks aside, architecture is just a tool for managing friction. The friction shows up as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How fast you can change&lt;/strong&gt; code and see the impact (local dev loop, CI time, release cadence).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How safely you can change&lt;/strong&gt; without causing outages or regressions (testing, rollback, blast radius).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How predictably you can scale&lt;/strong&gt; when usage changes (performance isolation, capacity planning).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How painful it is to operate&lt;/strong&gt; day to day (observability, incident response, deployment pipelines).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A monolith typically wins when friction is dominated by &lt;em&gt;organizational coordination&lt;/em&gt; and &lt;em&gt;integration overhead&lt;/em&gt;. A microservices approach can win when friction is dominated by &lt;em&gt;independent scaling needs&lt;/em&gt; and &lt;em&gt;different lifecycles&lt;/em&gt; between domains.&lt;/p&gt;
&lt;p&gt;A monorepo typically wins when friction is dominated by &lt;em&gt;cross-team coordination&lt;/em&gt; and &lt;em&gt;shared interfaces&lt;/em&gt;. A polyrepo can win when friction is dominated by &lt;em&gt;hard boundaries&lt;/em&gt;—legal, organizational, or release autonomy—that you genuinely need to enforce.&lt;/p&gt;
&lt;p&gt;The trap is pretending those tradeoffs map cleanly to ideology. They don’t. They map to your constraints.&lt;/p&gt;
&lt;h2 id="monoliths-arent-a-moral-failingcoordination-is-the-point"&gt;Monoliths aren’t a moral failing—coordination is the point&lt;/h2&gt;
&lt;p&gt;If you have a single codebase with clear boundaries inside it, you’re not “wrong.” You’re simply optimizing for the realities of engineering teams.&lt;/p&gt;
&lt;p&gt;Here’s what a well-run monolith looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your domain boundaries are explicit&lt;/strong&gt;: packages/modules/classes map to business concepts, not just technical convenience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your APIs are stable internally&lt;/strong&gt;: you avoid “randomly calling any function anywhere” by treating modules like contracts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your CI pipeline is disciplined&lt;/strong&gt;: linting, unit tests, integration tests, and fast feedback are part of the product—not optional ceremony.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your releases are boring&lt;/strong&gt;: frequent deploys, automated rollback, and feature flags reduce the stress that people mistakenly attribute to the “monolith vs microservices” question.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Concrete example: imagine a retail platform where checkout, catalog browsing, and order management live together. If checkout needs to evolve weekly and the catalog needs to evolve monthly, you don’t need microservices to decouple those rhythms—you need internal boundaries and tooling. You can isolate deploy risk with feature flags and carve out independent deployable components &lt;em&gt;within&lt;/em&gt; the monolith (for example, background jobs or internal modules that can be toggled independently).&lt;/p&gt;
&lt;p&gt;Now, when does a monolith start to hurt? Usually not at “scale of traffic” alone. It’s when teams can’t move independently anymore because changes collide constantly—when build times explode, when deploys become a scary event, when understanding the system requires a small group of “architect guardians.”&lt;/p&gt;
&lt;p&gt;That’s not a referendum on monoliths. It’s an alarm that your internal structure and engineering workflow need investment.&lt;/p&gt;
&lt;h2 id="microservices-arent-a-victory-lapdeployment-complexity-is-real"&gt;Microservices aren’t a victory lap—deployment complexity is real&lt;/h2&gt;
&lt;p&gt;Microservices are often sold as independence. Sometimes they deliver it. Often they deliver a new kind of coordination: you trade “one repo” problems for “many moving parts” problems.&lt;/p&gt;
&lt;p&gt;Microservices add real operational load:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deployment pipelines per service&lt;/strong&gt; (or at least per group)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service-to-service communication&lt;/strong&gt; (and the failure modes that come with it)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versioning and compatibility&lt;/strong&gt; (contracts don’t stay compatible by vibes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability across boundaries&lt;/strong&gt; (logs aren’t enough; you need tracing and metrics that actually connect)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common mistake is building microservices around organizational hopes rather than actual requirements. If you have a single team that changes everything together, splitting into services can amplify the cost of every change. You’ll end up writing distributed systems glue—retries, timeouts, circuit breakers—just to recreate what a monolith already handles naturally.&lt;/p&gt;
&lt;p&gt;Concrete example: two services, &lt;code&gt;user-profile&lt;/code&gt; and &lt;code&gt;billing&lt;/code&gt;, both changed together every sprint. If you introduce an API boundary between them without a real reason (separate scaling needs, separate teams, separate compliance, or independent release timelines), you’ll likely slow the team down. Your integration tests will become more complex. Your debugging will become more distributed. Your release process will become less “one click, done” and more “orchestrate the dance.”&lt;/p&gt;
&lt;p&gt;Microservices shine when you truly need independent evolution: different service ownership, different release cadence, different scaling constraints, or clear domain separation where teams can move without constant cross-team synchronization.&lt;/p&gt;
&lt;p&gt;If that’s not your reality yet, microservices will feel like paying for a gym membership you don’t use.&lt;/p&gt;
&lt;h2 id="monorepos-arent-too-muchtheyre-coordination-made-practical"&gt;Monorepos aren’t “too much”—they’re coordination made practical&lt;/h2&gt;
&lt;p&gt;Let’s talk monorepos without hand-wringing. A monorepo is just an engineering contract with your future self: &lt;em&gt;shared code should be discoverable, tested, and versioned consistently.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A monorepo is especially effective when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Teams frequently share libraries and components.&lt;/li&gt;
&lt;li&gt;You want consistent code review and automated testing across the stack.&lt;/li&gt;
&lt;li&gt;You need atomic changes across multiple packages (or at least predictable change sets).&lt;/li&gt;
&lt;li&gt;You rely on internal tools or shared schemas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The monorepo anti-pattern isn’t the repository. It’s the tooling and workflow. If your CI runs everything every time, your monorepo will feel slow. If developers can’t find the right library quickly, your monorepo will feel confusing. If ownership is unclear, you’ll get dependency sprawl.&lt;/p&gt;
&lt;p&gt;So make monorepos work by investing in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incremental builds and targeted testing&lt;/strong&gt; (run what changed, not everything).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear code ownership&lt;/strong&gt; (CODEOWNERS, teams, or explicit module maintainers).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good boundaries&lt;/strong&gt; (lint rules, package visibility, and dependency constraints).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer experience&lt;/strong&gt; (fast local builds, sensible test commands, and documentation that doesn’t rot).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yes, polyrepos can be great too. If you have strict release independence or hard organizational boundaries, polyrepos may reduce accidental coupling. But don’t pretend monorepos are inherently unscalable or inherently “unconventional.” They’re only as painful as your build, test, and ownership discipline.&lt;/p&gt;
&lt;h2 id="the-worst-architecture-is-the-halfway-one"&gt;The worst architecture is the halfway one&lt;/h2&gt;
&lt;p&gt;The most expensive pattern I see isn’t monolith or microservices—it’s mid-migration. The team ends up with the complexity of both worlds and the benefits of neither.&lt;/p&gt;
&lt;p&gt;What does “halfway” typically look like?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A monolith that’s been “service-ified” without a clean boundary plan, leaving tangled shared code and unclear ownership.&lt;/li&gt;
&lt;li&gt;A microservices rollout where only the newest features are split out, but the old system still drives critical workflows.&lt;/li&gt;
&lt;li&gt;Deploy pipelines that become a labyrinth: some changes go through monolith release, others through service releases, and both must be coordinated to make a single product change work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is predictable: slower iteration, more outages, and a constant tax of “Do we change the monolith or the service?” That ambiguity doesn’t stay theoretical—it turns into production incidents, and then it turns into blame.&lt;/p&gt;
&lt;p&gt;Pick a direction based on where your current friction actually lives.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your friction is &lt;strong&gt;team coordination and integration pain&lt;/strong&gt;, improve the monolith and tighten boundaries.&lt;/li&gt;
&lt;li&gt;If your friction is &lt;strong&gt;operational isolation and independent scaling&lt;/strong&gt;, commit to microservices (or at least a clear service extraction strategy).&lt;/li&gt;
&lt;li&gt;If your friction is &lt;strong&gt;release governance and shared dependencies&lt;/strong&gt;, choose monorepo and invest in tooling—or choose polyrepo and enforce boundaries intentionally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And when you do migrate, migrate with an end state in mind. Don’t start a journey without knowing what “done” looks like.&lt;/p&gt;
&lt;h2 id="how-to-decide-without-mythology"&gt;How to decide without mythology&lt;/h2&gt;
&lt;p&gt;Here’s a decision framework you can use this sprint, not in some mythical architecture committee:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write down your current deployment cadence.&lt;/strong&gt;&lt;br&gt;
If you deploy multiple times per day, microservices may add overhead without payoff. If you deploy monthly because the monolith release is scary, you may need better release engineering and internal modularity before changing architecture.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Identify your scaling pain.&lt;/strong&gt;&lt;br&gt;
Is performance constrained globally (monolith bottleneck) or selectively (a domain with distinct load patterns)? Microservices can help when specific domains need independent scaling, not when you just want to “be scalable.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Measure change propagation.&lt;/strong&gt;&lt;br&gt;
When a developer merges a feature, how many systems need to change? If it’s always everything, microservices won’t reduce the coordination cost yet. Focus on internal boundaries, tests, and feature flags.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Assess team topology.&lt;/strong&gt;&lt;br&gt;
Do you have stable team ownership per domain? If yes, microservices can map cleanly to teams. If not, a monolith with modular boundaries may be a better starting point.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Decide on repo strategy based on dependency reality.&lt;/strong&gt;&lt;br&gt;
If shared code is constant and correct integration matters, a monorepo usually reduces friction. If boundaries are strict and releases must be independent by policy, polyrepo might be appropriate.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key is to treat architecture as a living implementation detail of your delivery system. Don’t worship patterns. Build what reduces your time-to-productive-change.&lt;/p&gt;
&lt;h2 id="conclusion-stop-apologizingstart-optimizing"&gt;Conclusion: Stop apologizing—start optimizing&lt;/h2&gt;
&lt;p&gt;Monoliths are fine. Microservices are fine. Monorepos are fine. Polyrepos are fine. The only failing architecture is the one that stalls your team’s ability to ship reliable improvements.&lt;/p&gt;
&lt;p&gt;If your system is understandable, your deployments are manageable, and your team can iterate without dread, you already have the right architecture for today. Invest in boundaries, testing, observability, and developer experience—then revisit when your friction profile changes.&lt;/p&gt;
&lt;p&gt;Ship features. Keep the lights on. And retire the apology.&lt;/p&gt;</content></item><item><title>Python Ate Another Seven Points of Market Share and AI Is Why</title><link>https://decastro.work/blog/python-ate-seven-points-market-share-ai/</link><pubDate>Fri, 07 Mar 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/python-ate-seven-points-market-share-ai/</guid><description>&lt;p&gt;Every few years, the tech world witnesses a shift that feels almost unfair to legacy thinking: a mature, decades-old language suddenly starts behaving like a new platform. That’s what’s happening with Python—except this time the engine isn’t “developer preference” in the abstract. It’s the AI/LLM ecosystem, where Python has become the default operating system for building things that talk, predict, retrieve, and automate.&lt;/p&gt;
&lt;h2 id="the-impossible-growth-curve-why-pythons-jump-looks-different"&gt;The “impossible” growth curve: why Python’s jump looks different&lt;/h2&gt;
&lt;p&gt;A seven-percentage-point leap in a single year is the kind of movement you usually see when something genuinely new arrives—new browsers, new frameworks, new paradigms. Python is neither new nor small. It’s been around for over 30 years, and its core identity is stable: readable syntax, batteries-included standard library, and a vast ecosystem of packages.&lt;/p&gt;</description><content>&lt;p&gt;Every few years, the tech world witnesses a shift that feels almost unfair to legacy thinking: a mature, decades-old language suddenly starts behaving like a new platform. That’s what’s happening with Python—except this time the engine isn’t “developer preference” in the abstract. It’s the AI/LLM ecosystem, where Python has become the default operating system for building things that talk, predict, retrieve, and automate.&lt;/p&gt;
&lt;h2 id="the-impossible-growth-curve-why-pythons-jump-looks-different"&gt;The “impossible” growth curve: why Python’s jump looks different&lt;/h2&gt;
&lt;p&gt;A seven-percentage-point leap in a single year is the kind of movement you usually see when something genuinely new arrives—new browsers, new frameworks, new paradigms. Python is neither new nor small. It’s been around for over 30 years, and its core identity is stable: readable syntax, batteries-included standard library, and a vast ecosystem of packages.&lt;/p&gt;
&lt;p&gt;So when Python’s adoption visibly accelerates, the “why” matters. It’s not because Python suddenly became faster at the language level or because developers collectively changed their minds overnight. It’s because the work developers are being asked to do has changed—dramatically—and Python is the most efficient language for that work.&lt;/p&gt;
&lt;p&gt;Here’s the crux: AI development isn’t a single task. It’s a pipeline of tasks—data prep, experimentation, model training or fine-tuning, evaluation, orchestration, serving, monitoring, and iteration. Python sits at the center of that entire lifecycle, and the ecosystem around it has matured into an assembly line.&lt;/p&gt;
&lt;h2 id="ai-isnt-just-using-pythonits-demanding-python-first-tooling"&gt;AI isn’t just “using Python”—it’s demanding Python-first tooling&lt;/h2&gt;
&lt;p&gt;If you’ve built anything with modern machine learning, you know the pattern: you start with notebooks, you sketch experiments quickly, you iterate on data transformations, and you wire up training loops. Then you transition to APIs so the model can be used by applications. Finally, you productionize: versioning, logging, testing, deployment.&lt;/p&gt;
&lt;p&gt;Python is uniquely positioned because it’s already the glue language for each phase:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Research and training workflows&lt;/strong&gt; tend to be Python-native—think model code, training loops, evaluation scripts, and experimentation tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data tooling&lt;/strong&gt;—cleaning, preprocessing, feature engineering, dataset iteration—often lives in Python because integration is easy and libraries are everywhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serving and integration&lt;/strong&gt;—turning results into endpoints—works smoothly with Python web frameworks and inference libraries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why the AI boom doesn’t just “boost” Python. It reinforces Python as the center of gravity. The more LLM frameworks and ML libraries the community builds, the more Python becomes the path of least resistance for the next project. Ecosystems compound. Python is the compound-interest vehicle.&lt;/p&gt;
&lt;h2 id="fastapi-notebooks-and-pytorch-the-trio-that-made-python-the-default"&gt;FastAPI, notebooks, and PyTorch: the trio that made Python the default&lt;/h2&gt;
&lt;p&gt;Python’s AI advantage isn’t vague. It’s embodied in specific tooling choices that have become standard practice in real teams.&lt;/p&gt;
&lt;h3 id="fastapi-for-apis-from-prototype-to-endpoint-without-a-rewrite"&gt;FastAPI for APIs: from prototype to endpoint without a rewrite&lt;/h3&gt;
&lt;p&gt;Most teams don’t start with production architecture. They start with a prototype. For LLM apps, that prototype often ends up being an API call: “take this prompt, return that response.”&lt;/p&gt;
&lt;p&gt;FastAPI has become a go-to because it fits the workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can validate inputs cleanly.&lt;/li&gt;
&lt;li&gt;You can generate interactive documentation.&lt;/li&gt;
&lt;li&gt;You can ship endpoints with minimal friction.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical progression looks like this: you prototype a model invocation in Python, then wrap it as an HTTP service with FastAPI, then add background tasks (for streaming, retrieval, or longer-running jobs). The language stays the same across the journey, which means the team spends time improving the system—not rewriting it.&lt;/p&gt;
&lt;h3 id="jupyter-notebooks-for-experimentation-the-universal-sandbox"&gt;Jupyter notebooks for experimentation: the universal sandbox&lt;/h3&gt;
&lt;p&gt;Notebooks may be messy, but they’re effective. They’re where ideas become experiments and experiments become systems.&lt;/p&gt;
&lt;p&gt;In LLM development especially, experimentation is relentless:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prompt changes,&lt;/li&gt;
&lt;li&gt;retrieval tweaks,&lt;/li&gt;
&lt;li&gt;chunking strategies,&lt;/li&gt;
&lt;li&gt;tool-calling behaviors,&lt;/li&gt;
&lt;li&gt;evaluation sets and metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notebooks let teams iterate quickly, share results, and explore failure modes. Even when projects later move into structured codebases, notebooks often remain the operational “lab notebook” for ongoing iteration.&lt;/p&gt;
&lt;h3 id="pytorch-for-ai-research-and-development-the-practical-research-engine"&gt;PyTorch for AI research and development: the practical research engine&lt;/h3&gt;
&lt;p&gt;When you want to understand, modify, and train models, you need a framework that supports the entire research loop. PyTorch has become the lingua franca for that work—especially in the AI community—because it provides a flexible, Pythonic way to define and train neural networks.&lt;/p&gt;
&lt;p&gt;Even if your end product is not a training-heavy system, many teams still rely on PyTorch in the pipeline: generating embeddings, running inference, fine-tuning, or validating behaviors. It’s not just “research only.” It’s practical.&lt;/p&gt;
&lt;h2 id="the-real-reason-adoption-spikes-ai-development-is-iterative-and-python-minimizes-friction"&gt;The real reason adoption spikes: AI development is iterative, and Python minimizes friction&lt;/h2&gt;
&lt;p&gt;Developers don’t choose a language because it wins a theoretical contest. They choose it because it reduces time-to-result under constraints. AI projects have unusually tight feedback loops, and Python optimizes for those loops.&lt;/p&gt;
&lt;p&gt;Consider a concrete scenario: building an internal “policy assistant” using retrieval-augmented generation (RAG).&lt;/p&gt;
&lt;p&gt;A realistic workflow might include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ingest documents and normalize them.&lt;/li&gt;
&lt;li&gt;Chunk and embed text.&lt;/li&gt;
&lt;li&gt;Store embeddings in a vector database.&lt;/li&gt;
&lt;li&gt;Retrieve relevant chunks at query time.&lt;/li&gt;
&lt;li&gt;Compose prompts and run the LLM.&lt;/li&gt;
&lt;li&gt;Evaluate answers against a test set.&lt;/li&gt;
&lt;li&gt;Add citations, guardrails, and fallback behaviors.&lt;/li&gt;
&lt;li&gt;Expose the system via an API for the product team.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step benefits from Python’s ecosystem and expressiveness. You can move through the workflow without switching languages or constantly translating concepts across stacks. That matters because AI projects rarely go straight from “idea” to “finished.” They go from “works on my notebook” to “works for real users,” and that transition is where friction kills momentum.&lt;/p&gt;
&lt;p&gt;Python’s advantage is that it keeps you in the same environment while you iterate.&lt;/p&gt;
&lt;h2 id="practical-advice-if-youre-choosing-a-stack-optimize-for-the-ai-lifecycle"&gt;Practical advice: if you’re choosing a stack, optimize for the AI lifecycle&lt;/h2&gt;
&lt;p&gt;The right move isn’t “learn Python because everyone else does.” It’s to choose an approach that matches the AI lifecycle you’ll actually run.&lt;/p&gt;
&lt;p&gt;Here’s how to think about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;If your work involves experimentation, evaluation, and rapid iteration&lt;/strong&gt;, Python is the most efficient starting point. Notebooks, rapid prototyping, and model tinkering are where time is won.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you’re building model-backed services&lt;/strong&gt;, pair Python with a framework like FastAPI so the path from experimental code to maintainable APIs is straightforward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you’re doing training or fine-tuning&lt;/strong&gt;, expect Python-first tooling (PyTorch-style ecosystems) to remain central.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for production from day one&lt;/strong&gt;, even if you prototype quickly. Structure code early, treat notebooks as experiments, and move critical logic into tested modules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yes—use the right boundaries. One common failure mode is “notebook sprawl,” where the prototype becomes the product without guardrails. A mature workflow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep notebooks for exploration and experiments.&lt;/li&gt;
&lt;li&gt;Put production logic into a package/module.&lt;/li&gt;
&lt;li&gt;Add automated tests for core behaviors (preprocessing, prompt formatting, retrieval routing).&lt;/li&gt;
&lt;li&gt;Monitor model outputs and latency like any other system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal isn’t to worship Python—it’s to use Python to accelerate the parts that benefit from acceleration, without surrendering engineering discipline.&lt;/p&gt;
&lt;h2 id="what-this-means-for-the-language-wars-narrative"&gt;What this means for the “language wars” narrative&lt;/h2&gt;
&lt;p&gt;When Python’s market share rises this sharply, it doesn’t invalidate other languages. JavaScript still dominates the web surface area. Java and C# still power enterprise backends. Go and Rust show their strengths in systems and reliability-focused contexts.&lt;/p&gt;
&lt;p&gt;But AI changes the center of gravity. The “war” isn’t about which language is best at everything. It’s about which language sits at the intersection of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rapidly evolving libraries,&lt;/li&gt;
&lt;li&gt;widely shared examples and patterns,&lt;/li&gt;
&lt;li&gt;tooling that supports the full development loop,&lt;/li&gt;
&lt;li&gt;and teams that need results quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Python is winning that intersection because it behaves like an ecosystem, not just a language.&lt;/p&gt;
&lt;h2 id="conclusion-python-isnt-growing-despite-aiits-growing-because-ai-speaks-python"&gt;Conclusion: Python isn’t growing despite AI—it’s growing because AI speaks Python&lt;/h2&gt;
&lt;p&gt;Python’s seven-point jump isn’t a mystery if you look at what developers are building right now. LLMs and AI frameworks didn’t just add new jobs; they created a new development lifecycle—iterative, pipeline-driven, experiment-heavy, and ecosystem-dependent. Python is the native tongue of that lifecycle.&lt;/p&gt;
&lt;p&gt;If AI is the new default workload, then Python isn’t merely benefiting. It’s becoming the fastest route from idea to working product, and that kind of advantage compounds.&lt;/p&gt;</content></item><item><title>The MCP Ecosystem Exploded and We're Just Getting Started</title><link>https://decastro.work/blog/mcp-ecosystem-exploded-just-getting-started/</link><pubDate>Sat, 01 Mar 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/mcp-ecosystem-exploded-just-getting-started/</guid><description>&lt;p&gt;In under four months, the Model Context Protocol (MCP) went from an announcement to an integration ecosystem phenomenon. And the reason isn’t hype—it’s engineering. MCP is the rare kind of protocol that feels inevitable: simple enough that an individual developer can ship something useful in an afternoon, yet structured enough to scale into a real platform.&lt;/p&gt;
&lt;p&gt;What we’re watching is the TCP/IP moment for AI tool integration: once a common “wiring” standard exists, the ecosystem doesn’t need permission to grow.&lt;/p&gt;</description><content>&lt;p&gt;In under four months, the Model Context Protocol (MCP) went from an announcement to an integration ecosystem phenomenon. And the reason isn’t hype—it’s engineering. MCP is the rare kind of protocol that feels inevitable: simple enough that an individual developer can ship something useful in an afternoon, yet structured enough to scale into a real platform.&lt;/p&gt;
&lt;p&gt;What we’re watching is the TCP/IP moment for AI tool integration: once a common “wiring” standard exists, the ecosystem doesn’t need permission to grow.&lt;/p&gt;
&lt;h2 id="why-mcp-took-off-so-fast"&gt;Why MCP Took Off So Fast&lt;/h2&gt;
&lt;p&gt;Most integration efforts fail because the protocol is either too abstract to implement or too opinionated to reuse. MCP hit a sweet spot. It’s fundamentally just a small set of JSON-RPC methods over stdio, paired with a predictable process model. That choice matters.&lt;/p&gt;
&lt;p&gt;When integration is “one-way glue,” it’s fragile and expensive to maintain. When it’s a “real protocol,” it becomes a product primitive. MCP turned “tool access” into something you can implement once and expose broadly.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A tool provider (say, “PostgreSQL”) can expose a narrow, well-defined capability—querying, schema inspection, or metadata—without asking every client to learn its internals.&lt;/li&gt;
&lt;li&gt;A client (an AI application) can discover what’s available and call it using a consistent interface.&lt;/li&gt;
&lt;li&gt;A developer can write a server without standing up a complex web service, onboarding flow, auth system, or bespoke SDK every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The speed came from eliminating the parts that usually slow ecosystems down: bespoke protocols, custom client code, and long feedback loops.&lt;/p&gt;
&lt;h2 id="few-methods-over-stdio-is-the-secret-weapon"&gt;“Few Methods Over Stdio” Is the Secret Weapon&lt;/h2&gt;
&lt;p&gt;Let’s be concrete. The core experience of MCP for builders is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run a local process (the MCP server).&lt;/li&gt;
&lt;li&gt;Speak MCP over a standard stream.&lt;/li&gt;
&lt;li&gt;Use a tiny RPC surface to list tools and execute them.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That design compresses the path from idea to working integration. No load balancers. No gateway rewrites. No “works on my machine” service deployment drama. You can prototype quickly, then harden later.&lt;/p&gt;
&lt;p&gt;Consider a developer building an integration for a collaboration tool like Slack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The developer can implement “list channels,” “search messages,” or “post a message” as MCP tools.&lt;/li&gt;
&lt;li&gt;The AI client can call those tools with consistent parameters and get structured results back.&lt;/li&gt;
&lt;li&gt;From there, you can iterate: add richer query options, add better formatting, and tighten permission handling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compare that to typical integration patterns where every new model client requires a separate connector. MCP flips the equation: integrations become reusable “adapters” that any MCP-capable client can consume.&lt;/p&gt;
&lt;p&gt;And that’s the broader principle: protocols win when they make reuse the default.&lt;/p&gt;
&lt;h2 id="the-explosion-300-servers-and-a-copy-paste-ecosystem"&gt;The Explosion: 300+ Servers and a Copy-Paste Ecosystem&lt;/h2&gt;
&lt;p&gt;Once a protocol becomes implementable, the ecosystem fills in rapidly. We’ve already seen servers for mainstream tools—GitHub, Slack, PostgreSQL, Jira, Linear, Notion—and hundreds beyond them. But the most important detail isn’t just the number; it’s the distribution of effort.&lt;/p&gt;
&lt;p&gt;A typical outcome looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One developer builds an MCP server for a tool.&lt;/li&gt;
&lt;li&gt;Another developer extends it (better filtering, pagination, richer schema context).&lt;/li&gt;
&lt;li&gt;A third developer adapts it for a specific organization’s workflow (custom projects, naming conventions, internal policies).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because MCP servers are small, the ecosystem supports branching and specialization without requiring a full platform rewrite. The protocol becomes the stable center, and innovation happens at the edges.&lt;/p&gt;
&lt;p&gt;This is also why the “TCP/IP moment” analogy holds. When networking standards spread, they don’t just add more devices—they unlock new categories of application. MCP doesn’t just connect AI to tools; it makes tool connectivity composable.&lt;/p&gt;
&lt;h2 id="how-to-think-about-mcp-tools-context-and-safety"&gt;How to Think About MCP: Tools, Context, and Safety&lt;/h2&gt;
&lt;p&gt;It’s tempting to treat MCP as “just connectors.” But if you want real production value, you have to think in terms of capabilities and context management.&lt;/p&gt;
&lt;h3 id="tools-are-contracts-not-scripts"&gt;Tools are contracts, not scripts&lt;/h3&gt;
&lt;p&gt;A good MCP server exposes tools that are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Purposeful&lt;/strong&gt;: narrowly scoped operations (e.g., “search issues by JQL,” not “do whatever you want”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Typed&lt;/strong&gt;: clear input schemas that reduce ambiguity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt;: structured outputs that your client can reliably interpret.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="context-isnt-an-afterthought"&gt;Context isn’t an afterthought&lt;/h3&gt;
&lt;p&gt;AI clients need context to be useful. With MCP, servers can provide context via tools like “get schema” or “list resources,” but the real win is reducing the client’s guesswork.&lt;/p&gt;
&lt;p&gt;For example, with a database integration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of relying on the model to remember table names, the client can call a “schema inspection” tool.&lt;/li&gt;
&lt;li&gt;The model can then generate queries grounded in actual fields and relationships.&lt;/li&gt;
&lt;li&gt;The output can return query results in a format designed for follow-on reasoning.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="safety-must-be-designed-into-tool-boundaries"&gt;Safety must be designed into tool boundaries&lt;/h3&gt;
&lt;p&gt;The simplest tool to implement is often the most dangerous to expose. If your server includes an “execute arbitrary SQL” tool, you’ve effectively created a remote execution interface.&lt;/p&gt;
&lt;p&gt;A safer pattern is to expose constrained operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read-only querying tools for exploratory workflows.&lt;/li&gt;
&lt;li&gt;Parameterized actions that limit scope (e.g., only within a specific schema or allowlist).&lt;/li&gt;
&lt;li&gt;Explicit “confirm” steps for mutations, where applicable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need fancy research to justify this. You just need to respect that MCP tools are callable by an AI system—which means you should minimize the blast radius.&lt;/p&gt;
&lt;h2 id="building-an-mcp-server-in-an-afternoon-then-fixing-what-matters"&gt;Building an MCP Server in an Afternoon (Then Fixing What Matters)&lt;/h2&gt;
&lt;p&gt;The headline is true: implementing an MCP server can be fast. But the difference between a demo and a dependable integration is usually not protocol complexity—it’s product discipline.&lt;/p&gt;
&lt;p&gt;If you’re building (or evaluating) an MCP server, here’s a practical checklist:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with one or two tools&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Example: for GitHub, begin with “search repositories” and “create an issue.”&lt;/li&gt;
&lt;li&gt;Don’t start by trying to mirror the entire API surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Define strict input schemas&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer explicit fields over free-form text.&lt;/li&gt;
&lt;li&gt;Make pagination and limits first-class parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Return structured outputs&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide consistent keys and predictable formats.&lt;/li&gt;
&lt;li&gt;Include identifiers (IDs, URLs) so the client can chain actions safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Design for failure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Network issues, auth expiry, missing permissions—build reasonable error messages.&lt;/li&gt;
&lt;li&gt;Your AI client will surface these errors to users, so clarity matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat auth like a capability boundary&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure tokens are stored securely and never echoed to the model.&lt;/li&gt;
&lt;li&gt;If your integration needs user permissions, build the workflow explicitly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add guardrails for write operations&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mutations should be deliberate: scoped, confirmable, and logged.&lt;/li&gt;
&lt;li&gt;If your tool can change state, you should make it hard to do so accidentally.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do just these, your server will feel “real,” not “prototype-y,” even if it started as one.&lt;/p&gt;
&lt;h2 id="where-mcp-goes-next-from-adapters-to-infrastructure"&gt;Where MCP Goes Next: From Adapters to Infrastructure&lt;/h2&gt;
&lt;p&gt;The most interesting part of this story isn’t that MCP servers exist—it’s that they behave like infrastructure. Once you can standardize tool discovery and invocation, you can build higher-level systems on top:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multitool workflows&lt;/strong&gt; that coordinate GitHub + Jira + Linear without bespoke glue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment-aware automation&lt;/strong&gt; where an agent inspects the workspace, then decides which tools to call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable internal platforms&lt;/strong&gt;: teams can standardize on MCP servers for their data sources and business systems, making AI features portable across clients.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And because MCP is lightweight, teams can keep local control. Instead of routing everything through a heavyweight service layer, you can run servers where the credentials and policies already live.&lt;/p&gt;
&lt;p&gt;This is why the ecosystem is compounding. Each new server reduces the incremental cost of adding another capability. Each integration becomes a building block. And because the protocol is stable, you don’t have to rewrite your wiring every time a new client appears.&lt;/p&gt;
&lt;h2 id="conclusion-the-protocol-is-the-product"&gt;Conclusion: The Protocol Is the Product&lt;/h2&gt;
&lt;p&gt;MCP didn’t just spark a wave of integrations—it changed the economics of building them. A tiny JSON-RPC interface over stdio turned “AI tool support” into something developers can ship quickly, reuse broadly, and improve continuously.&lt;/p&gt;
&lt;p&gt;We’re not at the end of the story. We’re at the beginning of a new kind of tooling ecosystem—one where the plumbing is standardized and innovation happens at the capability layer. The moment protocols become boring, ecosystems get exciting.&lt;/p&gt;</content></item><item><title>SQLite Is Having a Moment and It's Not Just for Mobile Anymore</title><link>https://decastro.work/blog/sqlite-having-moment-not-just-mobile/</link><pubDate>Sun, 23 Feb 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/sqlite-having-moment-not-just-mobile/</guid><description>&lt;p&gt;For years, SQLite lived in the shadows: the reliable little workhorse quietly powering apps on a phone, a laptop, or an offline-first prototype. But the ground has shifted. Today, teams are turning SQLite into a distributed database platform—by adding replication, global distribution, and continuous backups around it. The result is a pragmatic architecture where your users don’t just get faster software—they get faster data.&lt;/p&gt;
&lt;h2 id="why-sqlite-suddenly-feels-distributed"&gt;Why SQLite Suddenly Feels “Distributed”&lt;/h2&gt;
&lt;p&gt;SQLite was never meant to be a cluster database. It’s a single-file engine designed to be embedded, deterministic, and easy to move. That simplicity used to be a liability when you needed horizontal scaling or HA.&lt;/p&gt;</description><content>&lt;p&gt;For years, SQLite lived in the shadows: the reliable little workhorse quietly powering apps on a phone, a laptop, or an offline-first prototype. But the ground has shifted. Today, teams are turning SQLite into a distributed database platform—by adding replication, global distribution, and continuous backups around it. The result is a pragmatic architecture where your users don’t just get faster software—they get faster data.&lt;/p&gt;
&lt;h2 id="why-sqlite-suddenly-feels-distributed"&gt;Why SQLite Suddenly Feels “Distributed”&lt;/h2&gt;
&lt;p&gt;SQLite was never meant to be a cluster database. It’s a single-file engine designed to be embedded, deterministic, and easy to move. That simplicity used to be a liability when you needed horizontal scaling or HA.&lt;/p&gt;
&lt;p&gt;The renaissance comes from a new pattern: &lt;strong&gt;don’t replace SQLite—wrap it with distribution&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of asking SQLite to do everything, modern systems treat it as the &lt;em&gt;core data engine&lt;/em&gt; and add:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global placement&lt;/strong&gt; (so reads and writes start near users)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replication&lt;/strong&gt; (so failures don’t equal downtime)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change capture + streaming backups&lt;/strong&gt; (so recovery doesn’t require heroic restores)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This changes how you build. You stop treating “database” as a remote service you must tolerate and start treating it as a local capability your application can rely on—then you distribute that capability where it matters.&lt;/p&gt;
&lt;p&gt;Think of it as local-first, but with production-grade guardrails.&lt;/p&gt;
&lt;h2 id="turso-shipping-sqlite-to-the-edge"&gt;Turso: Shipping SQLite to the Edge&lt;/h2&gt;
&lt;p&gt;Turso’s big idea is straightforward: &lt;strong&gt;make SQLite feel like a globally distributed database&lt;/strong&gt; without making you learn a whole new database paradigm. The implementation details vary, but the architecture goal is consistent: keep SQLite close to your users by running it in edge locations and coordinating access so your app can use it as if it were local.&lt;/p&gt;
&lt;p&gt;What does that unlock?&lt;/p&gt;
&lt;h3 id="example-a-read-heavy-app-that-shouldnt-pay-the-latency-tax"&gt;Example: A read-heavy app that shouldn’t pay the latency tax&lt;/h3&gt;
&lt;p&gt;Imagine a web app for product analytics where most requests are “give me aggregated metrics” and only a smaller portion are writes. If your data lives in a distant region, every page load quietly pays a latency toll.&lt;/p&gt;
&lt;p&gt;With an edge-distributed SQLite approach, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reduce time-to-first-byte for read queries&lt;/li&gt;
&lt;li&gt;Keep interactive browsing snappy&lt;/li&gt;
&lt;li&gt;Still support updates without forcing your whole system into a centralized OLTP design&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice-choose-your-consistency-expectations-deliberately"&gt;Practical advice: Choose your consistency expectations deliberately&lt;/h3&gt;
&lt;p&gt;Distributed systems are mostly about trade-offs. If your workload is read-heavy, you can often tolerate slightly fresher data than strict transactional semantics. The right move is to define what your users should experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does “freshness” mean seconds, minutes, or milliseconds?&lt;/li&gt;
&lt;li&gt;Are stale reads acceptable for non-critical views?&lt;/li&gt;
&lt;li&gt;Can you design writes to be idempotent so retries are safe?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Turso-style distribution shines when you shape the app around these realities instead of fighting them.&lt;/p&gt;
&lt;h2 id="litefs-replicate-sqlite-across-nodes-for-read-scaling"&gt;LiteFS: Replicate SQLite Across Nodes for Read Scaling&lt;/h2&gt;
&lt;p&gt;Turso gets you global proximity; LiteFS targets a different pain: &lt;strong&gt;scaling reads by replicating the database across multiple nodes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;SQLite is great at local performance. The trick is getting those benefits when multiple machines need access. LiteFS replicates changes so each node can run queries locally rather than funneling everything through a single database instance.&lt;/p&gt;
&lt;h3 id="example-a-multi-node-backend-for-the-same-app-data"&gt;Example: A multi-node backend for the “same” app data&lt;/h3&gt;
&lt;p&gt;Consider a service where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write traffic is moderate (events, orders, user actions)&lt;/li&gt;
&lt;li&gt;Read traffic is heavy (dashboards, recommendations, query-driven UIs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of having every node hit a central database over the network, you replicate the SQLite database. Now each node can run reads locally—faster, cheaper, and less fragile under network jitter.&lt;/p&gt;
&lt;h3 id="practical-advice-design-for-conflict-boundaries"&gt;Practical advice: Design for conflict boundaries&lt;/h3&gt;
&lt;p&gt;Replication systems usually have a “happy path” model: one primary writer, then fan out to replicas. That doesn’t mean you’re trapped in a single-writer design, but it does mean you should think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where writes originate&lt;/li&gt;
&lt;li&gt;How conflicts are avoided or resolved&lt;/li&gt;
&lt;li&gt;What happens during failover&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your app can centralize writes (or route them deterministically), you’ll get a lot of the simplicity that makes SQLite attractive in the first place.&lt;/p&gt;
&lt;h2 id="litestream-continuous-streaming-backups-you-can-actually-trust"&gt;Litestream: Continuous Streaming Backups You Can Actually Trust&lt;/h2&gt;
&lt;p&gt;Replication helps with uptime. But you still need backups—real ones. Litestream addresses the uncomfortable truth that backups are often an afterthought until a failure forces a restore you didn’t test.&lt;/p&gt;
&lt;p&gt;Litestream provides &lt;strong&gt;continuous streaming backups&lt;/strong&gt; of SQLite changes to durable storage like S3. The key value is not just “backups exist,” but that they exist &lt;em&gt;continuously&lt;/em&gt; and can be restored &lt;em&gt;incrementally&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="example-the-difference-between-well-restore-later-and-we-can-recover"&gt;Example: The difference between “we’ll restore later” and “we can recover”&lt;/h3&gt;
&lt;p&gt;Picture a production issue:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A deployment bug corrupts a database table&lt;/li&gt;
&lt;li&gt;You discover it quickly, but restoring from an overnight backup means losing hours of data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With continuous streaming backups, recovery becomes a routine operational move rather than a stressful forensic exercise. You restore to a recent point in time close to when the corruption occurred.&lt;/p&gt;
&lt;h3 id="practical-advice-treat-restore-as-a-first-class-feature"&gt;Practical advice: Treat restore as a first-class feature&lt;/h3&gt;
&lt;p&gt;Backups aren’t useful unless your team can restore them. Make it part of your process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automate restore into staging&lt;/li&gt;
&lt;li&gt;Run tabletop exercises (“What if the database is corrupted?”)&lt;/li&gt;
&lt;li&gt;Document the runbook so it doesn’t live only in someone’s head&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building with SQLite in a distributed-ish architecture, backups become the safety net that lets you move fast.&lt;/p&gt;
&lt;h2 id="putting-it-together-local-first-data-global-first-performance"&gt;Putting It Together: Local-First Data, Global-First Performance&lt;/h2&gt;
&lt;p&gt;The most compelling outcome isn’t any single tool—it’s the pattern they enable.&lt;/p&gt;
&lt;p&gt;By combining edge distribution (Turso), node replication (LiteFS), and continuous streaming backups (Litestream), you can build systems where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data is near users&lt;/strong&gt; (edge-first reads)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute scales horizontally&lt;/strong&gt; (each node can serve locally)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failures are survivable&lt;/strong&gt; (replication + durable backups)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recovery is predictable&lt;/strong&gt; (streaming backup trails)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="example-architecture-fast-dashboards-with-safe-writes"&gt;Example architecture: “Fast dashboards with safe writes”&lt;/h3&gt;
&lt;p&gt;A practical blueprint for many teams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clients read from a nearby SQLite-backed node&lt;/strong&gt;&lt;br&gt;
Dashboards feel instant because queries run locally to that edge region.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Writes flow to a primary (or a controlled writer path)&lt;/strong&gt;&lt;br&gt;
You avoid the chaos of multi-writer conflicts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Replicas receive updates&lt;/strong&gt;&lt;br&gt;
Each node updates its local SQLite copy, so reads remain fast.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Every change is continuously backed up&lt;/strong&gt;&lt;br&gt;
Litestream streams snapshots/segments to S3 (or another durable store), enabling quick, point-in-time restores.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Even if you don’t fully adopt all three components, you can borrow the design mindset: performance through locality, reliability through replication and streaming backups.&lt;/p&gt;
&lt;h2 id="when-sqlite-and-this-pattern-is-a-great-fit"&gt;When SQLite (and This Pattern) Is a Great Fit&lt;/h2&gt;
&lt;p&gt;SQLite isn’t “better than” every database. But it’s a strong fit when your product needs certain qualities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-heavy workloads&lt;/strong&gt;: dashboards, feeds, query-driven UI, analytics views&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded analytics&lt;/strong&gt;: compute aggregates close to where data lives&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local-first architectures&lt;/strong&gt;: offline capability, fast local writes, resilient sync&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moderate write throughput with real-time expectations&lt;/strong&gt;: events and state updates with immediate visibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your workload is dominated by complex joins across huge datasets, or you need heavyweight distributed transactions, you may still prefer other systems. But for many modern applications, you don’t need a monster—you need a fast, reliable core with sane operational properties.&lt;/p&gt;
&lt;h3 id="practical-advice-start-by-choosing-your-data-gravity"&gt;Practical advice: Start by choosing your “data gravity”&lt;/h3&gt;
&lt;p&gt;Ask: where should data live to make the app feel fast?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If users are global: edge distribution matters.&lt;/li&gt;
&lt;li&gt;If traffic is spiky and read-heavy: replication matters.&lt;/li&gt;
&lt;li&gt;If your biggest fear is data loss: streaming backups matter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pick one and build outward. The tools are modular. The architecture pattern is what sticks.&lt;/p&gt;
&lt;h2 id="conclusion-sqlite-is-becoming-an-architecture-not-a-tool"&gt;Conclusion: SQLite Is Becoming an Architecture, Not a Tool&lt;/h2&gt;
&lt;p&gt;SQLite’s moment isn’t nostalgia for “the little database.” It’s a serious rethinking of how data can be shipped, replicated, and protected without drowning teams in complexity.&lt;/p&gt;
&lt;p&gt;Turso brings SQLite to the edge, LiteFS scales reads by replicating across nodes, and Litestream makes continuous backups practical. Together, they unlock a modern promise: code and data close to users, with operational confidence.&lt;/p&gt;
&lt;p&gt;If you’ve been waiting for SQLite to become “enterprise enough,” this is the breakthrough. The secret weapon isn’t the engine—it’s the ecosystem and the architecture you build around it.&lt;/p&gt;</content></item><item><title>Docker's +17 Point Surge Is the Most Underreported Story in Dev</title><link>https://decastro.work/blog/docker-17-point-surge-underreported-story/</link><pubDate>Tue, 11 Feb 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/docker-17-point-surge-underreported-story/</guid><description>&lt;p&gt;Every few months, the ecosystem rediscovers the “next big thing” and spends weeks arguing about it. Meanwhile, Docker has been quietly doing something far more consequential: turning from “useful tool” into “default foundation.” In one of the largest single-year adoption jumps ever seen in the Stack Overflow survey data, Docker reportedly climbed by &lt;strong&gt;+17 percentage points&lt;/strong&gt;. That’s not a marketing win. That’s a phase transition—and it’s undercovered because nobody’s excited by infrastructure becoming boring.&lt;/p&gt;</description><content>&lt;p&gt;Every few months, the ecosystem rediscovers the “next big thing” and spends weeks arguing about it. Meanwhile, Docker has been quietly doing something far more consequential: turning from “useful tool” into “default foundation.” In one of the largest single-year adoption jumps ever seen in the Stack Overflow survey data, Docker reportedly climbed by &lt;strong&gt;+17 percentage points&lt;/strong&gt;. That’s not a marketing win. That’s a phase transition—and it’s undercovered because nobody’s excited by infrastructure becoming boring.&lt;/p&gt;
&lt;h2 id="what-the-17-really-means-and-why-it-matters"&gt;What the +17 Really Means (And Why It Matters)&lt;/h2&gt;
&lt;p&gt;Adoption graphs don’t move like that unless something changes at the workflow level. A +17 point swing in usage is the kind of step-change that usually follows an ecosystem shift: a platform matures, tooling stabilizes, and the “old way” stops being the path of least resistance.&lt;/p&gt;
&lt;p&gt;Think about what Docker represents in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A single command to run a service anywhere.&lt;/li&gt;
&lt;li&gt;A repeatable filesystem + runtime environment.&lt;/li&gt;
&lt;li&gt;Dependency boundaries you can reason about, review, and version.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building software, reproducibility isn’t a nice-to-have—it’s how teams stop burning time on “it works on my machine.” When a tool becomes the shortest route to reliable builds and deployments, adoption stops being optional. It becomes muscle memory.&lt;/p&gt;
&lt;p&gt;That’s the leap Docker is making: from “popular tool” to “assumed infrastructure,” similar to what Git became years ago. Once something is the baseline, discussions migrate elsewhere. People stop saying “I use Git,” and they just start shipping commits.&lt;/p&gt;
&lt;h2 id="docker-was-already-winning-the-dev-loopai-just-gave-it-a-runway"&gt;Docker Was Already Winning the Dev Loop—AI Just Gave It a Runway&lt;/h2&gt;
&lt;p&gt;It’s tempting to credit Docker’s growth to cloud adoption, microservices, or general container hype. Those factors matter, but the real accelerant is unmistakable: &lt;strong&gt;AI development&lt;/strong&gt;, especially the LLM-centric workflow that’s now everywhere.&lt;/p&gt;
&lt;p&gt;Here’s the uncomfortable truth for modern teams: LLM app development is dependency-heavy and environment-sensitive.&lt;/p&gt;
&lt;p&gt;A typical stack today looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Python runtime with the right CUDA/CPU behavior&lt;/li&gt;
&lt;li&gt;A specific version of PyTorch (or TensorFlow, JAX, or whatever your stack uses)&lt;/li&gt;
&lt;li&gt;Tokenizers and model-serving libraries with their own quirks&lt;/li&gt;
&lt;li&gt;System packages (often OS-level libraries) that “shouldn’t matter” until they do&lt;/li&gt;
&lt;li&gt;Feature flags and config tied to dataset preprocessing and model formatting&lt;/li&gt;
&lt;li&gt;GPU vs CPU variations across dev machines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, it’s not just “code.” It’s a fragile bundle of assumptions.&lt;/p&gt;
&lt;p&gt;Docker turns that bundle into an artifact you can share with confidence. When your team says, “Run it with Docker,” you’ve eliminated the most expensive failure mode in software: silent mismatches.&lt;/p&gt;
&lt;h2 id="the-llm-reproducibility-problem-docker-solves"&gt;The LLM Reproducibility Problem Docker Solves&lt;/h2&gt;
&lt;p&gt;Let’s make it concrete. Suppose you’re building a Retrieval-Augmented Generation (RAG) service. You might have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An embedding pipeline that depends on a specific model version and tokenizer behavior&lt;/li&gt;
&lt;li&gt;A vector database with a particular schema expectation&lt;/li&gt;
&lt;li&gt;An indexing job that must use identical preprocessing steps&lt;/li&gt;
&lt;li&gt;An API server that loads models and runs retrieval consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now add the real-world scenario: one developer uses Python 3.11 locally, another uses 3.10, and a third is on an older Linux image. Suddenly, you don’t get a clean error—you get subtle differences. Embeddings shift. Retrieval quality drifts. Tests “almost pass.” Debugging becomes a detective story instead of engineering.&lt;/p&gt;
&lt;p&gt;Docker gives you the one thing teams crave in fast-moving AI projects: &lt;strong&gt;environment determinism&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of debating whose laptop is “correct,” you codify the runtime:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pin base images.&lt;/li&gt;
&lt;li&gt;Pin dependency versions.&lt;/li&gt;
&lt;li&gt;Encode system dependencies explicitly.&lt;/li&gt;
&lt;li&gt;Make GPU requirements explicit instead of implicit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when the underlying model stack changes weekly, the container can be rebuilt with the new pins—and your team knows they’re running the same reality.&lt;/p&gt;
&lt;h2 id="from-container-trend-to-oxygen-why-teams-stop-questioning-it"&gt;From “Container Trend” to Oxygen: Why Teams Stop Questioning It&lt;/h2&gt;
&lt;p&gt;Containerization doesn’t become “oxygen” because people love containers. It becomes oxygen because the alternative costs too much.&lt;/p&gt;
&lt;p&gt;The cost shows up as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;onboarding time that drags for days&lt;/li&gt;
&lt;li&gt;PR review churn caused by environment failures&lt;/li&gt;
&lt;li&gt;flaky tests that pass locally and fail in CI (or worse, fail everywhere)&lt;/li&gt;
&lt;li&gt;production “it behaved differently here” incidents&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker doesn’t remove engineering complexity—but it corrals complexity into a predictable boundary. That boundary is powerful in AI development because your stack is moving quickly and your experiments are numerous. You need to iterate without the “reset the machine” tax.&lt;/p&gt;
&lt;p&gt;There’s also a cultural shift happening. Teams now expect that an AI demo, a training pipeline, or an evaluation harness comes with a runnable environment. If it doesn’t, people assume it will be painful—and they may just not try it.&lt;/p&gt;
&lt;p&gt;That expectation is exactly what the adoption surge signals. The tool stops being a topic and becomes a default behavior.&lt;/p&gt;
&lt;h2 id="practical-guidance-how-to-make-docker-serve-ai-not-just-run-code"&gt;Practical Guidance: How to Make Docker Serve AI (Not Just “Run Code”)&lt;/h2&gt;
&lt;p&gt;If Docker adoption is soaring, the winners won’t be the teams that “use Docker,” full stop. They’ll be the teams that structure their Docker usage like a product feature. Here’s what that looks like in real projects:&lt;/p&gt;
&lt;h3 id="1-treat-your-container-as-an-interface"&gt;1) Treat your container as an interface&lt;/h3&gt;
&lt;p&gt;Your container image is the contract between your code and the runtime. When you publish it (internally or externally), include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clear tags (e.g., &lt;code&gt;rag-eval:0.3.2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;documented build instructions&lt;/li&gt;
&lt;li&gt;a single entrypoint command for common tasks (train, index, serve)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your onboarding instructions say “run &lt;code&gt;pip install -r requirements.txt&lt;/code&gt; and hope,” you’re not getting the real benefit.&lt;/p&gt;
&lt;h3 id="2-pin-versions-aggressivelyespecially-for-ai-stacks"&gt;2) Pin versions aggressively—especially for AI stacks&lt;/h3&gt;
&lt;p&gt;For LLM apps, pinning isn’t about purity. It’s about avoiding shifting behavior. Pin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python and OS base image&lt;/li&gt;
&lt;li&gt;key ML frameworks&lt;/li&gt;
&lt;li&gt;tokenizers and preprocessing libraries&lt;/li&gt;
&lt;li&gt;evaluation dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yes, it requires maintenance. But maintenance is cheaper than debugging.&lt;/p&gt;
&lt;h3 id="3-separate-dev-containers-from-production-images"&gt;3) Separate dev containers from production images&lt;/h3&gt;
&lt;p&gt;A common failure mode is using the same container for everything. Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dev image: faster iteration, tooling installed&lt;/li&gt;
&lt;li&gt;prod image: minimal runtime, security posture, deterministic dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps build times and attack surface reasonable while preserving reproducibility.&lt;/p&gt;
&lt;h3 id="4-make-gpu-needs-explicit"&gt;4) Make GPU needs explicit&lt;/h3&gt;
&lt;p&gt;If your app needs CUDA or specific GPU behavior, encode it in the container strategy rather than relying on developer luck. The worst experience is a “works on my GPU” workflow that collapses for everyone else.&lt;/p&gt;
&lt;h3 id="5-dont-hide-complexity-behind-magic"&gt;5) Don’t hide complexity behind magic&lt;/h3&gt;
&lt;p&gt;It’s tempting to wrap Docker in scripts until nobody can tell what actually runs. Resist that. Keep Dockerfiles readable and the build process understandable. Your future self will thank you.&lt;/p&gt;
&lt;h2 id="why-this-story-is-underreportedand-why-its-still-the-one-to-watch"&gt;Why This Story Is Underreported—and Why It’s Still the One to Watch&lt;/h2&gt;
&lt;p&gt;AI gets attention because it’s novel and visible: demos, benchmarks, flashy tools. Docker’s story is more subtle because it’s the scaffolding. The adoption jump is a sign that teams are shifting from “experiment mode” to “build mode.”&lt;/p&gt;
&lt;p&gt;When Docker becomes assumed infrastructure, it means teams are standardizing. Standardization is what unlocks collaboration, automation, scaling, and velocity. It’s the quiet enabler behind many AI engineering practices people treat as “new.”&lt;/p&gt;
&lt;p&gt;So the underreported angle isn’t that Docker is growing. It’s that Docker is becoming the default. And defaults are where ecosystems converge.&lt;/p&gt;
&lt;p&gt;If you’re building anything LLM-related—and especially if you care about shipping reliably—Docker shouldn’t be an afterthought. It’s how you turn brittle experimentation into durable systems.&lt;/p&gt;
&lt;h2 id="conclusion-docker-is-becoming-the-baseline-for-reliable-ai-engineering"&gt;Conclusion: Docker Is Becoming the Baseline for Reliable AI Engineering&lt;/h2&gt;
&lt;p&gt;A +17 percentage point adoption jump isn’t just momentum; it’s an ecosystem reorientation. Docker is moving into the same role Git occupies: the tool people stop mentioning because it’s simply how the work gets done. AI development, with its dependency sensitivity and reproducibility demands, is the perfect catalyst. In the rush to chase models, Docker is already delivering the foundation teams need to build, test, and iterate without chaos.&lt;/p&gt;</content></item><item><title>Cursor Changed My Mind About AI-Native Editors</title><link>https://decastro.work/blog/cursor-changed-mind-ai-native-editors/</link><pubDate>Tue, 04 Feb 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/cursor-changed-mind-ai-native-editors/</guid><description>&lt;p&gt;I’ve been around enough “AI features” to distrust the hype. For a while, I treated AI in editors like a garnish: helpful, occasionally brilliant, but never fundamental. Then I spent a week in Cursor, and I had to admit something uncomfortable—my mental model was wrong. AI-native editors aren’t just VS Code with smarter plugins. They’re a different class of tool, and the difference shows up the moment you stop asking it to “help you type” and start asking it to understand and change your codebase.&lt;/p&gt;</description><content>&lt;p&gt;I’ve been around enough “AI features” to distrust the hype. For a while, I treated AI in editors like a garnish: helpful, occasionally brilliant, but never fundamental. Then I spent a week in Cursor, and I had to admit something uncomfortable—my mental model was wrong. AI-native editors aren’t just VS Code with smarter plugins. They’re a different class of tool, and the difference shows up the moment you stop asking it to “help you type” and start asking it to understand and change your codebase.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-ai-as-a-plugin"&gt;The real problem with “AI as a plugin”&lt;/h2&gt;
&lt;p&gt;Most AI experiences inside editors follow the same pattern: you trigger a chat or command, the tool generates some text, and you manually splice it into your project. Even when the integration is excellent, the architecture tends to be additive—AI bolted on after the editor’s core assumptions are already set.&lt;/p&gt;
&lt;p&gt;That matters because coding isn’t just writing lines. It’s maintaining invariants across files, refactoring across boundaries, obeying conventions, and avoiding silent regressions. A plugin-style AI assistant can be clever at answering questions, but it struggles to operate like a reliable collaborator when the unit of work is bigger than a snippet.&lt;/p&gt;
&lt;p&gt;Here’s the moment I usually notice the limitation: you ask for a refactor, the assistant proposes changes, and suddenly you’re doing reconciliation. Which file should change? Did it update all call sites? Did it preserve formatting? Did it accidentally break an import graph or introduce a circular dependency? With plugin-style AI, those questions are mostly answered by you—slowly, cautiously, and repeatedly.&lt;/p&gt;
&lt;p&gt;Cursor flips the default by making AI part of the editor’s workflow—not an overlay you summon when you’re stuck.&lt;/p&gt;
&lt;h2 id="ai-as-a-foundation-codebase-aware-chat-that-actually-knows-the-room"&gt;AI as a foundation: codebase-aware chat that actually knows the room&lt;/h2&gt;
&lt;p&gt;The first shift is how chat behaves when it’s integrated into the editor itself. In Cursor, the assistant isn’t just generating plausible suggestions; it’s participating in your project context as a first-class object. Ask something like: “Where are we validating auth tokens, and what happens when refresh fails?” and you don’t get a generic explanation—you get answers grounded in your repository’s structure, names, and actual code paths.&lt;/p&gt;
&lt;p&gt;What surprised me most wasn’t that it could “find files,” but that it could reason across them without making me jump through hoops. In a normal setup, you’d bounce between tabs, copy/paste snippets, and prompt the assistant with whatever you managed to extract. The cognitive tax is real.&lt;/p&gt;
&lt;p&gt;In Cursor, you can approach questions the way you would with a strong teammate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Show me every place this API is called and why the error mapping differs between routes.”&lt;/li&gt;
&lt;li&gt;“What would break if we change this function signature? List impacted files and propose a safe update plan.”&lt;/li&gt;
&lt;li&gt;“Explain this module like I’m onboarding—point me to the data flow and the contracts between layers.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can still prompt it like a chatbot, but the editor integration nudges you toward problem-solving. It’s less “talk to a model” and more “coordinate with a system that has your code loaded as working memory.”&lt;/p&gt;
&lt;h2 id="multi-file-edits-the-difference-between-suggestions-and-commits"&gt;Multi-file edits: the difference between suggestions and commits&lt;/h2&gt;
&lt;p&gt;The second shift is more practical, and arguably more important: how changes get applied.&lt;/p&gt;
&lt;p&gt;With plugin-style AI, you typically accept output text and then manually edit it into place. That turns every change into a series of separate steps. The refactor becomes fragile, because each step is an opportunity for mismatch—one missing import here, one inconsistent type there, a formatting difference that triggers lint errors later.&lt;/p&gt;
&lt;p&gt;Cursor’s multi-file editing is designed around atomic-ish workflows: you don’t just “get an answer,” you get a set of edits that are meant to land together. When you’re refactoring something that spans routing, validation, and storage, you want the assistant to change all relevant files in concert, not one at a time while you hope the remaining steps still align.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine you’re introducing a new field to a request payload and need it to propagate from the HTTP handler to the service layer, into persistence, and back out through a response transformer. In a plugin setup, you’d be juggling prompts, searching for usages, and verifying that everything updated cleanly.&lt;/p&gt;
&lt;p&gt;In Cursor, you can ask for the change as a goal—“Add &lt;code&gt;tenantId&lt;/code&gt; to the request payload, validate it, persist it, and include it in the response where applicable”—and then inspect a coherent set of modifications. Even when you disagree with parts, the workflow still feels like you’re reviewing a single proposed change, not stitching together a patchwork.&lt;/p&gt;
&lt;h2 id="ai-generated-diffs-you-can-acceptor-rejectper-hunk"&gt;AI-generated diffs you can accept—or reject—per hunk&lt;/h2&gt;
&lt;p&gt;This is where my trust issues finally got resolved. The most important feature of an AI editor isn’t raw generation quality; it’s controllability.&lt;/p&gt;
&lt;p&gt;Cursor presents changes as diffs with granularity—hunks you can accept or reject. That turns the assistant into something closer to a code review partner. You’re not forced to swallow a whole refactor in one gulp. You can say: yes, update the data model; no, not like that for validation; revise the mapping logic to match the existing error schema.&lt;/p&gt;
&lt;p&gt;In practice, this changes how you use the tool:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ask for a change plan in plain language.&lt;/li&gt;
&lt;li&gt;Review the diff with a “lint-and-logic” mindset.&lt;/li&gt;
&lt;li&gt;Accept the safe hunks first.&lt;/li&gt;
&lt;li&gt;Push back on the risky ones with targeted follow-ups.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’ve ever worked with code generation tools before, you know the difference between “here’s a replacement file” and “here are incremental, reviewable edits.” Cursor behaves more like the latter, and that makes it drastically easier to keep your standards intact.&lt;/p&gt;
&lt;h2 id="the-composer-effect-generating-features-across-boundaries"&gt;The composer effect: generating features across boundaries&lt;/h2&gt;
&lt;p&gt;The composer—Cursor’s ability to generate larger chunks of functionality—was the final nail in my old worldview. Plugins often excel at micro-edits or localized fixes. A composer mindset is different: it’s oriented around delivering a feature that spans multiple modules, not just patching what’s directly visible.&lt;/p&gt;
&lt;p&gt;Instead of “autocomplete, then fix the rest,” it’s closer to “implement the feature, then review and adjust.” You can request things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Build a new admin endpoint to list users with pagination and role filtering.”&lt;/li&gt;
&lt;li&gt;“Add a feature flag system that toggles behavior per request, using existing config patterns.”&lt;/li&gt;
&lt;li&gt;“Refactor this workflow into a cleaner service layer and update tests accordingly.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key isn’t that the assistant always nails every line on the first try. It’s that the editor experience supports iterative construction. You can accept the parts that match your architecture, reject the parts that don’t, and guide the rest until it conforms to the project’s style and semantics.&lt;/p&gt;
&lt;p&gt;And because the system is editor-native, the generated work is immediately inspectable, runnable, and diffable. That reduces the “AI output as text blob” problem that often kills momentum in other tools.&lt;/p&gt;
&lt;h2 id="how-i-use-cursor-day-to-day-without-losing-control"&gt;How I use Cursor day-to-day (without losing control)&lt;/h2&gt;
&lt;p&gt;Let me be blunt: AI-native editors only feel magical if you adopt the right habits. Otherwise, they become yet another source of confusion. Here’s the workflow that made Cursor consistently effective for me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start with intent, not instructions.&lt;/strong&gt; Instead of “write a function,” say “I need an endpoint that validates X, stores Y, and returns Z with these error cases.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrain scope explicitly.&lt;/strong&gt; “Update only files under &lt;code&gt;services/&lt;/code&gt; and &lt;code&gt;routes/&lt;/code&gt;” beats vague prompts every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review diffs like you would a PR.&lt;/strong&gt; Accept small hunks; reject anything that touches security-sensitive code without scrutiny.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the chat to verify assumptions.&lt;/strong&gt; When the assistant proposes something that sounds right, ask it to point to the exact code path it used to reach that conclusion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat the composer as a starting engine.&lt;/strong&gt; Expect to adjust architecture choices. Your job isn’t to rubber-stamp—it’s to steer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach is how you keep the assistant from becoming an autopilot. You’re still the engineer. You’re just delegating the grunt work: searching, wiring, applying consistent edits across files, and drafting the mechanical parts of refactors.&lt;/p&gt;
&lt;h2 id="conclusion-ai-native-beats-ai-adjacent-because-software-is-bigger-than-text"&gt;Conclusion: AI-native beats AI-adjacent because software is bigger than text&lt;/h2&gt;
&lt;p&gt;I resisted AI-native editors because I believed the value was mostly in models and prompts. Cursor forced a different realization: the editor experience—the way context flows, how changes are represented, and how multi-file edits are handled—determines whether AI is genuinely useful or just decorative.&lt;/p&gt;
&lt;p&gt;If you want AI to help with “the next line,” a plugin can be enough. But if you want an assistant that can see your repository as a coherent system and propose reviewable changes that span files, AI-native is the point. Cursor changed my mind because it made AI feel less like a suggestion engine and more like a collaborative editing environment—one where you can move fast without surrendering control.&lt;/p&gt;</content></item><item><title>The Case for Zig in 2025</title><link>https://decastro.work/blog/case-for-zig-2025/</link><pubDate>Thu, 30 Jan 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/case-for-zig-2025/</guid><description>&lt;p&gt;Every new language claims it will “make systems programming easier,” but most deliver the same bargain in different wrapping paper: you either accept C’s manual sharp edges or you accept a stricter compiler regime that can feel like it’s steering your hands. In 2025, Zig stands out because it offers a third path. You get C-like control, explicitness, and predictable performance—without reaching for a macro-heavy toolchain or wrestling the language into Rust’s ownership worldview. It’s not a replacement for Rust. It’s an alternative philosophy: trust the programmer, while making the unsafe parts loud and the compile-time metaprogramming sane.&lt;/p&gt;</description><content>&lt;p&gt;Every new language claims it will “make systems programming easier,” but most deliver the same bargain in different wrapping paper: you either accept C’s manual sharp edges or you accept a stricter compiler regime that can feel like it’s steering your hands. In 2025, Zig stands out because it offers a third path. You get C-like control, explicitness, and predictable performance—without reaching for a macro-heavy toolchain or wrestling the language into Rust’s ownership worldview. It’s not a replacement for Rust. It’s an alternative philosophy: trust the programmer, while making the unsafe parts loud and the compile-time metaprogramming sane.&lt;/p&gt;
&lt;h2 id="why-zig-is-different-explicitness-over-enforcement"&gt;Why Zig is different: “explicitness” over “enforcement”&lt;/h2&gt;
&lt;p&gt;Zig’s core pitch is simple: do more with less magic. C gives you direct control, but it also gives you footguns—buffer overflows, lifetime confusion, and error paths that are easy to ignore. Rust tries to prevent whole categories of problems by making invalid states hard to represent. That’s powerful, but it can also impose a mental model that doesn’t fit every problem, especially when you want to bend the compiler to your will.&lt;/p&gt;
&lt;p&gt;Zig takes a more human-centered approach. Instead of enforcing ownership and borrowing across the entire ecosystem, it emphasizes &lt;em&gt;explicit control flow&lt;/em&gt; and &lt;em&gt;visible effects&lt;/em&gt;. If something can fail, it’s expressed in the type system in a way that doesn’t hide behind conventions. If memory management is your job, Zig makes it your job—clearly and ergonomically—rather than pretending the compiler can understand every intent.&lt;/p&gt;
&lt;p&gt;This isn’t “no safety.” It’s “safety by design clarity.” For many teams, that trade feels healthier than either extreme.&lt;/p&gt;
&lt;h2 id="compile-time-code-that-behaves-like-code-not-macros"&gt;Compile-time code that behaves like code (not macros)&lt;/h2&gt;
&lt;p&gt;If you’ve spent time in C++ template land or C macro land, you already know the pain: metaprogramming that’s clever can become unreadable, and compile-time diagnostics often feel like deciphering an ancient prophecy.&lt;/p&gt;
&lt;p&gt;Zig’s &lt;code&gt;comptime&lt;/code&gt; flips that relationship. It’s not a macro system in the “substitute tokens until you hope it works” sense. Instead, Zig uses regular language constructs and asks the compiler to evaluate them at compile time when you choose. That means your metaprogramming is still “Zig,” with the same mental model—loops, branches, types, and values—just executed earlier.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine you’re writing a small serialization function that needs to know field offsets for a packed struct. With &lt;code&gt;comptime&lt;/code&gt;, you can iterate fields at compile time and generate the offset table using ordinary control flow. The result is deterministic, debuggable code generation without shoving logic into preprocessor macros.&lt;/p&gt;
&lt;p&gt;The practical upside for 2025 developers is speed of thinking. You can prototype compile-time logic quickly, because you’re not debugging token pastes—you’re debugging code. And when errors happen, they generally feel closer to “this is wrong here” rather than “somewhere inside the macro maze.”&lt;/p&gt;
&lt;h2 id="error-handling-without-ceremony-make-failure-a-first-class-path"&gt;Error handling without ceremony: make failure a first-class path&lt;/h2&gt;
&lt;p&gt;Zig’s error handling is one of its most distinctive design choices. It’s explicit, but it’s not wrapped in a thicket of ceremony. When a function can fail, that fact is represented, and callers must deal with it—either by handling it, transforming it, or bubbling it up.&lt;/p&gt;
&lt;p&gt;In practice, this leads to fewer “mystery returns.” Instead of returning sentinel values like &lt;code&gt;-1&lt;/code&gt; or &lt;code&gt;NULL&lt;/code&gt; and hoping the caller remembers the contract, Zig pushes the contract into the signature. The result is code that reads like a conversation: “here’s what can go wrong, and here’s what I do when it does.”&lt;/p&gt;
&lt;p&gt;Consider a typical scenario in systems tools: parsing a config file, allocating buffers, and then calling into an OS API. In many languages, those failure modes get collapsed into either exceptions (sometimes too global) or error codes (sometimes too easy to ignore). Zig encourages a middle road: propagate errors when you can, handle them when you must.&lt;/p&gt;
&lt;p&gt;For teams, this matters. Error paths are not an afterthought; they become part of the control flow you review during code review. That alone can reduce production incidents—even when you’re doing “unsafe” low-level work—because your “what if it fails” statements are no longer optional.&lt;/p&gt;
&lt;h2 id="memory-control-you-can-actually-live-with"&gt;Memory control you can actually live with&lt;/h2&gt;
&lt;p&gt;Zig’s approach to memory is direct. You don’t get implicit garbage collection, and you don’t get Rust’s ownership constraints everywhere. Instead, Zig gives you allocators as explicit parameters and patterns you can adopt consistently across a codebase.&lt;/p&gt;
&lt;p&gt;This is a big part of why Zig appeals to C developers who are tired of repeating the same manual checks, but also tired of fighting Rust’s rules for certain workflows. Game engines, embedded systems, performance-critical real-time components, plugin architectures—these can all involve patterns where strict ownership can be awkward or expensive to contort around.&lt;/p&gt;
&lt;p&gt;A practical example: suppose you’re building an asset pipeline that loads models and textures, then stores them in an arena allocator for the duration of the program. In Zig, you can thread an allocator through your loader functions, decide the allocation strategy at the boundary, and keep your internal code honest. You can even use different allocators per subsystem (arena for long-lived data, page allocator for transient buffers) without pretending that one allocator model magically fits everything.&lt;/p&gt;
&lt;p&gt;The key is that Zig’s memory model doesn’t try to “save you” by hiding complexity. It helps you &lt;em&gt;handle complexity&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;And importantly: you can test that complexity. Because memory management is explicit, it’s straightforward to plug in debug allocators in tests, catch leaks and misuse early, and keep production behavior predictable.&lt;/p&gt;
&lt;h2 id="cross-compilation-that-targets-reality-not-a-demo"&gt;Cross-compilation that targets reality, not a demo&lt;/h2&gt;
&lt;p&gt;Zig’s build system is designed for practical cross-compilation. If you’ve ever tried to cross-compile a non-trivial C/C++ project and spent days debugging platform quirks, you already appreciate what Zig is trying to reduce: the “works on my machine” tax.&lt;/p&gt;
&lt;p&gt;Zig’s tooling makes it more natural to produce binaries for different targets from the same codebase, including cross-compiling to architectures you don’t run locally. For teams delivering to embedded targets, containerized environments, or multiple CPU architectures, this can be a decisive advantage.&lt;/p&gt;
&lt;p&gt;Practically, this means you can structure projects with target-specific knobs while keeping the core logic shared. Build scripts can express the configuration clearly, rather than scattering it across fragile Makefile incantations and undocumented environment variables. The goal isn’t just compilation—it’s reproducibility.&lt;/p&gt;
&lt;p&gt;A good strategy is to define your “platform contract” early: what syscalls or libc assumptions you’re making, what alignment rules apply, and what ABI expectations you’re relying on. Then let Zig’s build configuration keep those decisions explicit and versioned.&lt;/p&gt;
&lt;h2 id="trust-the-programmer-the-right-philosophy-for-certain-projects"&gt;Trust the programmer: the “right” philosophy for certain projects&lt;/h2&gt;
&lt;p&gt;Zig’s positioning is not subtle: it’s a language that assumes competent programmers will make sensible choices, and it gives them the tools to do so without relying on compiler enforcement everywhere. That can sound risky—until you notice how Zig tries to compensate. It keeps the sharp edges visible. It prefers clear control flow over magical behavior. It makes compile-time generation understandable via real code.&lt;/p&gt;
&lt;p&gt;This philosophy fits specific project types extremely well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Low-level libraries&lt;/strong&gt; where you want predictable performance and explicit memory behavior, and where enforcing ownership would be more constraining than helpful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling and infrastructure&lt;/strong&gt; where clarity and portability matter, and where error handling needs to be disciplined without turning every function into a novel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded and systems-adjacent code&lt;/strong&gt; where you might be mapping to hardware resources and need a direct language-level handle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In these contexts, Zig offers something compelling: fewer “language fights.” You spend less time wrestling the compiler’s model of your intent, and more time expressing the system you’re building.&lt;/p&gt;
&lt;p&gt;That doesn’t mean Zig is universally “better.” If you crave Rust’s strongest invariants—especially around complex shared ownership and concurrency—Rust may still be the better bet. Zig’s win is in flexibility and readability of low-level intent, with compile-time power that doesn’t feel like a detour.&lt;/p&gt;
&lt;h2 id="conclusion-zig-is-the-pragmatic-third-option-in-2025"&gt;Conclusion: Zig is the pragmatic third option in 2025&lt;/h2&gt;
&lt;p&gt;Zig’s case in 2025 isn’t that it’s safer than everything else or that it replaces Rust or C. It’s that it offers a deliberate middle path: C-level control, explicit failure and memory decisions, and a compile-time system that behaves like real code. For teams that want fewer footguns than C provides, but also don’t want Rust’s ownership model to dictate architecture, Zig can be the right trade.&lt;/p&gt;
&lt;p&gt;If you’re building systems software where performance, portability, and clarity matter—and you want the compiler to assist rather than fully govern—Zig is worth serious consideration this year.&lt;/p&gt;</content></item><item><title>Model Context Protocol: The Standard That Makes AI Agents Work</title><link>https://decastro.work/blog/model-context-protocol-standard-ai-agents-work/</link><pubDate>Sat, 18 Jan 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/model-context-protocol-standard-ai-agents-work/</guid><description>&lt;p&gt;AI agents don’t fail because they “can’t think.” They fail because they can’t reliably &lt;em&gt;act&lt;/em&gt;: fetch the right data, call the right tools, and do it in a way you can trust. For years, the bottleneck wasn’t intelligence—it was integration. Model Context Protocol (MCP) is quickly becoming the answer, offering a universal way for large language models to connect to external tools and systems. If REST made web APIs boringly interoperable, MCP is doing the same for agent tool use.&lt;/p&gt;</description><content>&lt;p&gt;AI agents don’t fail because they “can’t think.” They fail because they can’t reliably &lt;em&gt;act&lt;/em&gt;: fetch the right data, call the right tools, and do it in a way you can trust. For years, the bottleneck wasn’t intelligence—it was integration. Model Context Protocol (MCP) is quickly becoming the answer, offering a universal way for large language models to connect to external tools and systems. If REST made web APIs boringly interoperable, MCP is doing the same for agent tool use.&lt;/p&gt;
&lt;h2 id="the-integration-problem-ai-keeps-tripping-over"&gt;The integration problem AI keeps tripping over&lt;/h2&gt;
&lt;p&gt;Most “agent” demos look impressive because they’re built on a stack that’s invisible to the user: custom wrappers, hand-written tool schemas, special-case authentication, bespoke adapters for each service. That’s fine for a prototype. It’s a nightmare for a product.&lt;/p&gt;
&lt;p&gt;Here’s what usually goes wrong:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool definitions don’t travel.&lt;/strong&gt; Every LLM integration ends up with its own format for “here are the tools you can call.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context is inconsistent.&lt;/strong&gt; Agents may retrieve data in different ways depending on the tool, causing unpredictable behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth and permissions are brittle.&lt;/strong&gt; Connecting to Slack, GitHub, databases, or ticketing systems often requires custom OAuth flows and environment-specific hacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance becomes an identity crisis.&lt;/strong&gt; Add one new capability (say, “read from Postgres”) and you rewrite integration glue.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a fragmented ecosystem where every new model or agent framework risks breaking existing tooling—or forcing developers to duplicate effort across integrations.&lt;/p&gt;
&lt;p&gt;MCP attacks this at the root: instead of building bespoke tool adapters for every LLM, you build once—against a standard protocol.&lt;/p&gt;
&lt;h2 id="what-mcp-is-in-plain-terms"&gt;What MCP is, in plain terms&lt;/h2&gt;
&lt;p&gt;Model Context Protocol is a standard way for LLM clients (the “agent runtime”) to talk to tool and data providers via MCP servers. Think of MCP as a contract for tool use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP server:&lt;/strong&gt; Exposes capabilities—tools, resources, and structured actions—over a consistent interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP client:&lt;/strong&gt; Connects to one or more MCP servers and lets the model invoke those capabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, that means an agent can connect to a GitHub server, a Slack server, and a Postgres server using the same underlying mechanism. The model doesn’t need a unique integration strategy per service. It needs the tools that are available—and MCP gives you a uniform way to describe them.&lt;/p&gt;
&lt;p&gt;The “context” part matters too. MCP isn’t only about calling functions. It’s also about giving the model a consistent pathway to retrieve relevant information (resources) from external systems so the agent can decide what to do next.&lt;/p&gt;
&lt;h2 id="before-mcp-bespoke-toolchains-and-fragile-glue"&gt;Before MCP: bespoke toolchains and fragile glue&lt;/h2&gt;
&lt;p&gt;To appreciate why MCP matters, you need to see the old workflow.&lt;/p&gt;
&lt;p&gt;Imagine you’re building an internal agent for engineers. You want it to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query issues in GitHub.&lt;/li&gt;
&lt;li&gt;Read or post messages in Slack.&lt;/li&gt;
&lt;li&gt;Pull deployment metadata from a Postgres database.&lt;/li&gt;
&lt;li&gt;Maybe also check CI status and open a ticket.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before a standard, you typically write separate integrations like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A GitHub “tool wrapper” that maps model intent into GitHub API calls.&lt;/li&gt;
&lt;li&gt;A Slack wrapper handling message formatting, channel selection, and permissions.&lt;/li&gt;
&lt;li&gt;A database wrapper converting SQL requests into safe queries (or, worse, running raw queries).&lt;/li&gt;
&lt;li&gt;A bespoke orchestration layer that manages auth tokens and error handling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now add new models or new agent runtimes. Even if they’re both “LLM clients,” they may expect different tool schemas, different calling conventions, or different streaming patterns. You end up maintaining N×M glue code: tools times clients. That slows down iteration and makes reliability worse, because every edge case is a custom one.&lt;/p&gt;
&lt;p&gt;MCP is the pivot: you standardize the interface between &lt;em&gt;clients&lt;/em&gt; and &lt;em&gt;servers&lt;/em&gt;. The tools become reusable building blocks.&lt;/p&gt;
&lt;h2 id="after-mcp-servers-turn-capabilities-into-plug-and-play-modules"&gt;After MCP: servers turn capabilities into plug-and-play modules&lt;/h2&gt;
&lt;p&gt;The most pragmatic way to understand MCP is to treat MCP servers as reusable capability packages.&lt;/p&gt;
&lt;p&gt;Suppose you’re building a support engineer copilot. You don’t want to write a custom “read from Jira” tool schema from scratch every time. With MCP, you can point your agent runtime at an MCP server that already exposes Jira capabilities in a consistent format.&lt;/p&gt;
&lt;p&gt;Even the examples in common ecosystems follow the same pattern: there are MCP servers for mainstream platforms like &lt;strong&gt;GitHub&lt;/strong&gt;, &lt;strong&gt;Slack&lt;/strong&gt;, and &lt;strong&gt;PostgreSQL&lt;/strong&gt;, and the community has expanded far beyond that. The key isn’t the specific services—it’s the repeatable integration mechanics.&lt;/p&gt;
&lt;p&gt;Here’s a concrete, product-minded scenario:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your agent receives a request: “Find the latest deployment failures and post a summary to #incident-response.”&lt;/li&gt;
&lt;li&gt;Your MCP-connected agent runtime:
&lt;ul&gt;
&lt;li&gt;Calls the Postgres server (or a CI/status server) to fetch relevant failure records.&lt;/li&gt;
&lt;li&gt;Calls the GitHub server to link failures to commits or issues (if needed).&lt;/li&gt;
&lt;li&gt;Calls the Slack server to post the summary using the correct channel and formatting rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Your application doesn’t re-learn each service’s calling conventions because MCP normalizes the interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is exactly what developers wanted from web APIs with REST: one consistent style for how clients talk to servers. MCP brings that “boring interoperability” to agent tool use.&lt;/p&gt;
&lt;h2 id="designing-robust-agents-with-mcp-not-just-demos"&gt;Designing robust agents with MCP (not just demos)&lt;/h2&gt;
&lt;p&gt;Standardizing tool access is a major step—but it’s not a magic wand. The difference between a demo and a dependable product is guardrails and operational discipline.&lt;/p&gt;
&lt;p&gt;Here are practical ways to build robustness on top of MCP:&lt;/p&gt;
&lt;h3 id="1-treat-tools-like-production-endpoints-not-suggestions"&gt;1) Treat tools like production endpoints, not suggestions&lt;/h3&gt;
&lt;p&gt;Even with MCP, you still need to define clear tool behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer read-only tools by default.&lt;/li&gt;
&lt;li&gt;Require explicit user confirmation before write actions (e.g., “post to Slack,” “open a PR”).&lt;/li&gt;
&lt;li&gt;Use structured inputs with validation so the model can’t “guess” parameter formats.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-make-permissions-explicit-and-least-privilege"&gt;2) Make permissions explicit and least-privilege&lt;/h3&gt;
&lt;p&gt;MCP servers will likely run with tokens/credentials. Don’t overload them.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Separate credentials for read vs write.&lt;/li&gt;
&lt;li&gt;Scope tokens to specific repositories, channels, or database schemas.&lt;/li&gt;
&lt;li&gt;Audit what the agent can do if a prompt injection tries to trick it into calling a write tool.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-normalize-your-data-contract-with-resources"&gt;3) Normalize your “data contract” with resources&lt;/h3&gt;
&lt;p&gt;Agents perform better when the retrieved context is predictable. If an MCP server returns resources in a consistent structure—like “issue summary + status + last updated timestamp”—the agent can reliably decide next steps.&lt;/p&gt;
&lt;p&gt;A simple win: design your internal MCP servers so they return compact, model-friendly payloads rather than dumping raw API responses.&lt;/p&gt;
&lt;h3 id="4-add-tracing-at-the-protocol-boundary"&gt;4) Add tracing at the protocol boundary&lt;/h3&gt;
&lt;p&gt;When something goes wrong—wrong tool called, wrong parameters, missing context—you want visibility.&lt;/p&gt;
&lt;p&gt;Log:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which MCP server was invoked,&lt;/li&gt;
&lt;li&gt;which tool/action was called,&lt;/li&gt;
&lt;li&gt;the request/response metadata (careful with secrets),&lt;/li&gt;
&lt;li&gt;timing and failure reasons.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes your agent debuggable, which is what you’ll need once usage grows beyond internal testing.&lt;/p&gt;
&lt;h3 id="5-compose-multiple-servers-into-a-workflow-not-a-single-god-tool"&gt;5) Compose multiple servers into a workflow, not a single “god tool”&lt;/h3&gt;
&lt;p&gt;It’s tempting to create one “Everything” MCP server that handles all integrations. Resist that.&lt;/p&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let each server own its domain (GitHub, Slack, Postgres).&lt;/li&gt;
&lt;li&gt;In your agent logic, orchestrate them deliberately.&lt;/li&gt;
&lt;li&gt;This makes it easier to swap providers and reduces blast radius when a single integration breaks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="where-mcp-fits-in-your-architecture-today"&gt;Where MCP fits in your architecture today&lt;/h2&gt;
&lt;p&gt;So when should you bet on MCP? If you’re building anything that needs to call external systems—especially if you expect those systems to change—MCP is the kind of decision that prevents future rewrites.&lt;/p&gt;
&lt;p&gt;Use MCP when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’re integrating more than one external system (almost everyone is).&lt;/li&gt;
&lt;li&gt;You want to reuse tool capabilities across different agent runtimes or model choices.&lt;/li&gt;
&lt;li&gt;You plan to add new tools over time without turning your codebase into a museum of custom adapters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You still have to choose the rest of your stack—agent runtime, orchestration layer, security model—but MCP reduces the surface area where integrations become bespoke and fragile.&lt;/p&gt;
&lt;p&gt;The opinionated takeaway: &lt;strong&gt;MCP should be your default integration layer for tool access&lt;/strong&gt;, not a “nice-to-have” experiment. Standards are only valuable when you commit to them early enough to prevent fragmentation.&lt;/p&gt;
&lt;h2 id="conclusion-the-standard-that-turns-agents-into-software"&gt;Conclusion: The standard that turns agents into software&lt;/h2&gt;
&lt;p&gt;AI agents are finally becoming practical, but not because models got smarter overnight. They’re becoming practical because the ecosystem is converging on shared interfaces for tool use. MCP does for agent integrations what REST did for web APIs: it gives developers a consistent, reusable contract so capabilities become modular instead of bespoke.&lt;/p&gt;
&lt;p&gt;If you’re building AI-powered applications that need to connect to real systems—code, messaging, data, workflows—MCP isn’t just worth tracking. It’s the integration foundation you’ll be glad you chose when your next feature request doesn’t turn into a rewrite.&lt;/p&gt;</content></item><item><title>The Year of the AI-Augmented Developer</title><link>https://decastro.work/blog/year-of-ai-augmented-developer/</link><pubDate>Sun, 12 Jan 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/year-of-ai-augmented-developer/</guid><description>&lt;p&gt;In 2025, “writing code” quietly stops meaning the same thing it did a year ago. The fastest hands will still matter—but the decisive advantage is shifting to a different skill set: defining the problem with precision, directing an agent toward the right solution, and catching the failure modes before they become incidents.&lt;/p&gt;
&lt;p&gt;The most important change isn’t that AI can generate code. It’s that AI can participate in an end-to-end workflow—inside the IDE, against your repo, and against your standards—while you stay responsible for correctness, security, and design.&lt;/p&gt;</description><content>&lt;p&gt;In 2025, “writing code” quietly stops meaning the same thing it did a year ago. The fastest hands will still matter—but the decisive advantage is shifting to a different skill set: defining the problem with precision, directing an agent toward the right solution, and catching the failure modes before they become incidents.&lt;/p&gt;
&lt;p&gt;The most important change isn’t that AI can generate code. It’s that AI can participate in an end-to-end workflow—inside the IDE, against your repo, and against your standards—while you stay responsible for correctness, security, and design.&lt;/p&gt;
&lt;h2 id="ai-in-the-ide-from-autocomplete-to-collaborators"&gt;AI in the IDE: from autocomplete to collaborators&lt;/h2&gt;
&lt;p&gt;For years, developer tooling treated AI like a suggestion engine: one line here, a refactor there. In 2025, that boundary is dissolving. Major IDE experiences are folding AI features directly into the editor loop: explain this file, generate tests, propose a migration, summarize a PR, and even execute multi-step plans across multiple files.&lt;/p&gt;
&lt;p&gt;If you’ve used tools like GitHub Copilot, Cursor, Windsurf, or Zed AI, you’ve probably noticed the pattern: the “agent” isn’t only writing. It’s reasoning through context you provide—sometimes by reading the codebase, sometimes by asking clarifying questions, and often by proposing a set of edits that look plausible at first glance.&lt;/p&gt;
&lt;p&gt;That’s why this year feels different. The bottleneck is no longer raw typing speed or even syntax knowledge. It’s intent. The agent can crank out boilerplate quickly, but it can’t reliably infer what &lt;em&gt;you&lt;/em&gt; meant when your requirements are incomplete, your constraints are implicit, or your architecture decisions live in your head.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Treat AI as a fast subordinate, not an autonomous authority. You set direction. The IDE assistant executes.&lt;/p&gt;
&lt;h2 id="the-new-advantage-problem-definition-beats-problem-execution"&gt;The new advantage: problem definition beats problem execution&lt;/h2&gt;
&lt;p&gt;The developers thriving in this moment aren’t the ones who type fastest. They’re the ones who make the agent’s job easy—by turning vague requests into crisp engineering tasks.&lt;/p&gt;
&lt;p&gt;Consider two prompts for the same feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“Add caching to this endpoint.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Add an in-memory cache to &lt;code&gt;GET /v1/orders&lt;/code&gt; keyed by &lt;code&gt;userId&lt;/code&gt;. Cache TTL: 60 seconds. Invalidate on &lt;code&gt;POST /v1/orders&lt;/code&gt; for that user. Preserve existing response shape. Add unit tests for cache hits/misses and concurrency behavior.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The second version is what an agent needs: explicit constraints, acceptance criteria, and where to look. Notice the difference: it’s not just describing behavior, it’s specifying failure boundaries.&lt;/p&gt;
&lt;p&gt;Now look at code review. In a normal workflow, you review the diff. In an AI-augmented workflow, you also review the &lt;em&gt;specification you gave the agent&lt;/em&gt;. If the spec was sloppy, the diff will be, too—only faster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Before you generate, write a mini “engineering contract” in plain language. Then ask the AI to implement &lt;em&gt;against that contract&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="critical-review-becomes-the-primary-engineering-skill"&gt;Critical review becomes the primary engineering skill&lt;/h2&gt;
&lt;p&gt;When AI starts producing multi-file changes, the review job changes shape. You still inspect code—but you also audit assumptions.&lt;/p&gt;
&lt;p&gt;Here are common “looks right, is wrong” categories to train yourself to catch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Boundary mistakes&lt;/strong&gt;&lt;br&gt;
Off-by-one logic, incorrect pagination semantics, wrong time zone conversions, or mishandled empty states. Agents often handle the “happy path” well and miss the edges unless you force them to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security drift&lt;/strong&gt;&lt;br&gt;
Authentication checks may be omitted in one route. Authorization logic might be inconsistent with existing patterns. Input validation can become “best effort.” The agent is optimizing for completion, not for threat modeling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inconsistent architecture&lt;/strong&gt;&lt;br&gt;
The AI may introduce a new abstraction where none exists, or use an established one incorrectly. It might also refactor code while preserving behavior—except the behavior includes subtle performance characteristics you didn’t mention.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Test theater&lt;/strong&gt;&lt;br&gt;
Generated tests can be brittle, duplicate existing coverage, or assert the wrong invariants. You want tests that lock down intent, not just that pass.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A sharp workflow is to review in layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spec layer:&lt;/strong&gt; Did the changes match the requirements you wrote?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety layer:&lt;/strong&gt; Any security or data integrity concerns?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correctness layer:&lt;/strong&gt; Edge cases and invariants.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintainability layer:&lt;/strong&gt; Naming, structure, and alignment with existing conventions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Don’t skim AI code like it’s just another PR. Review it like it’s a draft authored by someone who didn’t live with your system for years.&lt;/p&gt;
&lt;h2 id="architectural-judgment-scales-upward-not-sideways"&gt;Architectural judgment scales upward, not sideways&lt;/h2&gt;
&lt;p&gt;The profession isn’t shrinking; it’s stratifying. The floor rises because AI handles a lot of boilerplate—CRUD scaffolding, routine test creation, repetitive formatting, mechanical refactors, and documentation drafts. The ceiling rises faster because humans remain the only reliable source of architectural judgment in ambiguous situations.&lt;/p&gt;
&lt;p&gt;AI can propose patterns. It can even justify them. But it can’t truly “feel” the long-term shape of your product, your operational constraints, and your team’s maintenance realities. That’s where senior developers become disproportionately valuable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;choosing boundaries and data ownership&lt;/li&gt;
&lt;li&gt;designing failure modes (timeouts, retries, backpressure)&lt;/li&gt;
&lt;li&gt;establishing invariants across services&lt;/li&gt;
&lt;li&gt;deciding what &lt;em&gt;not&lt;/em&gt; to automate&lt;/li&gt;
&lt;li&gt;orchestrating migrations without breaking production&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-concrete-example-refactor-for-cleanliness-isnt-a-plan"&gt;A concrete example: “refactor for cleanliness” isn’t a plan&lt;/h3&gt;
&lt;p&gt;Imagine you ask an AI to “refactor the payment module for clarity.” It may produce cleaner functions and better naming. But if it also changes call graphs, introduces subtle ordering dependencies, or alters transactional semantics, you’ve traded readability for risk.&lt;/p&gt;
&lt;p&gt;Instead, a more senior framing looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Extract a payment authorization interface without changing transactional boundaries.”&lt;/li&gt;
&lt;li&gt;“Keep idempotency behavior identical.”&lt;/li&gt;
&lt;li&gt;“Add integration tests for rollback scenarios.”&lt;/li&gt;
&lt;li&gt;“Stage the refactor behind a feature flag.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re still letting the agent do work—but you’re putting guardrails around the decisions that must be yours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Use AI for execution. Reserve architecture for judgment.&lt;/p&gt;
&lt;h2 id="designing-systems-that-agents-can-modify-safely"&gt;Designing systems that agents can modify safely&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable truth: agents will modify your repo whether you’re ready or not. The winners will be the teams that make their codebases easier to edit safely.&lt;/p&gt;
&lt;p&gt;You can “agent-proof” a system without building a fantasy future of autonomous software. The goal is simple: make intent legible, reduce hidden coupling, and centralize policies.&lt;/p&gt;
&lt;p&gt;Practical steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create clear module boundaries&lt;/strong&gt;&lt;br&gt;
If authorization rules are scattered, an agent will eventually miss one. Centralize those rules, and document the contracts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Codify invariants&lt;/strong&gt;&lt;br&gt;
Examples: “All domain IDs must be validated at entry,” “All writes must be idempotent,” “Every API response must include correlation IDs.” Encode these in shared utilities and test fixtures, not in tribal memory.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make style and structure predictable&lt;/strong&gt;&lt;br&gt;
Consistent naming, standard folder layout, and established patterns reduce the chance the agent invents a new way to do the same thing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Invest in high-signal tests&lt;/strong&gt;&lt;br&gt;
Not every test needs to be generated. But when agents change behavior, you want fast tests that catch violations immediately—especially around authorization, parsing, and concurrency.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use review checklists for AI-generated diffs&lt;/strong&gt;&lt;br&gt;
For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Did we update authz/authn in every affected route?”&lt;/li&gt;
&lt;li&gt;“Are serialization formats unchanged?”&lt;/li&gt;
&lt;li&gt;“Are we preserving idempotency keys?”&lt;/li&gt;
&lt;li&gt;“Do we handle null/empty edge cases?”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Your best defense against incorrect AI edits is not distrust—it’s structure.&lt;/p&gt;
&lt;h2 id="working-with-agents-in-real-projects-a-disciplined-loop"&gt;Working with agents in real projects: a disciplined loop&lt;/h2&gt;
&lt;p&gt;You don’t need a new philosophy—you need a better loop. Here’s a pragmatic workflow many strong teams are converging on:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write a spec you can test&lt;/strong&gt;&lt;br&gt;
Include acceptance criteria, error handling expectations, and non-goals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ask for a small first change&lt;/strong&gt;&lt;br&gt;
Don’t request a full rewrite. Generate a minimal diff that proves the approach.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Review the diff and the reasoning&lt;/strong&gt;&lt;br&gt;
If the tool offers a plan, evaluate it. If it doesn’t, force the output to explain assumptions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add or adjust tests before widening scope&lt;/strong&gt;&lt;br&gt;
Make it safe to iterate. Generated code without tests is just faster risk.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Repeat with tighter constraints&lt;/strong&gt;&lt;br&gt;
Once you see how the agent interprets your instructions, sharpen the next prompt and limit the next edit.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you want a litmus test: if your prompt doesn’t mention failure modes, invariants, or constraints, you’re not directing an agent—you’re outsourcing ambiguity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical takeaway:&lt;/strong&gt; Treat each AI-assisted change like a contract negotiation: clarify, verify, then expand.&lt;/p&gt;
&lt;h2 id="conclusion-the-floor-rises-and-the-ceiling-rises-faster"&gt;Conclusion: the floor rises, and the ceiling rises faster&lt;/h2&gt;
&lt;p&gt;2025 will not reward the people who can produce code at maximum speed. It will reward the people who can produce &lt;em&gt;clarity&lt;/em&gt;: crisp requirements, thoughtful architectures, and ruthless reviews of AI output. The floor is rising as agents eliminate boilerplate. The ceiling is rising as humans handle ambiguity, responsibility, and system-level judgment.&lt;/p&gt;
&lt;p&gt;If you want to stay competitive, don’t just learn which button to click in your IDE. Upgrade your engineering habits: specify better, test more intentionally, and architect with the assumption that AI will be editing right alongside you.&lt;/p&gt;</content></item><item><title>AI Agents Are Coming for Your Workflow, Not Your Job</title><link>https://decastro.work/blog/ai-agents-coming-for-workflow-not-job/</link><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ai-agents-coming-for-workflow-not-job/</guid><description>&lt;p&gt;For years, AI in software has felt like a productivity accessory: a smarter autocomplete, a helpful chat, a quicker way to draft boilerplate. That era is ending. The real shift isn’t “AI replaces developers.” It’s “AI starts replacing the workflow steps you’ve been manually babysitting.” And the moment you realize that, you’ll stop asking whether agents are useful—and start asking how to deploy them safely.&lt;/p&gt;
&lt;p&gt;The evolution from assistants to agents is the biggest architectural change since microservices. Not because it’s flashy, but because it changes how work is structured: from single-shot answers to delegated, multi-step execution.&lt;/p&gt;</description><content>&lt;p&gt;For years, AI in software has felt like a productivity accessory: a smarter autocomplete, a helpful chat, a quicker way to draft boilerplate. That era is ending. The real shift isn’t “AI replaces developers.” It’s “AI starts replacing the workflow steps you’ve been manually babysitting.” And the moment you realize that, you’ll stop asking whether agents are useful—and start asking how to deploy them safely.&lt;/p&gt;
&lt;p&gt;The evolution from assistants to agents is the biggest architectural change since microservices. Not because it’s flashy, but because it changes how work is structured: from single-shot answers to delegated, multi-step execution.&lt;/p&gt;
&lt;h2 id="from-autocomplete-to-delegation"&gt;From autocomplete to delegation&lt;/h2&gt;
&lt;p&gt;Autocomplete guesses the next token. Assistants respond to your prompt. Agents do something different: they &lt;em&gt;decide&lt;/em&gt; what to do next, &lt;em&gt;run&lt;/em&gt; it, and &lt;em&gt;correct&lt;/em&gt; course when reality disagrees.&lt;/p&gt;
&lt;p&gt;In practice, coding agents behave less like a “chat partner” and more like an operator who can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;plan a task (e.g., “fix failing tests and update docs”),&lt;/li&gt;
&lt;li&gt;execute commands (run tests, lint, build),&lt;/li&gt;
&lt;li&gt;inspect outputs (parse error logs, check diffs),&lt;/li&gt;
&lt;li&gt;iterate (apply changes, re-run, repeat),&lt;/li&gt;
&lt;li&gt;and report back with concrete results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This loop matters. Most teams don’t lose time because code is hard to write; they lose time because code fails in the real world—CI breaks, dependencies shift, edge cases appear, formatting policies kick back, tests flare up with cryptic stack traces. Agents are designed to live inside that friction.&lt;/p&gt;
&lt;p&gt;A simple way to think about it: if assistants help you write code faster, agents help you &lt;em&gt;finish work&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="the-workflow-graph-becomes-the-product"&gt;The “workflow graph” becomes the product&lt;/h2&gt;
&lt;p&gt;Microservices changed architecture by turning one big system into many independently deployable pieces. AI agents are doing something similar to how developers think about work.&lt;/p&gt;
&lt;p&gt;Instead of a workflow that looks like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;you write code,&lt;/li&gt;
&lt;li&gt;you run tests,&lt;/li&gt;
&lt;li&gt;you debug failures manually,&lt;/li&gt;
&lt;li&gt;you repeat,&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;you get a workflow graph where an agent can traverse steps automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;determine the scope,&lt;/li&gt;
&lt;li&gt;open the relevant files,&lt;/li&gt;
&lt;li&gt;modify code,&lt;/li&gt;
&lt;li&gt;run targeted tests,&lt;/li&gt;
&lt;li&gt;read the failure output,&lt;/li&gt;
&lt;li&gt;change strategy,&lt;/li&gt;
&lt;li&gt;re-run until it meets acceptance criteria.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the concrete example that tends to land: a PR request comes in with “tests are failing in CI.” An agent can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;reproduce locally,&lt;/li&gt;
&lt;li&gt;run the smallest failing test subset,&lt;/li&gt;
&lt;li&gt;locate the exact failing assertion,&lt;/li&gt;
&lt;li&gt;patch the underlying logic,&lt;/li&gt;
&lt;li&gt;re-run the full suite,&lt;/li&gt;
&lt;li&gt;update any affected snapshots or docs,&lt;/li&gt;
&lt;li&gt;and summarize what changed and why.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s not magic; it’s execution + feedback. And once you can rely on loops, you can start treating “test-driven development” as something more like “test-driven iteration,” where the iteration part is automated.&lt;/p&gt;
&lt;h2 id="real-toolsand-the-patterns-behind-them"&gt;Real tools—and the patterns behind them&lt;/h2&gt;
&lt;p&gt;Tools like Claude Code and GitHub Copilot CLI (plus various open-source alternatives) are already pushing this behavior into day-to-day development: code generation that can actually run commands, inspect errors, and try again. The names vary, but the architectural pattern is consistent:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context ingestion:&lt;/strong&gt; read your repo, relevant files, and recent changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action planning:&lt;/strong&gt; decide what commands and edits to attempt next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execution sandboxing:&lt;/strong&gt; run tests, linters, or build steps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observation:&lt;/strong&gt; capture logs and interpret failures.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State management:&lt;/strong&gt; keep track of what was tried and what worked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Termination criteria:&lt;/strong&gt; stop when the definition of done is met (tests pass, checks pass, or an explicit threshold is reached).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why the “assistant vs agent” distinction is more than marketing. An assistant can help with code snippets. An agent can operate on the repo as a living system.&lt;/p&gt;
&lt;h3 id="practical-advice-design-for-agent-friendliness"&gt;Practical advice: design for agent-friendliness&lt;/h3&gt;
&lt;p&gt;If you want agents to work well, your repo can’t be a black box. You’ll get better results by doing the unsexy hygiene work that also benefits humans:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ensure &lt;code&gt;make test&lt;/code&gt; / &lt;code&gt;npm test&lt;/code&gt; / &lt;code&gt;pytest&lt;/code&gt; runs quickly and deterministically.&lt;/li&gt;
&lt;li&gt;Keep scripts documented and discoverable.&lt;/li&gt;
&lt;li&gt;Prefer meaningful error messages (fail fast, don’t swallow stack traces).&lt;/li&gt;
&lt;li&gt;Make the CI output legible enough for an agent to interpret (clear failing test names, useful logs).&lt;/li&gt;
&lt;li&gt;Add “golden” commands in your docs: “run these to reproduce.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it as DevEx for machines.&lt;/p&gt;
&lt;h2 id="the-devops-analogy-is-real-and-useful"&gt;The DevOps analogy is real (and useful)&lt;/h2&gt;
&lt;p&gt;Here’s the opinionated truth: managing AI agents is starting to look like DevOps container management. You’re not just “using a tool”—you’re orchestrating a fleet.&lt;/p&gt;
&lt;p&gt;In DevOps, you learned to ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What runtime environment?&lt;/li&gt;
&lt;li&gt;What permissions?&lt;/li&gt;
&lt;li&gt;What observability?&lt;/li&gt;
&lt;li&gt;What happens when it fails?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With agents, you need the same instincts.&lt;/p&gt;
&lt;h3 id="permissions-and-blast-radius"&gt;Permissions and blast radius&lt;/h3&gt;
&lt;p&gt;A coding agent that can edit your entire repo is powerful. It should also be constrained.&lt;/p&gt;
&lt;p&gt;Practical guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run in a controlled environment (container, ephemeral workspace, or at least isolated branch).&lt;/li&gt;
&lt;li&gt;Use least privilege: don’t let the agent access secrets it doesn’t need.&lt;/li&gt;
&lt;li&gt;Require changes go through a PR flow, not direct merges.&lt;/li&gt;
&lt;li&gt;Make the agent operate on a branch and let CI be the final arbiter.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="observability-logs-are-your-ground-truth"&gt;Observability: logs are your ground truth&lt;/h3&gt;
&lt;p&gt;Agents will sometimes “sound confident” while making incorrect assumptions. Your defense is instrumentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Capture the commands the agent ran.&lt;/li&gt;
&lt;li&gt;Store diffs it attempted.&lt;/li&gt;
&lt;li&gt;Save the failing logs it used to decide next steps.&lt;/li&gt;
&lt;li&gt;Track iteration counts to prevent runaway loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you can review an agent’s decision trail, you can improve prompts, tighten constraints, and debug failures like any other automation.&lt;/p&gt;
&lt;h2 id="how-developers-adapt-shift-from-craft-to-orchestration"&gt;How developers adapt: shift from craft to orchestration&lt;/h2&gt;
&lt;p&gt;Let’s address the fear head-on: yes, some tasks will shrink. The junior developer who spends their day writing boilerplate and chasing simple failures will feel it first. But the bigger transformation is that developer skill shifts upward.&lt;/p&gt;
&lt;p&gt;Instead of “I can write the code,” the new differentiator is “I can direct an automated workflow to the correct outcome.”&lt;/p&gt;
&lt;p&gt;That includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;writing clear acceptance criteria (“tests pass,” “lint is clean,” “performance regression not introduced”),&lt;/li&gt;
&lt;li&gt;decomposing work into an agent-executable plan,&lt;/li&gt;
&lt;li&gt;shaping prompts to reduce ambiguity,&lt;/li&gt;
&lt;li&gt;curating the context an agent uses,&lt;/li&gt;
&lt;li&gt;and reviewing results with an expert eye.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever reviewed a teammate’s PR, you already have the muscle. AI just changes the ratio: you’ll review more, but the changes will often be more mechanical—and the stakes are higher because automation can produce large diffs quickly.&lt;/p&gt;
&lt;h3 id="concrete-example-directing-an-agent-to-fix-a-regression-safely"&gt;Concrete example: directing an agent to fix a regression safely&lt;/h3&gt;
&lt;p&gt;Suppose a service starts failing with a new error after a dependency update. Instead of saying “fix it,” you can instruct:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Reproduce failure locally.”&lt;/li&gt;
&lt;li&gt;“Run integration tests first; only run full suite if they pass.”&lt;/li&gt;
&lt;li&gt;“Patch the smallest surface area to restore behavior.”&lt;/li&gt;
&lt;li&gt;“Add or update tests that cover the regression.”&lt;/li&gt;
&lt;li&gt;“Stop when CI checks pass and include a summary of changes.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not micromanaging. You’re setting a safe boundary and a stop condition.&lt;/p&gt;
&lt;p&gt;That’s orchestration: giving the agent enough structure to be effective without turning it loose.&lt;/p&gt;
&lt;h2 id="the-hard-part-isnt-technicalits-organizational"&gt;The hard part isn’t technical—it’s organizational&lt;/h2&gt;
&lt;p&gt;The biggest challenge teams face isn’t whether agents can run tests. It’s whether your process can absorb autonomous iteration.&lt;/p&gt;
&lt;p&gt;Start small:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick one workflow step that’s repetitive and well-instrumented (e.g., fixing failing tests, updating documentation formatting, triaging build errors).&lt;/li&gt;
&lt;li&gt;Gate it behind CI and PRs.&lt;/li&gt;
&lt;li&gt;Measure outcomes with human review (time-to-merge, number of failed attempts, diff size).&lt;/li&gt;
&lt;li&gt;Expand scope once you trust the loop.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then align incentives. If an agent produces 20 commits for a single fix, developers will lose patience and revert to manual work. If the agent only runs the right commands and stops cleanly, adoption becomes a no-brainer.&lt;/p&gt;
&lt;p&gt;And don’t ignore policy. Decide what agents can do with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;external network access,&lt;/li&gt;
&lt;li&gt;dependency changes,&lt;/li&gt;
&lt;li&gt;secret handling,&lt;/li&gt;
&lt;li&gt;license-sensitive operations,&lt;/li&gt;
&lt;li&gt;and code ownership boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t bureaucracy—it’s how you keep velocity without burning trust.&lt;/p&gt;
&lt;h2 id="conclusion-your-job-isnt-disappearingyour-workflow-is-being-rewritten"&gt;Conclusion: your job isn’t disappearing—your workflow is being rewritten&lt;/h2&gt;
&lt;p&gt;AI agents aren’t coming for your job; they’re coming for your workflow. The shift from assistants to agents turns software work into a loop: plan, execute, observe, iterate. Developers who adapt will manage agent-powered automation the way DevOps engineers managed containers—confidently, with constraints, observability, and clear acceptance criteria.&lt;/p&gt;
&lt;p&gt;The ones who don’t will keep doing manually what their peers will accomplish in minutes. The only question left is whether you’ll be the person orchestrating the new architecture—or the person stuck doing the old work in the margins.&lt;/p&gt;</content></item><item><title>The Tech I'm Watching in 2025</title><link>https://decastro.work/blog/tech-watching-2025/</link><pubDate>Sun, 22 Dec 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/tech-watching-2025/</guid><description>&lt;p&gt;Every year has its hype cycles, but 2025 feels different: the conversation is shifting from “can we build cool demos?” to “can we ship reliable systems that keep working when the novelty wears off?” This is the year I’m watching for tools and frameworks that move from prototypes to infrastructure—especially in AI integration, developer productivity, and the boring parts of reliability nobody wants to own.&lt;/p&gt;
&lt;p&gt;Here’s my 2025 watchlist: AI agents that truly chain tasks autonomously, Anthropic’s Model Context Protocol as a universal integration layer, Zig gaining momentum as an accessible systems language, Effect-TS bringing algebraic effects to mainstream TypeScript, PostgreSQL’s pgvector pressuring standalone vector databases, and the perennial question: will Rust finally break into mainstream web development?&lt;/p&gt;</description><content>&lt;p&gt;Every year has its hype cycles, but 2025 feels different: the conversation is shifting from “can we build cool demos?” to “can we ship reliable systems that keep working when the novelty wears off?” This is the year I’m watching for tools and frameworks that move from prototypes to infrastructure—especially in AI integration, developer productivity, and the boring parts of reliability nobody wants to own.&lt;/p&gt;
&lt;p&gt;Here’s my 2025 watchlist: AI agents that truly chain tasks autonomously, Anthropic’s Model Context Protocol as a universal integration layer, Zig gaining momentum as an accessible systems language, Effect-TS bringing algebraic effects to mainstream TypeScript, PostgreSQL’s pgvector pressuring standalone vector databases, and the perennial question: will Rust finally break into mainstream web development?&lt;/p&gt;
&lt;h2 id="1-ai-agents-that-can-chain-tasks-without-faceplanting"&gt;1) AI agents that can chain tasks without faceplanting&lt;/h2&gt;
&lt;p&gt;The headline in 2025 isn’t “AI.” It’s &lt;em&gt;execution&lt;/em&gt;. The agents I’m watching are those that can take a goal, decompose it into steps, call tools, check their work, and continue until completion—without constantly asking the user what to do next.&lt;/p&gt;
&lt;p&gt;What separates real agents from toy demos? Three things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) Deterministic control around non-deterministic models.&lt;/strong&gt;&lt;br&gt;
If an agent delegates everything to a model, it will eventually drift, loop, or hallucinate tool parameters. The stronger pattern is to keep the model responsible for &lt;em&gt;reasoning and selection&lt;/em&gt;, while the system owns &lt;em&gt;workflow and state&lt;/em&gt;. For example: represent tasks as a graph, track completed steps, and enforce “only call tool X with schema Y.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Tool contracts and validation.&lt;/strong&gt;&lt;br&gt;
A practical agent treats tools like APIs, not suggestions. Use strict schemas for inputs/outputs; validate responses; reject malformed results; and have fallback strategies. If your agent can’t parse a JSON response from a tool reliably, it doesn’t get to proceed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) Persistent memory with guardrails.&lt;/strong&gt;&lt;br&gt;
Agents need context—previous steps, user preferences, project state. But memory shouldn’t become a dumping ground. In practice, you’ll want layered context: session state (ephemeral), task state (durable for the current run), and user profile (long-lived). And you should be able to inspect or clear it when things go wrong.&lt;/p&gt;
&lt;p&gt;A concrete example: an “ops assistant” agent that handles incident triage. In a good design, it doesn’t just ask ChatGPT to “look at logs.” It:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pulls recent errors from your log tool (via a tool call).&lt;/li&gt;
&lt;li&gt;Extracts key symptoms (model reasoning).&lt;/li&gt;
&lt;li&gt;Queries runbooks (retrieval tool).&lt;/li&gt;
&lt;li&gt;Proposes a short plan with explicit steps and expected outcomes.&lt;/li&gt;
&lt;li&gt;Executes safe actions (restart service, increase log level) with confirmation gates.&lt;/li&gt;
&lt;li&gt;Writes a postmortem draft with sources.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In 2025, I’m placing bets on agents that &lt;em&gt;earn trust through control&lt;/em&gt;, not vibes.&lt;/p&gt;
&lt;h2 id="2-mcp-the-integration-layer-that-could-replace-the-glue-code"&gt;2) MCP: the integration layer that could replace the glue code&lt;/h2&gt;
&lt;p&gt;If agents are the execution engine, the integration layer is the plumbing. That’s why I’m watching Anthropic’s Model Context Protocol (MCP) as the most promising “universal adapter” approach in the AI tooling ecosystem.&lt;/p&gt;
&lt;p&gt;The core idea is simple: define a standard way for model clients to connect to external tools and data sources through consistent capabilities and message semantics. In other words, MCP aims to reduce the “N adapters for N tools” problem—where every new model or agent framework forces you to rewrite integrations.&lt;/p&gt;
&lt;p&gt;Here’s what MCP changes in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool portability.&lt;/strong&gt; A tool exposed via MCP becomes available to multiple clients without bespoke wiring each time. If you’ve ever built a custom connector to a single agent framework, you know how fast that turns into maintenance debt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner separation of concerns.&lt;/strong&gt; Your “tool server” focuses on exposing capabilities, authentication, and data shaping. Your “agent” focuses on orchestration and reasoning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable context.&lt;/strong&gt; Instead of stuffing everything into prompts, clients can request structured context from multiple providers with less chaos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building with an eye toward 2025 stability, don’t just evaluate MCP as a protocol—evaluate it as an &lt;em&gt;organizational strategy&lt;/em&gt;. Start by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Selecting 2–3 high-value integrations you already use (e.g., ticketing, docs, internal APIs).&lt;/li&gt;
&lt;li&gt;Exposing them behind MCP with strict schemas.&lt;/li&gt;
&lt;li&gt;Plugging them into one agent workflow end-to-end.&lt;/li&gt;
&lt;li&gt;Measuring the reduction in integration churn across framework changes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My bet: MCP becomes a default path for integrations, not because it’s trendy, but because “standard adapters” win over time.&lt;/p&gt;
&lt;h2 id="3-zigs-momentum-systems-programming-you-can-actually-approach"&gt;3) Zig’s momentum: systems programming you can actually approach&lt;/h2&gt;
&lt;p&gt;Zig has been inching forward for years, but what I’m watching in 2025 is whether it becomes a &lt;em&gt;default choice for production-adjacent systems components&lt;/em&gt;—not just a hobbyist darling.&lt;/p&gt;
&lt;p&gt;What’s the practical appeal? Zig sits in an interesting middle ground: lower-level control without the complexity tax that often comes with C/C++ toolchains, and a growing ecosystem that’s good enough for serious work. The language also encourages explicitness: when you care about memory, error handling, or build behavior, Zig tends to make that visible rather than hiding it behind conventions.&lt;/p&gt;
&lt;p&gt;Where I think Zig can land in mainstream engineering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance-critical services&lt;/strong&gt; that need predictable behavior and minimal runtime overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI tools&lt;/strong&gt; and infrastructure utilities where correctness and deployability matter more than flashy abstraction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interop layers&lt;/strong&gt; between languages (think: “a small fast core” with a stable API surface).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete “reasonable Zig project” in 2025: a fast log processor that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Streams files or stdin.&lt;/li&gt;
&lt;li&gt;Parses structured logs with minimal allocations.&lt;/li&gt;
&lt;li&gt;Outputs normalized JSON for downstream indexing.&lt;/li&gt;
&lt;li&gt;Produces stable exit codes and error messages for automation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you can wrap that as a standalone binary with predictable cross-compilation, you’ll feel Zig’s value immediately. The test is not whether Zig can do everything—it’s whether it can do the kind of work teams keep shipping every week.&lt;/p&gt;
&lt;h2 id="4-effect-ts-in-typescript-algebraic-effects-for-the-rest-of-us"&gt;4) Effect-TS in TypeScript: algebraic effects for the rest of us&lt;/h2&gt;
&lt;p&gt;TypeScript is the default language for a reason: it’s pragmatic and reachable. But error handling, side effects, retries, timeouts, and cancellation can turn even clean codebases into spaghetti. Effect-TS is one of the more compelling attempts to fix that, bringing ideas from algebraic effects into the TypeScript world.&lt;/p&gt;
&lt;p&gt;I’m watching Effect-TS in 2025 because it tackles a real pain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Error handling becomes consistent and composable.&lt;/li&gt;
&lt;li&gt;Side effects become explicit and testable.&lt;/li&gt;
&lt;li&gt;Resource management (like acquiring/releasing) can be modeled rather than “remembered.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical framing: instead of sprinkling try/catch and ad-hoc retry logic across your app, you build workflows where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each effect (HTTP call, DB access, cache lookup) is modeled explicitly.&lt;/li&gt;
&lt;li&gt;Dependencies are injected through the effect environment.&lt;/li&gt;
&lt;li&gt;You can test a workflow by swapping real effects with deterministic test doubles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common integration scenario: a checkout-like flow that needs to call multiple services with strict rules. With effect-style programming, you can express “if payment fails, don’t attempt fulfillment; if fulfillment fails, trigger compensation; always release resources.” That’s the kind of logic that otherwise becomes fragile across a tangle of promises and control flags.&lt;/p&gt;
&lt;p&gt;If you’re curious, don’t start by rewriting your whole app. Start by isolating one workflow with meaningful side effects—say, “create order + reserve inventory + confirm payment”—and model it using Effect-TS patterns. The goal isn’t purity. It’s reducing the number of ways your system can fail silently.&lt;/p&gt;
&lt;h2 id="5-pgvector-pressures-standalone-vector-databases"&gt;5) pgvector pressures standalone vector databases&lt;/h2&gt;
&lt;p&gt;Vector search is no longer a novelty feature; it’s showing up everywhere—from semantic search in docs to recommendation systems and retrieval-augmented generation. The question for 2025 is whether teams need a separate vector database, or whether the mainstream database stack can carry the load.&lt;/p&gt;
&lt;p&gt;That’s where PostgreSQL’s pgvector is interesting. The practical advantage of using a database you already run is operational simplicity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One authentication and access pattern.&lt;/li&gt;
&lt;li&gt;One backup/restore story.&lt;/li&gt;
&lt;li&gt;One schema migration workflow.&lt;/li&gt;
&lt;li&gt;One place to reason about queries and data consistency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Does that mean standalone vector databases disappear? Not necessarily. There are workloads where a dedicated system makes sense—especially at high scale or where specialized indexing and retrieval features offer real benefits.&lt;/p&gt;
&lt;p&gt;But the momentum I’m watching is different: more teams will prototype and ship vector-backed features using pgvector because it lowers the barrier to production. If you can do semantic search with PostgreSQL tables, and your latency/throughput needs aren’t extreme, you avoid the “second operational universe.”&lt;/p&gt;
&lt;p&gt;A concrete decision rule for 2025:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your data already lives in Postgres, and your retrieval requirements are within reasonable bounds, start with pgvector.&lt;/li&gt;
&lt;li&gt;If you later hit limitations (indexing cost, scaling strategy, or specialized filtering), migrate to a dedicated engine with a clean abstraction around the retrieval interface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how you prevent vector search from becoming a permanent architectural tax.&lt;/p&gt;
&lt;h2 id="6-will-rust-break-into-mainstream-web-development"&gt;6) Will Rust break into mainstream web development?&lt;/h2&gt;
&lt;p&gt;Rust has been “one year away” from mainstream web development for a long time. Still, I’m watching 2025 closely because the language is mature enough now that the conversation can shift from “can Rust do it?” to “can Rust do it with tolerable ergonomics and ecosystem support?”&lt;/p&gt;
&lt;p&gt;Mainstream web adoption hinges on a few factors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Framework and tooling maturity.&lt;/strong&gt; Developers need a comfortable path for routing, middleware, templating or APIs, and deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ergonomics for the web’s reality:&lt;/strong&gt; async IO, streaming bodies, cookies, auth, and JSON handling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interoperability.&lt;/strong&gt; Web stacks rarely live in isolation; they integrate with Node tooling, CI systems, and sometimes legacy services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What I’d look for in 2025 isn’t a universal Rust takeover. It’s more nuanced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More production deployments of Rust backends in teams that value reliability and performance.&lt;/li&gt;
&lt;li&gt;Better “batteries included” workflows that reduce friction for newcomers.&lt;/li&gt;
&lt;li&gt;Clear patterns for integrating Rust services with existing TypeScript frontends and infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My bet? Rust continues to grow where correctness and performance matter and where teams are willing to invest in a stable backend foundation—even if it never becomes the default for every web app. But it &lt;em&gt;could&lt;/em&gt; become common enough that “Rust backend” stops sounding like a contrarian move.&lt;/p&gt;
&lt;h2 id="conclusion-the-2025-winners-will-feel-boring-in-the-best-way"&gt;Conclusion: the 2025 winners will feel boring in the best way&lt;/h2&gt;
&lt;p&gt;The most exciting tech in 2025 isn’t the flashiest feature—it’s the infrastructure that makes systems dependable: agents that chain tasks with control, MCP that reduces integration sprawl, Zig that makes systems work approachable, Effect-TS that makes side effects manageable, pgvector that keeps vector search grounded in standard operations, and Rust continuing its slow march into everyday backend work.&lt;/p&gt;
&lt;p&gt;Place your bets. Just bet on the things that help you ship—and keep shipping—without rewriting your glue every time the ecosystem shifts.&lt;/p&gt;</content></item><item><title>2024: The Year AI Became Infrastructure</title><link>https://decastro.work/blog/2024-year-ai-became-infrastructure/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/2024-year-ai-became-infrastructure/</guid><description>&lt;p&gt;In 2024, AI didn’t “get smarter” in some magical, headline-grabbing way—it got &lt;em&gt;useful&lt;/em&gt; in a way that changed how teams build. The result: what used to be a demo became plumbing. The biggest shift wasn’t just technology; it was mindset. Developers stopped asking, “Can we do this with AI?” and started asking, “How do we operationalize it safely, cheaply, and reliably?”&lt;/p&gt;
&lt;h2 id="rag-stopped-being-a-research-idea-and-became-the-default-architecture"&gt;RAG stopped being a research idea and became the default architecture&lt;/h2&gt;
&lt;p&gt;If there was a single enterprise pattern that won in 2024, it was RAG—retrieval-augmented generation. Not because it’s perfect, but because it’s &lt;em&gt;accountable&lt;/em&gt;. When your AI answers from company documents, the system can be designed around citations, access controls, and retrieval policies. That matters when you’re moving from “chat” to “workflow.”&lt;/p&gt;</description><content>&lt;p&gt;In 2024, AI didn’t “get smarter” in some magical, headline-grabbing way—it got &lt;em&gt;useful&lt;/em&gt; in a way that changed how teams build. The result: what used to be a demo became plumbing. The biggest shift wasn’t just technology; it was mindset. Developers stopped asking, “Can we do this with AI?” and started asking, “How do we operationalize it safely, cheaply, and reliably?”&lt;/p&gt;
&lt;h2 id="rag-stopped-being-a-research-idea-and-became-the-default-architecture"&gt;RAG stopped being a research idea and became the default architecture&lt;/h2&gt;
&lt;p&gt;If there was a single enterprise pattern that won in 2024, it was RAG—retrieval-augmented generation. Not because it’s perfect, but because it’s &lt;em&gt;accountable&lt;/em&gt;. When your AI answers from company documents, the system can be designed around citations, access controls, and retrieval policies. That matters when you’re moving from “chat” to “workflow.”&lt;/p&gt;
&lt;p&gt;A practical way teams adopted RAG: they stopped treating it like a clever prompt trick and started treating it like a pipeline.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ingest&lt;/strong&gt;: chunk documents intelligently (respecting headings, tables, and sections), deduplicate, and tag content by tenant, domain, or product line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrieve&lt;/strong&gt;: use embeddings plus filtering (permissions, language, recency, customer segment). Retrieval isn’t just vector similarity; it’s “what’s allowed” and “what’s relevant.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate&lt;/strong&gt;: constrain the model to the retrieved context; require “I don’t know” behavior when retrieval is weak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate&lt;/strong&gt;: measure answer correctness against a test set tied to real tasks, not just “sounds good” examples.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The biggest win wasn’t the model’s output quality alone—it was the ability to improve outcomes by iterating on retrieval quality. In practice, teams got better by fixing boring stuff: chunk sizes, metadata quality, and query rewriting. That’s the infrastructure mindset: optimize the system, not the magic.&lt;/p&gt;
&lt;p&gt;And yes, RAG also made skepticism rational. If the answer is wrong, you can often locate the failure mode: missing documents, poor chunking, bad filters, stale indices, or a prompt that didn’t respect the context. That observability is a forcing function toward reliability.&lt;/p&gt;
&lt;h2 id="open-source-models-reached-good-enough-parity-for-many-jobs"&gt;Open-source models reached “good enough” parity for many jobs&lt;/h2&gt;
&lt;p&gt;2024 was also the year open-source models became normal business choices. Not universally—some edge cases still favor proprietary systems, especially where tooling, reliability guarantees, or niche capabilities are critical. But for the bulk of day-to-day tasks—summarization, classification, extraction, code assistance, customer support triage—many teams found the trade-offs no longer justify paying the “closed” tax.&lt;/p&gt;
&lt;p&gt;Here’s what “parity” looked like in real deployments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Document processing&lt;/strong&gt;: extract fields from messy text, normalize formats, and route results to downstream systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customer support&lt;/strong&gt;: classify intent, draft responses with a retrieval context, and enforce policy constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer productivity&lt;/strong&gt;: generate boilerplate, explain code, and propose diffs—then let tests and linting arbitrate correctness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical takeaway: open-source adoption wasn’t just about licensing. It was about owning the lifecycle. Teams could tune prompts, run evaluations in their own environments, and switch model versions without vendor drama.&lt;/p&gt;
&lt;p&gt;But infrastructure doesn’t mean “set it and forget it.” The teams that benefited most treated model serving like any other production dependency: versioned models, staged rollouts, latency budgets, and predictable cost controls. If you can’t measure it, you can’t trust it—especially when models evolve.&lt;/p&gt;
&lt;h2 id="vector-databases-moved-from-niche-to-necessarybecause-retrieval-became-mission-critical"&gt;Vector databases moved from niche to necessary—because retrieval became mission-critical&lt;/h2&gt;
&lt;p&gt;Once RAG became the default, vector storage stopped being a “nice to have” and started being a core dependency. In 2024, vector databases went mainstream not because they were trendy, but because retrieval requires persistence, indexing, and performance characteristics you can’t hack together forever.&lt;/p&gt;
&lt;p&gt;The infrastructure shift was visible in how teams evaluated vector DBs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Indexing and update behavior&lt;/strong&gt;: Can you handle incremental document updates without painful reindexing every time?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filtering support&lt;/strong&gt;: Permissions and tenancy aren’t optional. You need metadata-aware retrieval, not just similarity search.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency and throughput&lt;/strong&gt;: If your RAG system times out, the user experience suffers instantly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational tooling&lt;/strong&gt;: Observability, backups, migrations—things that matter when you have real traffic and real risk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common practical pattern: many teams used a hybrid approach even when they called it “RAG.” For instance, they relied on keyword search for exact matches and vector search for semantic similarity, then merged results with a reranker. The “vector database” was only one component of retrieval quality, but it became the backbone that made retrieval reliable at scale.&lt;/p&gt;
&lt;p&gt;And reliability exposed a hidden constraint: embeddings are not a stable artifact. Change the embedding model or chunking strategy and your “truth” changes. That forced teams to adopt better data versioning—another infrastructure hallmark.&lt;/p&gt;
&lt;h2 id="docker-kept-winning-quietlyand-ai-made-it-even-more-important"&gt;Docker kept winning quietly—and AI made it even more important&lt;/h2&gt;
&lt;p&gt;Docker’s story in 2024 wasn’t dramatic. It just worked—so teams kept using it. What AI changed is that containerization became the easiest way to manage a growing stack: embedding generation, retrieval services, reranking, model inference, evaluation harnesses, and observability.&lt;/p&gt;
&lt;p&gt;In practice, Docker helped teams standardize environments across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;developer machines&lt;/li&gt;
&lt;li&gt;staging pipelines&lt;/li&gt;
&lt;li&gt;production clusters&lt;/li&gt;
&lt;li&gt;ephemeral evaluation runs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever watched a demo collapse because “it works on my laptop,” you already know why this matters. AI systems amplify environment drift: model versions, CUDA dependencies, tokenization settings, and even subtle library differences can swing outputs.&lt;/p&gt;
&lt;p&gt;The infrastructure move: build a repeatable “AI service image,” then pin versions everywhere:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;base images&lt;/li&gt;
&lt;li&gt;Python/Node dependencies&lt;/li&gt;
&lt;li&gt;model binaries or serving endpoints&lt;/li&gt;
&lt;li&gt;embedding model versions&lt;/li&gt;
&lt;li&gt;prompt and retrieval configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Docker didn’t create these best practices—but it made them sustainable.&lt;/p&gt;
&lt;h2 id="postgresql-expanded-its-role-as-the-everything-databaseand-thats-not-an-accident"&gt;PostgreSQL expanded its role as the everything-database—and that’s not an accident&lt;/h2&gt;
&lt;p&gt;The “AI stack” is full of specialized tools, but 2024 reinforced a simple truth: operational data still belongs in operational databases. PostgreSQL won more mindshare as the center of gravity for teams who wanted fewer moving parts and better auditability.&lt;/p&gt;
&lt;p&gt;Where PostgreSQL fit particularly well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Document metadata and permissions&lt;/strong&gt;: tenants, roles, access rules, document lifecycle state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job queues and ingestion tracking&lt;/strong&gt;: status, retries, and backfills.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluation results&lt;/strong&gt;: prompts, contexts, model versions, and human feedback signals.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit logs&lt;/strong&gt;: what the system retrieved and why an answer was produced.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When teams paired PostgreSQL with vector storage, it looked less like “AI architecture cosplay” and more like sane engineering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Postgres tells you &lt;em&gt;what&lt;/em&gt; exists and &lt;em&gt;who can access it&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The vector database helps you &lt;em&gt;find similar content&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;The inference layer focuses on &lt;em&gt;generating&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Observability ties everything together.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That separation of concerns is the infrastructure philosophy. You keep state where it’s durable and queryable, and you use specialized systems where they’re genuinely superior.&lt;/p&gt;
&lt;h2 id="the-real-shift-skepticism-grewbecause-teams-demanded-infrastructure-grade-trust"&gt;The real shift: skepticism grew—because teams demanded infrastructure-grade trust&lt;/h2&gt;
&lt;p&gt;The most interesting part of 2024 wasn’t the tech. It was the mood. Developers became more skeptical even as adoption accelerated. That’s a good sign. Skepticism isn’t resistance; it’s quality control.&lt;/p&gt;
&lt;p&gt;You saw it in the questions that replaced early “AI hype” prompts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What’s the failure mode when retrieval misses?”&lt;/li&gt;
&lt;li&gt;“How do we prevent data leaks across tenants?”&lt;/li&gt;
&lt;li&gt;“Can we reproduce answers for debugging?”&lt;/li&gt;
&lt;li&gt;“What’s our cost per task at peak traffic?”&lt;/li&gt;
&lt;li&gt;“How do we evaluate improvements without gaming the metrics?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Healthy skepticism also showed up in how teams chose to integrate AI into products. Instead of replacing everything with chat, they embedded AI where it adds value with guardrails: drafting, summarizing, extracting, classifying, and assisting. They let deterministic systems handle irreversible decisions—and used AI for reversible, human-reviewed steps when stakes were high.&lt;/p&gt;
&lt;p&gt;The best teams treated AI like a living system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tests for retrieval and generation quality&lt;/li&gt;
&lt;li&gt;monitoring for drift and latency&lt;/li&gt;
&lt;li&gt;rollback strategies for model updates&lt;/li&gt;
&lt;li&gt;clear ownership for the pipeline components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s what infrastructure does. It turns uncertainty into manageability.&lt;/p&gt;
&lt;h2 id="conclusion-ai-became-infrastructure-by-earning-the-right-to-stay"&gt;Conclusion: AI became infrastructure by earning the right to stay&lt;/h2&gt;
&lt;p&gt;2024 marked the year AI stopped being an experiment and started being infrastructure—because teams demanded systems they could operate. RAG became standard because it connects answers to real data. Open-source models became viable because they delivered enough capability with more control. Vector databases became necessary because retrieval moved from “nice feature” to “core function.” Docker kept things reproducible. PostgreSQL kept things durable.&lt;/p&gt;
&lt;p&gt;And through it all, developer skepticism grew—not as a brake, but as a compass. The industry doesn’t need blind adoption. It needs engineering discipline. If 2024 taught anything, it’s that AI becomes infrastructure only when it’s trustworthy enough to be relied on.&lt;/p&gt;</content></item><item><title>Advent of Code 2024: Why Zig Is My Weapon of Choice This Year</title><link>https://decastro.work/blog/advent-of-code-2024-zig-weapon-choice/</link><pubDate>Thu, 28 Nov 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/advent-of-code-2024-zig-weapon-choice/</guid><description>&lt;p&gt;Every year I tell myself I’ll do Advent of Code “for fun,” and every year I end up turning it into a stress test for whatever language I’m learning. This year, that language is Zig—and after a few days of solutions, it’s already the most satisfying systems-programming journey I’ve had since first picking up Rust. Why? Because Zig lets me learn by doing the things I usually hand-wave: memory, performance, and compilation behavior. It doesn’t just &lt;em&gt;allow&lt;/em&gt; mistakes—it gives you the levers to correct them quickly.&lt;/p&gt;</description><content>&lt;p&gt;Every year I tell myself I’ll do Advent of Code “for fun,” and every year I end up turning it into a stress test for whatever language I’m learning. This year, that language is Zig—and after a few days of solutions, it’s already the most satisfying systems-programming journey I’ve had since first picking up Rust. Why? Because Zig lets me learn by doing the things I usually hand-wave: memory, performance, and compilation behavior. It doesn’t just &lt;em&gt;allow&lt;/em&gt; mistakes—it gives you the levers to correct them quickly.&lt;/p&gt;
&lt;p&gt;In short: if Rust is the safe-but-strict parent, Zig is the uncle who trusts you with power tools.&lt;/p&gt;
&lt;h2 id="the-moment-zig-clicked-for-me"&gt;The moment Zig “clicked” for me&lt;/h2&gt;
&lt;p&gt;I didn’t love Zig on day one. It’s not a “read-the-docs and feel smart” language. It’s a “ship the code and feel the consequences” language.&lt;/p&gt;
&lt;p&gt;Advent of Code is perfect for that. It’s the same pattern over and over:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parse some input.&lt;/li&gt;
&lt;li&gt;Build data structures (arrays, maps, graphs, bitsets).&lt;/li&gt;
&lt;li&gt;Run an algorithm with tight loops.&lt;/li&gt;
&lt;li&gt;Debug edge cases under time pressure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Zig, you do all of that directly. There’s no magic framework getting in the way, no hidden runtime doing the heavy lifting, and—crucially—no hand-wavy memory model. You see every allocation decision, every buffer lifetime, and every opportunity for efficiency.&lt;/p&gt;
&lt;p&gt;The first time I rewrote a parser to use a resizable buffer and an allocator you can actually reason about, it felt like I stopped “coding” and started &lt;em&gt;engineering&lt;/em&gt;. That’s the addiction.&lt;/p&gt;
&lt;h2 id="comptime-the-regular-zig-code-metaprogramming-superpower"&gt;Comptime: the “regular Zig code” metaprogramming superpower&lt;/h2&gt;
&lt;p&gt;Zig’s comptime is the headline feature for me, mostly because it’s not what I expected. I came in thinking it would be another macro system with a syntax tax. Instead, comptime is code execution at compile time—using the same language constructs you already know.&lt;/p&gt;
&lt;p&gt;That matters for Advent of Code because you constantly want small variations without rewriting everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different grid sizes&lt;/li&gt;
&lt;li&gt;Different neighbor rules&lt;/li&gt;
&lt;li&gt;Parsing strategies&lt;/li&gt;
&lt;li&gt;Precomputed lookup tables (like direction maps or transition tables)&lt;/li&gt;
&lt;li&gt;Specialized data structures for a known maximum size&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what this looks like in practice: suppose you’re working on a puzzle where you need a fixed-size 2D grid. You can write a generic grid routine that compiles into a specialized version for each grid size—without resorting to macro tricks or template gymnastics.&lt;/p&gt;
&lt;p&gt;Instead of “macro generates code,” you’re basically saying: “run this logic during compilation so the result can be baked into the program.” And because it’s normal Zig code, you can debug it as you develop it. The mental model is consistent, which is rare in metaprogramming.&lt;/p&gt;
&lt;p&gt;My favorite part is that comptime doesn’t force you to use it. You can start with a simple, runtime version and only move “up” to comptime when you actually need it. That’s how you learn the power without turning your early solutions into a compilation-themed art project.&lt;/p&gt;
&lt;h2 id="memory-management-thats-explicitbut-not-hostile"&gt;Memory management that’s explicit—but not hostile&lt;/h2&gt;
&lt;p&gt;Manual memory management is the thing people warn you about with C. Zig does not pretend manual memory management is “free.” It makes it explicit, but it doesn’t try to punish you for knowing what you’re doing.&lt;/p&gt;
&lt;p&gt;This is where Zig feels like the “learning-by-doing” language I always want. The allocator system is front and center: you pass an allocator into functions that need memory, you decide what to allocate, and you control lifetimes. If you leak, you’ll know. If you use memory incorrectly, the program can fail loudly—rather than letting undefined behavior quietly corrupt your confidence.&lt;/p&gt;
&lt;p&gt;Concretely, Advent of Code often wants a temporary structure: parse input into something, process it once, then discard it. In languages with heavy abstractions, you either allocate more than you need or fight the framework. In Zig, you can pick the right tool for the job.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use an arena allocator when you allocate many small objects during parsing and then discard them all at the end of the run.&lt;/li&gt;
&lt;li&gt;Use a general purpose allocator when you truly need allocations that come and go.&lt;/li&gt;
&lt;li&gt;Keep scratch buffers local so your algorithm stays readable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical pattern looks like this conceptually:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read input.&lt;/li&gt;
&lt;li&gt;Create an allocator.&lt;/li&gt;
&lt;li&gt;Parse into data structures that live for the duration of the solution.&lt;/li&gt;
&lt;li&gt;Compute answers.&lt;/li&gt;
&lt;li&gt;Free everything (or tear down the arena) in one predictable step.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s the whole point: memory is a first-class design element, not a hidden tax.&lt;/p&gt;
&lt;p&gt;And unlike C, you’re not left guessing which function “probably” frees memory. Zig asks you to be deliberate.&lt;/p&gt;
&lt;h2 id="cross-compilation-without-ritual"&gt;Cross-compilation without ritual&lt;/h2&gt;
&lt;p&gt;Advent of Code is computer science, but it’s also… platform friction. You often want to run and test solutions across your laptop, maybe a server, maybe a different target architecture if you’re feeling adventurous.&lt;/p&gt;
&lt;p&gt;Zig’s cross-compilation is one of the quiet advantages that makes it easier to keep coding instead of managing your environment. The workflow is straightforward: compile for a target in a single command rather than setting up a pile of toolchains and environment variables that you forget how to undo.&lt;/p&gt;
&lt;p&gt;In practice, this means I’m more willing to treat Zig as my “single-source of truth” across systems. I’m not just building a solution—I’m building a portable tool. Even if the puzzle runner is local only, that portability mindset is its own reward.&lt;/p&gt;
&lt;h2 id="systems-performance-you-can-actually-measure"&gt;Systems performance you can actually measure&lt;/h2&gt;
&lt;p&gt;Advent of Code doesn’t demand low-level performance in the way kernel development does, but it absolutely exposes inefficient choices. If your algorithm is sloppy, your runtime or memory will show it. If your parsing is wasteful, it compounds. If your data structures are a poor fit, you’ll feel it when you iterate on the fix.&lt;/p&gt;
&lt;p&gt;Zig encourages efficient structure without forcing you into “micro-optimizations forever.” The language gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predictable data layout for many common patterns.&lt;/li&gt;
&lt;li&gt;Clear control over allocations.&lt;/li&gt;
&lt;li&gt;The ability to write tight loops without contortions.&lt;/li&gt;
&lt;li&gt;Good opportunities to reduce overhead because you’re not fighting the runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also where Zig’s comptime can sneak in wins. Precomputing tables or specializing logic for known constants turns repeated work into compile-time computation. That’s the kind of optimization you can add after the solution is correct, not before—which is how optimization should work.&lt;/p&gt;
&lt;p&gt;My rule this year: first make it correct and readable, then tighten. Zig supports that workflow unusually well.&lt;/p&gt;
&lt;h2 id="how-i-approach-advent-of-code-in-zig-so-it-stays-fun"&gt;How I approach Advent of Code in Zig (so it stays fun)&lt;/h2&gt;
&lt;p&gt;If you try to “Zig everything” from day one, you can end up writing impressive code that still doesn’t solve the puzzle in time. The key is to use Zig’s strengths in the right order.&lt;/p&gt;
&lt;p&gt;Here’s my practical process:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with a clean parser.&lt;/strong&gt;&lt;br&gt;
Get from text to a structured representation fast. Use an allocator you can tear down at the end. Don’t optimize parsing until you need to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Choose data structures based on the puzzle shape.&lt;/strong&gt;&lt;br&gt;
If you’re doing grid traversal, keep it simple: arrays and coordinate math beat overengineering. If you’re doing frequency counts, map/set approaches can keep your reasoning clean.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use debug-friendly checks early.&lt;/strong&gt;&lt;br&gt;
Zig’s tooling and explicitness make it easier to localize errors. I’ll add temporary assertions that catch off-by-one mistakes in neighbor logic or index math before I waste an hour chasing phantom bugs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add comptime only when there’s repetition.&lt;/strong&gt;&lt;br&gt;
If the solution has repeated patterns with only small differences, that’s your cue. Specialize grid sizes, precompute transitions, or build lookup tables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tighten memory last.&lt;/strong&gt;&lt;br&gt;
If the solution works but allocates too much, switch parsing to an arena allocator or restructure buffers. This is often a small, high-impact change.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And yes, I still keep it sharp and readable. Zig rewards clarity, and you don’t win Advent of Code by producing unreadable wizardry—even if it’s tempting.&lt;/p&gt;
&lt;h2 id="conclusion-zig-makes-advent-of-code-feel-like-real-engineering"&gt;Conclusion: Zig makes Advent of Code feel like real engineering&lt;/h2&gt;
&lt;p&gt;Advent of Code is a strange tradition: it’s competitive, but it’s also educational. This year, Zig is my weapon of choice because it hits the sweet spot between power and understanding. You get explicit memory management without the constant anxiety of unsafe habits. You get comptime without turning your code into a macro labyrinth. And you get portability and performance with minimal ceremony.&lt;/p&gt;
&lt;p&gt;If you’ve been itching to learn systems programming the practical way—by building, debugging, and refining—Advent of Code in Zig is an unusually satisfying path. Rust made me careful. Zig is making me confident.&lt;/p&gt;</content></item><item><title>AI-Generated Code Is Creating a Testing Crisis Nobody Talks About</title><link>https://decastro.work/blog/ai-generated-code-testing-crisis/</link><pubDate>Sat, 16 Nov 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ai-generated-code-testing-crisis/</guid><description>&lt;p&gt;AI coding assistants can make you feel unstoppable: you ask for a feature, it ships in minutes, and your repo fills up with fresh commits that look—mostly—correct. But there’s a quieter problem happening in parallel: we’re generating code faster than we can verify it, and our testing practices are starting to lag behind. The result isn’t just more bugs. It’s a growing verification gap that slowly turns “shipping faster” into “breaking faster with confidence.”&lt;/p&gt;</description><content>&lt;p&gt;AI coding assistants can make you feel unstoppable: you ask for a feature, it ships in minutes, and your repo fills up with fresh commits that look—mostly—correct. But there’s a quieter problem happening in parallel: we’re generating code faster than we can verify it, and our testing practices are starting to lag behind. The result isn’t just more bugs. It’s a growing verification gap that slowly turns “shipping faster” into “breaking faster with confidence.”&lt;/p&gt;
&lt;p&gt;And the most dangerous part? Many teams aren’t noticing until it’s too late, because the failure mode doesn’t look like a dramatic outage. It looks like subtly declining test quality, mysteriously brittle behavior, and a rising backlog of “can’t reproduce” issues that only appear in production.&lt;/p&gt;
&lt;h2 id="the-hidden-math-output-is-rising-verification-isnt"&gt;The Hidden Math: Output Is Rising, Verification Isn’t&lt;/h2&gt;
&lt;p&gt;When teams adopt AI coding tools, the headline is obvious: you write more code, faster. Fewer blank pages. Less time wrestling with boilerplate. More completed tasks per sprint. That’s real productivity—at least up to the moment you ask the next question: &lt;em&gt;How much verification do those extra lines of code get?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In practice, verification rarely scales linearly. Test suites are expensive to maintain, flaky tests sap trust, and coverage alone can become a hollow metric. When engineers feel pressured to “keep up,” they do what humans do under time constraints: they focus on functionality and treat testing as something you sprinkle on later.&lt;/p&gt;
&lt;p&gt;The outcome is an asymmetry:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI increases production throughput&lt;/strong&gt; (more code written, faster).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teams often keep testing effort flat&lt;/strong&gt; (same number of tests, same review time, same CI budget).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test coverage can even decline&lt;/strong&gt; because the new code isn’t exercised, or because tests are rewritten less frequently than application logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how technical debt accelerates without anyone explicitly deciding to take it on. The debt isn’t just missing tests; it’s missing &lt;em&gt;confidence&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="why-llms-make-the-happy-path-look-like-the-whole-story"&gt;Why LLMs Make the Happy Path Look Like the Whole Story&lt;/h2&gt;
&lt;p&gt;AI coding assistants are optimized for plausibility and helpfulness, not adversarial thinking. That’s why their code tends to pass the tests we already have—and why it often fails at exactly the places we don’t test.&lt;/p&gt;
&lt;p&gt;Consider a common scenario: an LLM helps you implement a “discount” function for an e-commerce app.&lt;/p&gt;
&lt;p&gt;You might ask for something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apply 10% discount&lt;/li&gt;
&lt;li&gt;Cap discount at $50&lt;/li&gt;
&lt;li&gt;Handle null values gracefully&lt;/li&gt;
&lt;li&gt;Support percentages as integers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The assistant writes code that satisfies the spec and likely includes common branches. But if your existing test suite only checks “normal” inputs—positive prices, straightforward percentages, expected formatting—you’ve created a runway for the happy path to look solid while the edge cases quietly rot.&lt;/p&gt;
&lt;p&gt;LLMs are particularly prone to omissions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Boundary mistakes&lt;/strong&gt; (e.g., inclusive vs. exclusive ranges)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Empty inputs and malformed data&lt;/strong&gt; (e.g., &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, unexpected types)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large values and overflow behavior&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State transitions&lt;/strong&gt; (e.g., retries, idempotency, concurrent updates)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invariant violations&lt;/strong&gt; (e.g., “discounted total must never increase”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not that LLMs “don’t know edge cases.” It’s that they don’t &lt;em&gt;prioritize&lt;/em&gt; them. Their defaults are to produce code that fits typical patterns. Edge cases require a different kind of rigor—one that’s harder to automate with simple generation.&lt;/p&gt;
&lt;h2 id="the-verification-gap-when-bugs-stop-being-reproducible"&gt;The Verification Gap: When Bugs Stop Being Reproducible&lt;/h2&gt;
&lt;p&gt;A verification gap is what you get when your confidence model breaks. You start shipping code that is only partially validated, and the failures shift downstream.&lt;/p&gt;
&lt;p&gt;Here’s what it often looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CI passes more frequently&lt;/strong&gt;, but production reports increase.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bug reports become harder to reproduce&lt;/strong&gt;, because the failure depends on rare input combinations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regression testing becomes slower&lt;/strong&gt;, because each new workaround adds another “special case” that wasn’t specified or verified.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developers lose trust in the test suite&lt;/strong&gt;, either because it’s too slow or because it doesn’t catch what matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then a vicious loop begins: to keep velocity, teams relax coverage expectations or skip expensive tests. The test suite becomes less representative of reality, so confidence drops further. Meanwhile the AI keeps producing code at the speed of thought.&lt;/p&gt;
&lt;p&gt;If you want a simple diagnostic: look for trends in &lt;em&gt;test quality signals&lt;/em&gt;, not just &lt;em&gt;test counts&lt;/em&gt;. Are integration tests failing more often? Are unit tests covering fewer code paths? Are you adding new tests at a slower rate than new code? These are warning lights.&lt;/p&gt;
&lt;h2 id="make-tests-scale-with-code-property-based--mutation-testing"&gt;Make Tests Scale With Code: Property-Based + Mutation Testing&lt;/h2&gt;
&lt;p&gt;If AI is widening the gap, you need verification strategies that widen faster. Two techniques do that especially well: &lt;strong&gt;property-based testing&lt;/strong&gt; and &lt;strong&gt;mutation testing&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="property-based-testing-generate-edge-cases-automatically"&gt;Property-based testing: Generate edge cases automatically&lt;/h3&gt;
&lt;p&gt;Instead of writing one test for one input, property-based testing asks: &lt;em&gt;What should always be true?&lt;/em&gt; You then generate many inputs—often including nasty boundary cases—to try to falsify that property.&lt;/p&gt;
&lt;p&gt;Example (language-agnostic idea): for the discount function, define properties like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The discounted total is &lt;strong&gt;never greater&lt;/strong&gt; than the original total.&lt;/li&gt;
&lt;li&gt;The discount amount is &lt;strong&gt;never negative&lt;/strong&gt; and &lt;strong&gt;never exceeds the cap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;For any valid input, the result matches the agreed formula.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A property-based framework might try thousands of price/discount combinations, including zeros, extreme values, and unusual types, to find violations. This is exactly the kind of behavior that LLMs don’t reliably anticipate when writing only happy-path tests.&lt;/p&gt;
&lt;p&gt;The key is to write properties tied to invariants your system must obey, not to mirror the implementation.&lt;/p&gt;
&lt;h3 id="mutation-testing-measure-whether-tests-can-detect-wrong-code"&gt;Mutation testing: Measure whether tests can detect wrong code&lt;/h3&gt;
&lt;p&gt;Mutation testing works differently: it intentionally makes small, plausible changes to your code (mutations) and then checks whether your tests fail. If your tests still pass after meaningful mutations, that means the suite isn’t actually protecting you.&lt;/p&gt;
&lt;p&gt;This is where teams catch a common trap: “We have coverage” often means “we have coverage of code paths that aren’t sensitive to incorrect logic.” Mutation testing forces the question: &lt;em&gt;Would a real bug slip through?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Set targets thoughtfully. Start with a narrow scope (critical modules, pure functions, business rules), then expand.&lt;/p&gt;
&lt;h2 id="tighten-the-system-coverage-thresholds-that-dont-lie"&gt;Tighten the System: Coverage Thresholds That Don’t Lie&lt;/h2&gt;
&lt;p&gt;Coverage thresholds can be useful—but only if they measure something that correlates with risk. Many teams set a single global number and call it done. That encourages gaming and doesn’t address the real problem: new code entering the repo without adequate verification.&lt;/p&gt;
&lt;p&gt;Instead, adopt coverage rules that are hard to bypass:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Require minimum coverage for changed code&lt;/strong&gt;, not the entire repo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Differentiate unit vs. integration coverage&lt;/strong&gt;, and don’t pretend one substitute can replace the other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use branch coverage or mutation score where it matters&lt;/strong&gt;, especially for decision-heavy logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set stricter thresholds for AI-assisted modules&lt;/strong&gt; (business rules, parsing, validation, permissions)—the places where edge cases are expensive.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: when AI writes code, treat it like code you didn’t fully reason about. That means your review and testing expectations should be higher, not lower. If your team already struggles with test maintenance, AI is going to make that struggle worse unless you invest in scalable verification.&lt;/p&gt;
&lt;h2 id="operational-guardrails-review-ci-budgets-and-test-first-contracts"&gt;Operational Guardrails: Review, CI Budgets, and Test-First Contracts&lt;/h2&gt;
&lt;p&gt;The right response isn’t “ban AI code generation.” It’s to wrap it in a verification discipline that matches its speed.&lt;/p&gt;
&lt;p&gt;A strong, realistic workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Define test contracts up front.&lt;/strong&gt; For example, specify invariants for transformations and explicit parsing/validation behavior for inputs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Require tests for every AI-generated change that affects behavior.&lt;/strong&gt; Boilerplate helpers are one thing; business logic is another.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use CI to enforce verification budgets.&lt;/strong&gt; Property-based tests can be heavier, mutation tests can be slower—so run mutation tests on a schedule or only on critical paths, and tune input counts for property tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review for invariants and edge cases, not just correctness.&lt;/strong&gt; Your reviewer should ask, “What would break this in production?” not “Does it look right?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make test failures informative.&lt;/strong&gt; If your suite is flaky or noisy, developers will stop trusting it, and then you’re back to the same verification gap.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Concrete example: if AI generates a serializer/deserializer, require:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;round-trip properties (serialize then deserialize yields the original structure),&lt;/li&gt;
&lt;li&gt;invalid input behaviors (expected errors or safe handling),&lt;/li&gt;
&lt;li&gt;boundary handling (max/min sizes, encoding errors).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll catch issues that a handful of example-based tests miss—especially when the assistant’s default assumptions don’t match your real-world data.&lt;/p&gt;
&lt;h2 id="conclusion-speed-without-verification-isnt-productivity"&gt;Conclusion: Speed Without Verification Isn’t Productivity&lt;/h2&gt;
&lt;p&gt;AI-generated code is genuinely accelerating development. The problem is that “faster” is not the same thing as “safer,” and our testing systems haven’t adapted to the new throughput. LLMs optimize for the happy path; real systems live and die by the edges.&lt;/p&gt;
&lt;p&gt;To close the verification gap, pair AI coding with &lt;strong&gt;property-based testing&lt;/strong&gt; to explore input space, &lt;strong&gt;mutation testing&lt;/strong&gt; to validate test sensitivity, and &lt;strong&gt;stricter, change-aware coverage thresholds&lt;/strong&gt; that can’t be gamed. Then add operational guardrails so CI and review reinforce the discipline you want—not the shortcuts you’ll accidentally take.&lt;/p&gt;
&lt;p&gt;The future isn’t slower engineering. It’s verification that keeps pace.&lt;/p&gt;</content></item><item><title>Anthropic's Claude 3.5 Sonnet Is the Best Coding Model Nobody Expected</title><link>https://decastro.work/blog/claude-3-5-sonnet-best-coding-model/</link><pubDate>Sun, 10 Nov 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/claude-3-5-sonnet-best-coding-model/</guid><description>&lt;p&gt;For a while, the AI coding conversation sounded like a single question: &lt;em&gt;How close can we get to “universal” intelligence by scaling up?&lt;/em&gt; But in practice, developers don’t live in benchmarks. They live in tickets, pull requests, CI failures, and deadlines. And that’s why Claude 3.5 Sonnet quietly became the developer’s model of choice for code generation and analysis—despite not being the obvious “flagship” pick.&lt;/p&gt;
&lt;h2 id="the-mid-tier-model-that-shipped-the-goods"&gt;The “mid-tier” model that shipped the goods&lt;/h2&gt;
&lt;p&gt;In the typical model lineup narrative, “best” almost always means “biggest.” The marketing gravity goes to the headline model—more capability, more ambition, more compute. The smaller sibling is often treated like an economy option.&lt;/p&gt;</description><content>&lt;p&gt;For a while, the AI coding conversation sounded like a single question: &lt;em&gt;How close can we get to “universal” intelligence by scaling up?&lt;/em&gt; But in practice, developers don’t live in benchmarks. They live in tickets, pull requests, CI failures, and deadlines. And that’s why Claude 3.5 Sonnet quietly became the developer’s model of choice for code generation and analysis—despite not being the obvious “flagship” pick.&lt;/p&gt;
&lt;h2 id="the-mid-tier-model-that-shipped-the-goods"&gt;The “mid-tier” model that shipped the goods&lt;/h2&gt;
&lt;p&gt;In the typical model lineup narrative, “best” almost always means “biggest.” The marketing gravity goes to the headline model—more capability, more ambition, more compute. The smaller sibling is often treated like an economy option.&lt;/p&gt;
&lt;p&gt;Claude 3.5 Sonnet flipped that script for developers.&lt;/p&gt;
&lt;p&gt;The surprising part isn’t that Sonnet can code. Most modern frontier models can produce working code when you give them a clear prompt. The surprise is that Sonnet feels &lt;em&gt;operationally useful&lt;/em&gt;: it tends to hit the sweet spot where you can iterate quickly, get coherent reasoning, and avoid the “draft-your-own-architecture” problem that plagues weaker models.&lt;/p&gt;
&lt;p&gt;Developers discovered it the way they always discover good tools: not by reading a press release, but by running a real task end-to-end. “Can it refactor this without breaking things?” “Can it update the call sites and imports correctly?” “Will it suggest a test strategy instead of dumping random assertions?” Sonnet answered those questions with fewer follow-ups than many expected from a non-flagship model.&lt;/p&gt;
&lt;h2 id="why-speed-and-cost-matter-more-than-you-think"&gt;Why speed and cost matter more than you think&lt;/h2&gt;
&lt;p&gt;Coding with AI is a feedback loop. You don’t just want one perfect output—you want a dialogue that converges quickly. In that loop, time and cost are not “secondary concerns.” They determine whether you can actually use the model repeatedly, throughout the day, on the kinds of problems that show up in production engineering.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Refactoring work&lt;/strong&gt; rarely fits in one prompt. You ask for a change, you review the diff, you catch a logic edge case, you re-prompt with constraints (“preserve behavior,” “keep API stable,” “update tests accordingly”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test generation&lt;/strong&gt; often needs iteration. You’ll accept an initial test suite, then realize you need more coverage for a boundary case, or you want to switch from integration tests to unit tests for determinism.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Codebase explanation&lt;/strong&gt; is not a one-shot activity. You ask the model to summarize a module, then you ask follow-ups: “Show me the data flow,” “Where is the authorization enforced?” “What happens on retry?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a model is “good enough,” it becomes easier to do more turns. And when you can afford more turns, you get higher-quality final results. In the end, “best” becomes: &lt;em&gt;best at solving your real problems with the resources you actually have.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Sonnet landed there—strong enough to do serious coding work, fast enough to iterate without friction, and priced in a way that encourages developer usage rather than experimental dabbling.&lt;/p&gt;
&lt;h2 id="complex-refactoring-the-difference-between-code-and-a-change"&gt;Complex refactoring: the difference between code and a change&lt;/h2&gt;
&lt;p&gt;A lot of models can generate code snippets. Fewer models can perform refactoring in a way that feels like a developer touching a live codebase.&lt;/p&gt;
&lt;p&gt;Claude 3.5 Sonnet’s standout behavior in refactoring is its tendency to reason about the change as a system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It identifies what needs to be updated (call sites, types, interfaces, error handling).&lt;/li&gt;
&lt;li&gt;It preserves invariants (data formats, side effects, contract semantics).&lt;/li&gt;
&lt;li&gt;It accounts for knock-on effects (tests, mocks, configuration).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical refactoring request might be:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Extract this validation logic into a dedicated module. Keep the public API unchanged. Update any tests that depend on old error messages.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In many models, this turns into a partial rewrite: a new module appears, but error messages drift, tests still expect the old behavior, or subtle semantics change. Sonnet more reliably produces refactors that “close the loop”—a change that includes the scaffolding and verification needed to merge safely.&lt;/p&gt;
&lt;p&gt;The practical takeaway: when you refactor, ask for &lt;em&gt;the full transformation&lt;/em&gt;, not just the new function. Include requirements like “preserve behavior,” “update tests,” and “keep interfaces stable.” The best models will follow through.&lt;/p&gt;
&lt;h2 id="tests-that-dont-feel-like-an-afterthought"&gt;Tests that don’t feel like an afterthought&lt;/h2&gt;
&lt;p&gt;Generating tests is where model competence becomes visible. A weak model writes tests that are either too shallow (“assert true”) or overly brittle (hard-coding irrelevant details). A strong model produces tests that map to behavior and edge cases.&lt;/p&gt;
&lt;p&gt;Sonnet tends to do something valuable: it treats tests as part of the design.&lt;/p&gt;
&lt;p&gt;For example, if you ask it to implement a feature—say, adding idempotency keys to an API—Sonnet doesn’t just write the endpoint handler. It typically proposes a test plan that covers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Happy path behavior&lt;/strong&gt; (first request succeeds, subsequent duplicates are handled correctly)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt; (missing or malformed keys, TTL expiration behavior)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure modes&lt;/strong&gt; (database constraints, retries, concurrency considerations at the application level)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration boundaries&lt;/strong&gt; (what should be mocked vs. exercised end-to-end)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when you don’t explicitly request a testing strategy, the model often generates tests that correspond to the &lt;em&gt;meaning&lt;/em&gt; of the feature, not just its syntax. That’s exactly what developers need when they’re trying to land changes quickly without turning QA into a guessing game.&lt;/p&gt;
&lt;p&gt;Practical advice: prompt for tests in the same style you write them. If your team uses Jest, pytest, or Go’s testing package, specify the framework and conventions. Then add one sentence: “Write tests that would catch regressions in behavior, not implementation details.” It forces the model to aim at correctness rather than imitation.&lt;/p&gt;
&lt;h2 id="explaining-codebases-with-coherent-reasoning"&gt;Explaining codebases with coherent reasoning&lt;/h2&gt;
&lt;p&gt;Another underrated coding use case is analysis: understanding code you didn’t write. This is where many AI outputs become “helpful-sounding” but shallow—summaries that don’t actually answer the questions you care about.&lt;/p&gt;
&lt;p&gt;Sonnet’s strength shows up when you use it like an engineering partner, not a search engine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What is the data flow from request to response?”&lt;/li&gt;
&lt;li&gt;“Where is permission checked, and how is it enforced?”&lt;/li&gt;
&lt;li&gt;“Which functions mutate state, and under what conditions?”&lt;/li&gt;
&lt;li&gt;“If I add this feature flag, what are the minimal changes?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When it works well, you get a narrative you can follow—one that maps responsibilities to locations in the codebase. You can sanity-check it against what you see in files, then ask targeted follow-ups.&lt;/p&gt;
&lt;p&gt;If you’ve ever used a model that “explains” by listing functions, you know how frustrating that is. A coherent explanation should help you predict behavior. Sonnet’s outputs often feel like they’re aiming at that prediction, not just summarization.&lt;/p&gt;
&lt;p&gt;A practical workflow that works well: ask for a brief explanation first, then request a diagram-like breakdown in text (components → inputs → transformations → outputs). Finally, ask where the critical invariants live. You’ll get fewer meandering answers and more actionable guidance.&lt;/p&gt;
&lt;h2 id="the-real-lesson-best-is-the-time-cost-tradeoff-that-wins"&gt;The real lesson: “best” is the time-cost tradeoff that wins&lt;/h2&gt;
&lt;p&gt;The industry’s loudest instinct is to chase the largest model. But developers don’t optimize for theoretical capability—they optimize for throughput, iteration speed, and merge safety.&lt;/p&gt;
&lt;p&gt;A flagship model can be excellent, but it may come with constraints that matter in daily work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slower response times that break your momentum&lt;/li&gt;
&lt;li&gt;higher per-request costs that limit experimentation&lt;/li&gt;
&lt;li&gt;output styles that require more editing to integrate cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sonnet’s success suggests a clearer rule of thumb: the “best” coding model is the one that reliably completes the loop—generate, review, modify, test—within the constraints of real engineering time.&lt;/p&gt;
&lt;p&gt;In other words, the winning model is often not the most impressive. It’s the one that makes the engineer feel faster &lt;em&gt;without sacrificing correctness&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="conclusion-pick-the-model-that-matches-your-workflow"&gt;Conclusion: pick the model that matches your workflow&lt;/h2&gt;
&lt;p&gt;Claude 3.5 Sonnet became the best coding model for many developers not because it was destined to be the headliner, but because it fit the real demands of coding work: speed for iteration, capability for non-trivial changes, and enough coherence to reduce back-and-forth.&lt;/p&gt;
&lt;p&gt;If you’re evaluating models for development use, don’t start with “which is smartest?” Start with “which will make me ship?” Run a few tasks that mirror your day—refactor with tests, implement a feature with edge cases, and explain an unfamiliar module. The model that wins will almost certainly be the one that handles your constraints, not just your ambitions.&lt;/p&gt;</content></item><item><title>The Container Security Landscape Every Developer Should Understand</title><link>https://decastro.work/blog/container-security-landscape-developers-understand/</link><pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/container-security-landscape-developers-understand/</guid><description>&lt;p&gt;If you write Dockerfiles, you’re already in the blast radius. Most teams treat container security like an operations problem—something for the platform team to “deal with.” But the truth is sharper: the image you build is the product you ship, and every security shortcut you take in the Dockerfile becomes an attack surface your users can’t afford.&lt;/p&gt;
&lt;p&gt;Let’s walk through the container security landscape in a developer-first way—what’s commonly broken, why it matters, and how to fix it with practical, repeatable patterns.&lt;/p&gt;</description><content>&lt;p&gt;If you write Dockerfiles, you’re already in the blast radius. Most teams treat container security like an operations problem—something for the platform team to “deal with.” But the truth is sharper: the image you build is the product you ship, and every security shortcut you take in the Dockerfile becomes an attack surface your users can’t afford.&lt;/p&gt;
&lt;p&gt;Let’s walk through the container security landscape in a developer-first way—what’s commonly broken, why it matters, and how to fix it with practical, repeatable patterns.&lt;/p&gt;
&lt;h2 id="start-with-the-reality-your-dockerfile-decides-the-threat-model"&gt;Start with the reality: your Dockerfile decides the threat model&lt;/h2&gt;
&lt;p&gt;Container security isn’t magic tooling; it’s a chain of decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What’s inside the image&lt;/strong&gt; (packages, binaries, shells, utilities)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What runs by default&lt;/strong&gt; (user, permissions, entrypoint behavior)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What gets baked in during build&lt;/strong&gt; (secrets, compilers, debug tools)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where dependencies come from&lt;/strong&gt; (base images, package repos, build scripts)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s exposed&lt;/strong&gt; (ports, network permissions, filesystem write access)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most insecure images share a few predictable traits:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;They run as root.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They rely on unverified or moving base images.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They include build tooling in production.&lt;/strong&gt; (compilers, package managers, shells)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They ship more than necessary&lt;/strong&gt;—extra packages, language runtimes, package caches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They lack scanning in the build pipeline&lt;/strong&gt;, so vulnerabilities survive until runtime.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These aren’t “ops mistakes.” They’re developer workflow mistakes.&lt;/p&gt;
&lt;p&gt;A good mental model: &lt;strong&gt;an image is immutable, but your build process isn’t.&lt;/strong&gt; The build process determines what immutability preserves.&lt;/p&gt;
&lt;h2 id="the-biggest-easy-win-stop-running-as-root"&gt;The biggest easy win: stop running as root&lt;/h2&gt;
&lt;p&gt;Running as root is a top-tier convenience tax. It usually happens because it “just works” during local development, and it tends to be copy-pasted into production.&lt;/p&gt;
&lt;p&gt;Instead, treat non-root as a baseline requirement.&lt;/p&gt;
&lt;h3 id="a-practical-dockerfile-pattern"&gt;A practical Dockerfile pattern&lt;/h3&gt;
&lt;p&gt;For many apps, you can do something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a dedicated user/group&lt;/li&gt;
&lt;li&gt;Ensure the application directory is owned by that user&lt;/li&gt;
&lt;li&gt;Switch to that user&lt;/li&gt;
&lt;li&gt;Avoid setuid surprises&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example (Node.js-style, but the pattern generalizes):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a non-root user like &lt;code&gt;appuser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy only what you need&lt;/li&gt;
&lt;li&gt;Ensure file permissions are correct before &lt;code&gt;USER appuser&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; &lt;span style="color:#e6db74"&gt;node:20-slim&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/app&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Create non-root user&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; groupadd -r appuser &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; useradd -r -g appuser appuser&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Install dependencies (keep it minimal)&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; package*.json ./&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; npm ci --omit&lt;span style="color:#f92672"&gt;=&lt;/span&gt;dev&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Copy application&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; . .&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Fix ownership&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;RUN&lt;/span&gt; chown -R appuser:appuser /app&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;USER&lt;/span&gt; &lt;span style="color:#e6db74"&gt;appuser&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;EXPOSE&lt;/span&gt; &lt;span style="color:#e6db74"&gt;3000&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CMD&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;node&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;server.js&amp;#34;&lt;/span&gt;]&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two developer tips here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don’t rely on Kubernetes SecurityContext alone.&lt;/strong&gt; It’s a safety net, not a replacement for correct image defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test permissions locally.&lt;/strong&gt; If your app needs to write to a directory (logs, temp files), create and chown those paths explicitly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re migrating an existing app, expect failures around filesystem write. Fix them directly in the image rather than granting root privileges “temporarily.”&lt;/p&gt;
&lt;h2 id="reduce-attack-surface-with-multi-stage-builds-and-minimal-runtimes"&gt;Reduce attack surface with multi-stage builds and minimal runtimes&lt;/h2&gt;
&lt;p&gt;A common anti-pattern is “install everything once, keep it forever.” Build tools shouldn’t ship in production—yet many images do exactly that: compilers, debuggers, and package managers included “just in case.”&lt;/p&gt;
&lt;h3 id="multi-stage-builds-the-developer-native-security-tool"&gt;Multi-stage builds: the developer-native security tool&lt;/h3&gt;
&lt;p&gt;Multi-stage builds let you compile/build in one stage and copy only runtime artifacts into the final image. That shrinks both the image size and the reachable tools an attacker could leverage.&lt;/p&gt;
&lt;p&gt;For example, a typical approach for compiled languages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stage 1: build your binary (include build dependencies)&lt;/li&gt;
&lt;li&gt;Stage 2: copy the binary into a lightweight runtime image&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you don’t adopt distroless immediately, multi-stage builds are a massive improvement because they reduce what an attacker gets.&lt;/p&gt;
&lt;h3 id="why-less-matters-beyond-size"&gt;Why “less” matters beyond size&lt;/h3&gt;
&lt;p&gt;Fewer packages means fewer known vulnerabilities, fewer ways to execute code, and fewer shells/utilities for post-exploitation. It also improves your scanning signal—fewer components, fewer false leads.&lt;/p&gt;
&lt;h2 id="prefer-distroless-or-at-least-slim-and-be-deliberate-about-base-images"&gt;Prefer distroless (or at least slim) and be deliberate about base images&lt;/h2&gt;
&lt;p&gt;Your base image is the foundation of your security posture. If you use an outdated base or a constantly changing tag, you inherit risk without noticing.&lt;/p&gt;
&lt;h3 id="use-pinned-digests-not-just-tags"&gt;Use pinned digests, not just tags&lt;/h3&gt;
&lt;p&gt;Tags like &lt;code&gt;latest&lt;/code&gt; and even semver tags can move over time. For deterministic builds, prefer &lt;strong&gt;pinning by digest&lt;/strong&gt; (the immutable identifier for an image).&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FROM python:3.12-slim&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FROM python@sha256:&amp;lt;digest&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t make vulnerabilities disappear, but it makes your build reproducible and your upgrades intentional.&lt;/p&gt;
&lt;h3 id="distroless-fewer-utilities-fewer-surprises"&gt;Distroless: fewer utilities, fewer surprises&lt;/h3&gt;
&lt;p&gt;Distroless images generally omit shells and package managers, which is exactly what you want for production. Attackers rely on tooling; distroless reduces that capability dramatically.&lt;/p&gt;
&lt;p&gt;The tradeoff is operational friction: debugging “inside the container” is harder. That’s a fair cost. Debugging should be done via logs, metrics, traces, and local reproduction—not by opening a shell in production.&lt;/p&gt;
&lt;p&gt;A practical compromise many teams adopt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Development&lt;/strong&gt;: full-featured images for convenience&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/testing&lt;/strong&gt;: minimal images for early feedback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production&lt;/strong&gt;: distroless (or very slim) with a hard non-root baseline&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="make-scanning-part-of-the-build-not-a-report-card"&gt;Make scanning part of the build, not a report card&lt;/h2&gt;
&lt;p&gt;If you run vulnerability scanning only after deployment, you’re creating a delayed feedback loop. Developers need fast, actionable signals when the Dockerfile and dependencies are still editable.&lt;/p&gt;
&lt;p&gt;Two tools commonly used for scanning are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Trivy&lt;/strong&gt; (often favored for simplicity and Docker-native workflows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snyk&lt;/strong&gt; (useful in ecosystems with strong dependency management features)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Regardless of tool choice, the principle is the same: &lt;strong&gt;scan the built image and fail fast on serious issues.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="what-baseline-looks-like-in-practice"&gt;What “baseline” looks like in practice&lt;/h3&gt;
&lt;p&gt;A reasonable workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build image in CI&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run image scan&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gate on severity thresholds&lt;/strong&gt; (be strict on critical/high; handle medium thoughtfully)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish scan artifacts&lt;/strong&gt; (so teams can trace what was scanned and when)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create remediation paths&lt;/strong&gt; (upgrade base image, rebuild with updated dependencies, or adjust package selection)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also: treat scanning output as a starting point, not an oracle. For example, a vulnerability in a package you don’t actually include is irrelevant—but an image with build tools and shells makes “maybe exploitable” a more dangerous category. That’s why minimizing the image matters before you even argue about scan results.&lt;/p&gt;
&lt;h2 id="secrets-and-build-time-tooling-the-silent-security-footguns"&gt;Secrets and build-time tooling: the silent security footguns&lt;/h2&gt;
&lt;p&gt;Containers are often blamed for runtime vulnerabilities, but a more subtle risk is &lt;strong&gt;secrets baked into images during build&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Common mistakes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copying &lt;code&gt;.env&lt;/code&gt; files into the image&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;ARG&lt;/code&gt; or &lt;code&gt;ENV&lt;/code&gt; for credentials&lt;/li&gt;
&lt;li&gt;Leaving package manager caches or build artifacts that contain tokens&lt;/li&gt;
&lt;li&gt;Running build steps with tools that persist into the final stage&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-rules-of-thumb"&gt;Practical rules of thumb&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never copy secret files into the image context.&lt;/strong&gt; Use proper secret injection mechanisms in your CI system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use multi-stage builds&lt;/strong&gt; so build-time tooling and artifacts don’t persist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t run &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;pip install&lt;/code&gt; with dev/test extras&lt;/strong&gt; unless you truly need them at runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean up caches&lt;/strong&gt; if your build pipeline or base image leaves them behind (and again: multi-stage is cleaner than cleanup heroics).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you must fetch private dependencies, do it in a way that doesn’t leave credentials in layers. The simplest posture is to ensure credentials don’t make it past the build stage—and ideally never into the final layer graph.&lt;/p&gt;
&lt;h2 id="oci-and-ecosystem-agnostic-security-your-dockerfile-travels"&gt;OCI and ecosystem-agnostic security: your Dockerfile travels&lt;/h2&gt;
&lt;p&gt;One reason this topic feels messy is that people conflate “security on Docker” with “security on Kubernetes.” The good news is that modern container standards push toward consistency.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;OCI image specification&lt;/strong&gt; makes the image format ecosystem-agnostic: your build output isn’t tied to a single platform. That means better Dockerfile practices—non-root defaults, minimal layers, pinned bases, clean runtime artifacts—carry forward regardless of whether you run on Kubernetes, Nomad, or another compatible runtime.&lt;/p&gt;
&lt;p&gt;In other words, you’re not just improving one deployment. You’re improving a portable artifact.&lt;/p&gt;
&lt;h2 id="conclusion-treat-image-authoring-as-part-of-secure-engineering"&gt;Conclusion: treat image authoring as part of secure engineering&lt;/h2&gt;
&lt;p&gt;Container security isn’t an ops afterthought. It’s a developer responsibility embedded in the Dockerfile, the build pipeline, and the dependency choices you automate.&lt;/p&gt;
&lt;p&gt;If you remember nothing else, adopt these baselines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run as non-root&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use multi-stage builds to keep build tooling out of production&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer minimal or distroless images&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin base images by digest for reproducibility&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scan images in CI and fail fast&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ensure secrets never make it into image layers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Write fewer “convenient” layers. Ship fewer utilities. Make your builds deterministic. You’ll reduce risk immediately—and you’ll make the rest of your security tooling far more effective.&lt;/p&gt;</content></item><item><title>Gleam Might Be the Language the BEAM VM Deserved All Along</title><link>https://decastro.work/blog/gleam-language-beam-vm-deserved/</link><pubDate>Wed, 23 Oct 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/gleam-language-beam-vm-deserved/</guid><description>&lt;p&gt;Erlang’s runtime is the kind of engineering that feels unfair: it’s been hammered by real-world concurrency for decades and keeps winning. But the developer experience around it has often been… an acquired taste. Gleam is the antidote—an ergonomically modern, statically typed language that targets the BEAM VM, with a syntax many developers will recognize immediately. If you like the reliability of Erlang but want code you can trust before you run it, Gleam is suddenly hard to ignore.&lt;/p&gt;</description><content>&lt;p&gt;Erlang’s runtime is the kind of engineering that feels unfair: it’s been hammered by real-world concurrency for decades and keeps winning. But the developer experience around it has often been… an acquired taste. Gleam is the antidote—an ergonomically modern, statically typed language that targets the BEAM VM, with a syntax many developers will recognize immediately. If you like the reliability of Erlang but want code you can trust before you run it, Gleam is suddenly hard to ignore.&lt;/p&gt;
&lt;h2 id="the-beam-reliability-built-into-the-substrate"&gt;The BEAM: reliability built into the substrate&lt;/h2&gt;
&lt;p&gt;Before you fall in love with any language surface syntax, it helps to respect the machine underneath. The BEAM VM is famous for concurrency and fault isolation. Processes are lightweight, message passing is straightforward, and supervision lets systems recover from failures without bringing the whole application down.&lt;/p&gt;
&lt;p&gt;That’s why the BEAM shows up in places where uptime is not a nice-to-have—it’s the product. WhatsApp’s messaging layer, parts of Discord’s services, and telecom workloads where reliability matters more than trendiness all benefit from this runtime model. The point isn’t that BEAM is “better” in some abstract ranking; it’s that it solves a class of problems extremely well: concurrent systems that must keep moving when components fail.&lt;/p&gt;
&lt;p&gt;Erlang, in turn, is the original BEAM native language. It exposes the runtime model directly. That’s a strength—and also why so many developers bump into friction early: the syntax can be terse to the point of feeling hostile, and the mental model of pattern matching and function heads is powerful but not always beginner-friendly.&lt;/p&gt;
&lt;p&gt;So you end up with a familiar pattern in the ecosystem: the runtime is a powerhouse, but writing production code is sometimes harder than it needs to be. That gap is exactly where Gleam enters.&lt;/p&gt;
&lt;h2 id="erlangs-runtime-erlangs-syntax-and-the-productivity-tax"&gt;Erlang’s runtime, Erlang’s syntax, and the productivity tax&lt;/h2&gt;
&lt;p&gt;Erlang gets results. Its process model is clean, its failure handling is disciplined, and its ecosystem matured by surviving real deployments. But if you’ve ever tried to write something moderately complex—say, a web API with validation, a multi-step state machine, or a data pipeline—you’ve likely felt the productivity tax of the language surface.&lt;/p&gt;
&lt;p&gt;Erlang code often reads like it’s optimized for the compiler’s comfort, not the developer’s attention. You can absolutely become fluent, but the learning curve is steep and unforgiving. You pay in readability, in refactoring safety, and in how quickly you can iterate.&lt;/p&gt;
&lt;p&gt;And then there’s the type story. Erlang’s dynamic typing is a feature of its time and a fit for its philosophy, but it shifts error discovery to runtime. That’s acceptable in some domains, but it’s not a great fit for teams that want to catch entire classes of bugs before integration testing ever begins.&lt;/p&gt;
&lt;p&gt;This is where Elixir helped. It made BEAM development feel mainstream: a friendlier syntax, the joy of pipelines, and a strong web ecosystem. But Elixir still largely relies on runtime checking. You can get pretty far with tests and contracts, and the community has built excellent tooling—but static types remain the missing safety net for many teams.&lt;/p&gt;
&lt;p&gt;Gleam’s pitch is simple: keep the BEAM strengths, add a modern type system, and make code feel pleasant to write and review.&lt;/p&gt;
&lt;h2 id="why-static-types-matter-on-beam-more-than-youd-think"&gt;Why static types matter on BEAM (more than you’d think)&lt;/h2&gt;
&lt;p&gt;In a concurrent system, correctness isn’t just “does this compile?” It’s also “does this message have the shape I expect?” and “will this branch return the kind of value other code relies on?”&lt;/p&gt;
&lt;p&gt;Dynamic types can handle this, but only if you’re disciplined about runtime validation. That means you often discover problems after deployment—or at least after you’ve exercised the relevant path in tests. Static types move those checks earlier, when it’s cheaper to fix them.&lt;/p&gt;
&lt;p&gt;Gleam doesn’t just add “types for show.” It aims to make types work for day-to-day programming:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Function signatures become contracts.&lt;/strong&gt; If your API handler returns &lt;code&gt;Result&lt;/code&gt;, callers must handle failures explicitly rather than assuming success.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern matching becomes safer.&lt;/strong&gt; You can encode invariants in types so that illegal states are unrepresentable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactoring becomes less terrifying.&lt;/strong&gt; When you change a data structure, the compiler points you to every place the change ripples.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical outcome is fewer production surprises. Not zero surprises—no tool can promise that—but fewer “we only saw this bug in the wild” moments.&lt;/p&gt;
&lt;h2 id="gleam-in-practice-readable-syntax-typed-results-sane-ergonomics"&gt;Gleam in practice: readable syntax, typed results, sane ergonomics&lt;/h2&gt;
&lt;p&gt;Gleam’s syntax will feel familiar if you’ve touched TypeScript or Rust. It’s readable, expression-oriented, and tends to keep the “shape” of your code visually obvious.&lt;/p&gt;
&lt;p&gt;Here’s a small example of modeling success/failure with explicit types. (Think of it as the kind of helper you’d use in a web handler or data pipeline.)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gleam" data-lang="gleam"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserError&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;MissingField&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;InvalidEmail&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;pub&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;fn&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;validate_email&lt;/span&gt;(email&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Result&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;UserError&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; email&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;contains&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;@&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Ok&lt;/span&gt;(email)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Error&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;InvalidEmail&lt;/span&gt;(email))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even without the rest of the project, you can see what the function does: it returns either a validated value or a specific error variant. In a dynamic language you’d typically return &lt;code&gt;nil&lt;/code&gt; or throw, and then your callers might or might not remember to check. In Gleam, your compiler becomes the annoying teammate who demands you handle failure cases.&lt;/p&gt;
&lt;p&gt;Now imagine turning that into a more complete flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parse input&lt;/li&gt;
&lt;li&gt;Validate fields&lt;/li&gt;
&lt;li&gt;Construct a domain type&lt;/li&gt;
&lt;li&gt;Return either a domain object or a typed error you can map to an HTTP response&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With static types, this chain can become self-documenting. You can refactor validation rules without losing confidence in how errors propagate.&lt;/p&gt;
&lt;h3 id="concurrency-and-processes-without-losing-type-safety"&gt;Concurrency and processes without losing type safety&lt;/h3&gt;
&lt;p&gt;Because Gleam compiles to BEAM bytecode, it can participate in the same fault-tolerant concurrency model that made Erlang legendary. The important difference is that the code you write can still be checked by a static type system.&lt;/p&gt;
&lt;p&gt;That means you can structure concurrent components—workers, supervisors, actors—while keeping your message shapes and invariants under control. In practice, that reduces the “mystery” aspect of distributed message passing. Your processes still fail and recover as designed, but you’re less likely to send the wrong thing and only discover it when a branch crashes at runtime.&lt;/p&gt;
&lt;p&gt;The BEAM ecosystem often emphasizes &lt;em&gt;designing for failure&lt;/em&gt;. Gleam pairs well with that philosophy: types don’t remove failure, they make failure less arbitrary.&lt;/p&gt;
&lt;h2 id="the-ecosystem-advantage-what-gleam-unlocks-for-teams"&gt;The ecosystem advantage: what Gleam unlocks for teams&lt;/h2&gt;
&lt;p&gt;The BEAM ecosystem is already strong, but the language you use affects who joins and how fast they become productive. Erlang’s syntax deters some engineers. Elixir welcomes many, but static types remain a gap for teams that want compile-time guarantees.&lt;/p&gt;
&lt;p&gt;Gleam occupies a sweet spot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It’s modern enough to feel approachable.&lt;/li&gt;
&lt;li&gt;It’s typed enough to make refactoring safer.&lt;/li&gt;
&lt;li&gt;It compiles to the same runtime primitives that power the most battle-tested systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters for business outcomes, not just developer aesthetics. Consider the typical pressures on a production team:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need to onboard developers quickly.&lt;/li&gt;
&lt;li&gt;You need to maintain and extend code without constant rewrites.&lt;/li&gt;
&lt;li&gt;You need confidence during deployments.&lt;/li&gt;
&lt;li&gt;You want to spend time building features, not chasing edge-case regressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Types improve the cost profile of all of these. They make code review sharper because the compiler has already filtered out many classes of mistakes. They also help when you introduce new contributors: reading a well-typed module is often easier than interpreting the implicit expectations of a dynamically typed one.&lt;/p&gt;
&lt;p&gt;And because Gleam targets the BEAM, you’re not trading away operational maturity. You’re getting a friendlier front door to the same backend strengths.&lt;/p&gt;
&lt;p&gt;If you’ve ever wished BEAM development had the “modern language feel” of today’s ecosystems—without giving up fault-tolerant concurrency—Gleam is essentially that wishlist, turned into code.&lt;/p&gt;
&lt;h2 id="building-with-gleam-a-pragmatic-path-for-production-use"&gt;Building with Gleam: a pragmatic path for production use&lt;/h2&gt;
&lt;p&gt;If you’re evaluating Gleam for real projects, the best strategy is to be pragmatic. Don’t try to boil the ocean.&lt;/p&gt;
&lt;p&gt;Start with the kind of code where types deliver immediate value:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;input validation&lt;/li&gt;
&lt;li&gt;domain modeling&lt;/li&gt;
&lt;li&gt;data transformation pipelines&lt;/li&gt;
&lt;li&gt;boundary logic (mapping errors to responses)&lt;/li&gt;
&lt;li&gt;message payload definitions for concurrent components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use Gleam to make illegal states unrepresentable. Then lean on BEAM’s runtime model for the concurrency and fault-tolerance you actually care about.&lt;/p&gt;
&lt;p&gt;A practical workflow might look like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Model your domain with explicit types.&lt;/strong&gt; Represent errors and data structures directly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write small, pure functions first.&lt;/strong&gt; Let the compiler guide you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add concurrency for the parts that need it.&lt;/strong&gt; Use BEAM’s process model for orchestration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep effects at the edges.&lt;/strong&gt; The more you can isolate pure logic, the more the type system pays off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat refactoring as a feature.&lt;/strong&gt; In typed codebases, safe refactors are not a luxury—they’re how you stay fast.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You’ll find that Gleam encourages readable structure. The syntax stays out of the way, while types make expectations explicit. That combination is exactly what teams need when code has to survive both time and traffic.&lt;/p&gt;
&lt;h2 id="conclusion-the-beam-era-deserves-modern-ergonomics"&gt;Conclusion: the BEAM era deserves modern ergonomics&lt;/h2&gt;
&lt;p&gt;The BEAM VM already proved that concurrency, supervision, and fault recovery can be engineered into something reliable enough for the real world. Erlang delivered the runtime. Elixir delivered usability. Gleam delivers the missing piece many teams want most: static types with a modern, friendly syntax.&lt;/p&gt;
&lt;p&gt;If you care about the operational strengths of BEAM but want compile-time confidence and clearer code, Gleam is hard to dismiss. It doesn’t replace the BEAM story—it completes it.&lt;/p&gt;</content></item><item><title>The Observability Stack Is Consolidating and That's Good</title><link>https://decastro.work/blog/observability-stack-consolidating-good/</link><pubDate>Fri, 11 Oct 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/observability-stack-consolidating-good/</guid><description>&lt;p&gt;For years, observability felt like a series of one-off experiments: pick a backend, instrument your app, pray it works with your dashboards, and then repeat when the next platform “wins.” That era is ending. OpenTelemetry has emerged as the common language, and the ecosystem is converging around it—making observability less of a vendor-specific art project and more of an engineering discipline.&lt;/p&gt;
&lt;h2 id="from-observability-wars-to-shared-instrumentation"&gt;From “observability wars” to “shared instrumentation”&lt;/h2&gt;
&lt;p&gt;The observability ecosystem didn’t just grow—it fragmented. StatsD fought for metrics, Prometheus won a generation of monitoring fans, while Jaeger and Zipkin splashed the trace landscape with competing ideas. Even when two tools were both “good,” they often required different instrumentation strategies, different exporters, different semantics, and different operational habits.&lt;/p&gt;</description><content>&lt;p&gt;For years, observability felt like a series of one-off experiments: pick a backend, instrument your app, pray it works with your dashboards, and then repeat when the next platform “wins.” That era is ending. OpenTelemetry has emerged as the common language, and the ecosystem is converging around it—making observability less of a vendor-specific art project and more of an engineering discipline.&lt;/p&gt;
&lt;h2 id="from-observability-wars-to-shared-instrumentation"&gt;From “observability wars” to “shared instrumentation”&lt;/h2&gt;
&lt;p&gt;The observability ecosystem didn’t just grow—it fragmented. StatsD fought for metrics, Prometheus won a generation of monitoring fans, while Jaeger and Zipkin splashed the trace landscape with competing ideas. Even when two tools were both “good,” they often required different instrumentation strategies, different exporters, different semantics, and different operational habits.&lt;/p&gt;
&lt;p&gt;The practical downside was predictable: instrumentation became backend-driven rather than application-driven. If you started with one stack and later wanted to switch, you weren’t just changing dashboards—you were rethinking how your software reported telemetry.&lt;/p&gt;
&lt;p&gt;Consolidation fixes this. The core shift is that instrumentation is now largely backend-agnostic. Your application emits telemetry in a standard format, and the backend decides what to store, how to index, and how to visualize it. That means less rewriting when strategies change—and faster adoption of new analysis techniques without re-instrumenting your systems.&lt;/p&gt;
&lt;h2 id="opentelemetry-won-because-it-standardized-the-wire-and-the-sdk"&gt;OpenTelemetry won because it standardized the “wire” and the “SDK”&lt;/h2&gt;
&lt;p&gt;OpenTelemetry is more than a marketing umbrella. It standardized two things that matter to real teams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A common telemetry model&lt;/strong&gt; (what spans, metrics, and logs represent, and how they relate).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A common protocol and SDK approach&lt;/strong&gt; for emitting that data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The impact shows up immediately in everyday developer workflows. Consider a typical microservice deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You add tracing to a service.&lt;/li&gt;
&lt;li&gt;You add metrics to track latency and errors.&lt;/li&gt;
&lt;li&gt;You correlate those signals during incidents.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With a consolidated approach, you can instrument once, and you can route the output to different destinations. Today you might send traces to Tempo and metrics to Prometheus-compatible storage. Tomorrow you might add a vendor backend for enterprise workflows—or run a mixed environment during migration.&lt;/p&gt;
&lt;p&gt;The key is separation of concerns: &lt;strong&gt;your application speaks OpenTelemetry; your platforms listen.&lt;/strong&gt; That’s the difference between “observability as a product decision” and “observability as an engineering capability.”&lt;/p&gt;
&lt;h2 id="the-backend-landscape-is-converginggrafana-datadog-and-more"&gt;The backend landscape is converging—Grafana, Datadog, and more&lt;/h2&gt;
&lt;p&gt;Once OpenTelemetry became the shared foundation, the competition didn’t disappear—it evolved. Backends now differentiate on storage, scale, UI/UX, alerting ergonomics, cost, and operational maturity—not on whether your instrumentation can even work.&lt;/p&gt;
&lt;h3 id="grafanas-lgtm-stack-is-a-credible-open-source-alternative"&gt;Grafana’s LGTM stack is a credible open-source alternative&lt;/h3&gt;
&lt;p&gt;Grafana’s “LGTM” story—Loki (logs), Grafana (dashboards), Tempo (traces), and Mimir (metrics)—is compelling because it treats the observability surface as a cohesive platform. The result is a workflow many teams actually want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search logs in Loki while looking at traces in Tempo&lt;/li&gt;
&lt;li&gt;Use Grafana panels to correlate metrics and traces&lt;/li&gt;
&lt;li&gt;Maintain one query and visualization layer across signals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where this matters in practice is during incidents. Suppose a customer reports elevated checkout failures. A useful workflow looks like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use Grafana to see the error rate spike (metrics).&lt;/li&gt;
&lt;li&gt;Jump to a trace exemplifying a failing request (traces).&lt;/li&gt;
&lt;li&gt;Inspect the relevant log lines and upstream/downstream calls (logs).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When these signals are tied together consistently, investigation time collapses. You spend less time stitching together evidence and more time fixing root causes.&lt;/p&gt;
&lt;p&gt;And because LGTM is largely open-source, teams can keep the cost structure sane. You can run it yourself, scale it deliberately, and avoid paying for features you don’t need. (That doesn’t automatically mean “cheaper in every case,” but it does mean you have options that aren’t locked behind contracts.)&lt;/p&gt;
&lt;h3 id="datadog-still-wins-on-speed-to-value"&gt;Datadog still wins on speed-to-value&lt;/h3&gt;
&lt;p&gt;Datadog’s strength has long been operational convenience: getting to dashboards and alerting quickly, and handling ingestion, indexing, and analysis in a polished way. If you want a turnkey “ship it tomorrow” experience, vendor ecosystems still have an edge.&lt;/p&gt;
&lt;p&gt;But consolidation changes the negotiation. If you start with Datadog but later decide you need more control—or you want to reduce recurring cost—you’re not trapped by instrumentation lock-in in the same way. OpenTelemetry reduces the pain of switching backends because your application doesn’t have to be rewritten to emit different telemetry formats.&lt;/p&gt;
&lt;h3 id="the-real-takeaway-compatibility-is-now-a-default-expectation"&gt;The real takeaway: compatibility is now a default expectation&lt;/h3&gt;
&lt;p&gt;The emerging norm is that observability tools should accept OpenTelemetry-compatible data. That doesn’t mean they’re identical—storage and semantics still differ—but it means compatibility is no longer a special project.&lt;/p&gt;
&lt;p&gt;For engineering leaders, this is the shift worth caring about. It turns observability procurement from a gamble into a managed lifecycle.&lt;/p&gt;
&lt;h2 id="practical-migration-strategies-how-to-avoid-the-rewrite-everything-trap"&gt;Practical migration strategies: how to avoid the “rewrite everything” trap&lt;/h2&gt;
&lt;p&gt;Even with consolidation, most organizations aren’t starting from greenfield. They have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;existing tracing systems&lt;/li&gt;
&lt;li&gt;Prometheus metrics scraped today&lt;/li&gt;
&lt;li&gt;log pipelines built around specific agents&lt;/li&gt;
&lt;li&gt;dashboards that encode institutional knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the goal isn’t “rip and replace.” It’s staged convergence.&lt;/p&gt;
&lt;h3 id="step-1-instrument-once-then-fan-out"&gt;Step 1: Instrument once, then fan out&lt;/h3&gt;
&lt;p&gt;If you already collect telemetry with partial standards, move toward OpenTelemetry at the edges. A practical pattern is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep existing backends running.&lt;/li&gt;
&lt;li&gt;Begin exporting via OpenTelemetry.&lt;/li&gt;
&lt;li&gt;Validate that correlation works: trace IDs link to logs, metrics align with span events, and service naming is consistent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make this measurable. For example, pick one critical user journey—like “search to checkout”—and ensure you can follow it end-to-end using the new pipeline without guessing.&lt;/p&gt;
&lt;h3 id="step-2-standardize-service-names-and-attributes-early"&gt;Step 2: Standardize service names and attributes early&lt;/h3&gt;
&lt;p&gt;Convergence fails when teams invent inconsistent conventions. Decide what you mean by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service name&lt;/li&gt;
&lt;li&gt;environment (prod/staging)&lt;/li&gt;
&lt;li&gt;deployment version&lt;/li&gt;
&lt;li&gt;instance identifiers&lt;/li&gt;
&lt;li&gt;request/user correlation keys (when appropriate)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OpenTelemetry gives you the structure; humans still define the meaning. Invest time here early, because it determines how usable your observability data becomes in Grafana or any UI.&lt;/p&gt;
&lt;h3 id="step-3-use-one-source-of-truth-for-each-signal-during-migration"&gt;Step 3: Use one “source of truth” for each signal during migration&lt;/h3&gt;
&lt;p&gt;Mixing duplicate pipelines is tempting—especially when you can’t turn everything off at once. But duplication can create confusing dashboards and noisy alerts.&lt;/p&gt;
&lt;p&gt;A cleaner approach is to pick a default destination per signal while migrating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;traces: Tempo (new)&lt;/li&gt;
&lt;li&gt;metrics: Mimir or existing Prometheus (choose deliberately)&lt;/li&gt;
&lt;li&gt;logs: Loki or existing log store&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then sunset old pipelines after validation.&lt;/p&gt;
&lt;h3 id="step-4-treat-cost-and-retention-as-first-class-design-inputs"&gt;Step 4: Treat cost and retention as first-class design inputs&lt;/h3&gt;
&lt;p&gt;When observability becomes consolidated, the temptation is to collect everything. Don’t. Define your retention and downsampling strategy for each signal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep high-cardinality data short-lived.&lt;/li&gt;
&lt;li&gt;Store detailed logs for a limited window.&lt;/li&gt;
&lt;li&gt;Keep tracing sampling tuned to your budget and incident needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because now you’re able to route the same telemetry to different backends, you can also right-size storage decisions without forcing new instrumentation.&lt;/p&gt;
&lt;h2 id="what-this-means-for-teams-less-lock-in-more-engineering-focus"&gt;What this means for teams: less lock-in, more engineering focus&lt;/h2&gt;
&lt;p&gt;The most important benefit of consolidation isn’t the choice of vendor or open-source stack. It’s what consolidation frees your organization to do.&lt;/p&gt;
&lt;h3 id="you-can-plan-for-change-instead-of-fearing-it"&gt;You can plan for change instead of fearing it&lt;/h3&gt;
&lt;p&gt;When instrumentation is standard, switching backends becomes an infrastructure migration, not a feature rewrite. That changes the tone of platform roadmaps. Instead of treating observability selection as a one-time irreversible bet, teams can evolve their stack based on cost, performance, and usability.&lt;/p&gt;
&lt;h3 id="incident-response-becomes-faster-and-more-consistent"&gt;Incident response becomes faster and more consistent&lt;/h3&gt;
&lt;p&gt;Correlation is where observability earns its keep. With standardized telemetry, trace IDs can reliably connect to logs and span context can consistently enrich metrics and dashboards. The result is less time spent on plumbing and more time on diagnosis.&lt;/p&gt;
&lt;h3 id="developers-get-a-smoother-path-from-instrumented-to-understood"&gt;Developers get a smoother path from “instrumented” to “understood”&lt;/h3&gt;
&lt;p&gt;A subtle but real advantage: once telemetry meaning is consistent, tools can provide better defaults—service maps, better drill-down experiences, clearer dashboards, and more useful alerts. That makes observability not just something ops manages, but something developers can use to iterate confidently.&lt;/p&gt;
&lt;h2 id="conclusion-the-observability-stack-is-becoming-a-utility"&gt;Conclusion: the observability stack is becoming a utility&lt;/h2&gt;
&lt;p&gt;The observability wars weren’t meaningless—they drove innovation. But the fragmentation was expensive, and the ecosystem is now correcting course. OpenTelemetry unified instrumentation and wire protocol, and that shared foundation is enabling real interoperability across backends.&lt;/p&gt;
&lt;p&gt;Whether you choose Grafana’s LGTM approach, a vendor platform like Datadog, or a hybrid strategy, the trend is clear: observability is consolidating into a utility-like capability. For teams, that means fewer rewrites, less lock-in, and faster paths from signals to fixes. That’s not just good architecture—it’s good business.&lt;/p&gt;</content></item><item><title>Go 1.23 Range Functions Are a Bigger Deal Than You Think</title><link>https://decastro.work/blog/go-1-23-range-functions-bigger-deal/</link><pubDate>Sat, 05 Oct 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/go-1-23-range-functions-bigger-deal/</guid><description>&lt;p&gt;Go has always been opinionated about iteration: simple syntax, predictable control flow, and—when you need concurrency—explicit goroutines and channels. So when Go 1.23 expands what &lt;code&gt;range&lt;/code&gt; can do, it doesn’t just add a small convenience. It changes the shape of what Go code can express, particularly around custom iterators and “generator-like” patterns.&lt;/p&gt;
&lt;p&gt;The headline feature sounds small: you can now range over function types in Go 1.23. But the real story is what that unlocks. You can write lazy, composable iteration pipelines—filters, transformers, even reductions—without channels, without callback spaghetti, and without paying the mental cost of concurrency when you don’t need it.&lt;/p&gt;</description><content>&lt;p&gt;Go has always been opinionated about iteration: simple syntax, predictable control flow, and—when you need concurrency—explicit goroutines and channels. So when Go 1.23 expands what &lt;code&gt;range&lt;/code&gt; can do, it doesn’t just add a small convenience. It changes the shape of what Go code can express, particularly around custom iterators and “generator-like” patterns.&lt;/p&gt;
&lt;p&gt;The headline feature sounds small: you can now range over function types in Go 1.23. But the real story is what that unlocks. You can write lazy, composable iteration pipelines—filters, transformers, even reductions—without channels, without callback spaghetti, and without paying the mental cost of concurrency when you don’t need it.&lt;/p&gt;
&lt;h2 id="what-changed-in-go-123-and-why-it-matters"&gt;What changed in Go 1.23 (and why it matters)&lt;/h2&gt;
&lt;p&gt;Before Go 1.23, you could &lt;code&gt;range&lt;/code&gt; over arrays, slices, maps, strings, and channels. You &lt;em&gt;could&lt;/em&gt; build custom iteration abstractions, but the moment you wanted to integrate with &lt;code&gt;for range&lt;/code&gt;, you typically had to choose between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channels&lt;/strong&gt;: convenient, but ties your design to concurrency and buffering semantics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Callbacks&lt;/strong&gt;: flexible, but often devolves into hard-to-read control flow and “who owns what?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manual loops&lt;/strong&gt;: straightforward, but you lose the uniformity and readability that &lt;code&gt;range&lt;/code&gt; gives you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 1.23 extends &lt;code&gt;range&lt;/code&gt; to work with &lt;strong&gt;range-over-function types&lt;/strong&gt;. The exact mechanics are language-level, but the practical effect is simple: you can define an iterator as a function (or a function-like type) and then iterate it with &lt;code&gt;for ... range&lt;/code&gt; in a way that feels native to Go.&lt;/p&gt;
&lt;p&gt;This is the kind of change that doesn’t trumpet itself as a new abstraction—because it doesn’t introduce a new concept you have to learn. It upgrades a syntax tool you already know.&lt;/p&gt;
&lt;h2 id="from-iterator-objects-to-lazy-pipelines"&gt;From “iterator objects” to lazy pipelines&lt;/h2&gt;
&lt;p&gt;The biggest win is &lt;em&gt;composition without ceremony&lt;/em&gt;. Once your iteration source can be ranged over, you can build pipelines that are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lazy&lt;/strong&gt;: items are produced on demand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt;: no goroutine scheduling, no races, no buffering surprises.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ergonomic&lt;/strong&gt;: &lt;code&gt;for v := range it { ... }&lt;/code&gt; reads like iteration, not like framework code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider a common need: filter a stream of records and then transform them. With channels, you’d typically spin up a goroutine pipeline. With the new capability, you can write something closer to iterator algebra.&lt;/p&gt;
&lt;p&gt;Imagine a function that yields values matching a predicate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It takes a collection (or another iterator).&lt;/li&gt;
&lt;li&gt;It returns an &lt;em&gt;iterator function type&lt;/em&gt; that &lt;code&gt;range&lt;/code&gt; can consume.&lt;/li&gt;
&lt;li&gt;It produces results lazily as the consumer requests them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you’re used to Go’s functional packages, the key shift is that you’re not building “map/filter” helpers that eagerly allocate slices. You’re building something you can chain and consume like native iteration.&lt;/p&gt;
&lt;h3 id="practical-example-filter--map-without-channels"&gt;Practical example: filter + map without channels&lt;/h3&gt;
&lt;p&gt;Suppose you have a slice of integers and you want odd numbers squared, but only until you’ve collected 10.&lt;/p&gt;
&lt;p&gt;With a lazy iterator approach, you can stop early naturally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;for v := range IterFrom(xs).Filter(isOdd).Map(square) { ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Break after the 10th result.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The early break matters. Eager implementations would already compute the entire transformed slice before you ever know you can stop. Lazy iterators let you align computation with need.&lt;/p&gt;
&lt;p&gt;That alignment is more than performance. It’s also correctness-by-construction: your “pipeline” doesn’t accidentally do extra work or depend on side effects that should have been deferred.&lt;/p&gt;
&lt;h2 id="the-case-for-custom-iterators-over-channels"&gt;The case for custom iterators over channels&lt;/h2&gt;
&lt;p&gt;It’s tempting to treat channels as a universal iterator mechanism. In practice, that’s usually a tax.&lt;/p&gt;
&lt;p&gt;Channels shine when you’re doing &lt;strong&gt;concurrency&lt;/strong&gt;: independent work streams that must overlap. But when you’re doing ordinary data traversal, channels force you to answer questions your code shouldn’t have to care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Should the pipeline run in a goroutine, or not?&lt;/li&gt;
&lt;li&gt;Who closes the channel, and how do you guarantee it?&lt;/li&gt;
&lt;li&gt;What happens on early termination (e.g., &lt;code&gt;break&lt;/code&gt;)?&lt;/li&gt;
&lt;li&gt;What’s the buffering strategy, and why?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Custom iterators eliminate most of that. They let you keep the &lt;em&gt;shape&lt;/em&gt; of Go’s &lt;code&gt;range&lt;/code&gt; loops while staying fully in the single-threaded world unless you explicitly opt into concurrency.&lt;/p&gt;
&lt;p&gt;This is especially valuable for library code. A library that exposes an iterator-like API shouldn’t implicitly decide that its user now has to think about goroutine lifetime. With function-based range iterators, the consumer owns the loop; cancellation becomes as simple as breaking out of the loop.&lt;/p&gt;
&lt;h2 id="the-standard-librarys-iter-package-becomes-more-than-helpers"&gt;The standard library’s &lt;code&gt;iter&lt;/code&gt; package becomes more than helpers&lt;/h2&gt;
&lt;p&gt;Go’s ecosystem has long had “iterator” utilities, but the real difference here is that the language now makes iterator composition feel first-class.&lt;/p&gt;
&lt;p&gt;The standard library’s &lt;code&gt;iter&lt;/code&gt; package (and its map/filter/reduce style operations) becomes dramatically more usable because you can integrate those operations directly into &lt;code&gt;for range&lt;/code&gt; loops. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumers can use familiar loop constructs.&lt;/li&gt;
&lt;li&gt;Iterator transformations remain lazy and composable.&lt;/li&gt;
&lt;li&gt;You can build “generator patterns” without inventing your own mini-framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical way to think about it: &lt;code&gt;iter&lt;/code&gt; gives you a vocabulary, and Go 1.23 gives you grammar. Previously, you could write pipeline logic, but the moment you wanted it to &lt;em&gt;feel like iteration&lt;/em&gt;, you had friction. Now you can make it feel native.&lt;/p&gt;
&lt;h3 id="example-reduce-with-readable-control-flow"&gt;Example: reduce with readable control flow&lt;/h3&gt;
&lt;p&gt;Reduction is a good example of why this matters. A reduction often wants a single pass and no intermediate allocations.&lt;/p&gt;
&lt;p&gt;With iterators, a reducer can consume values as they’re produced. That means you can express things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sum only items matching a condition.&lt;/li&gt;
&lt;li&gt;Find the first element meeting a criterion.&lt;/li&gt;
&lt;li&gt;Build a fold that carries state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;for range&lt;/code&gt; integration makes these patterns straightforward to read: the loop is where the control flow is, and the iterator is where the values come from.&lt;/p&gt;
&lt;h2 id="a-more-go-like-alternative-to-generator-frameworks"&gt;A more Go-like alternative to generator frameworks&lt;/h2&gt;
&lt;p&gt;If you’ve used other languages with generator syntax, you know the appeal: define a “sequence producer” once, then consume it with plain loops. Go’s history has been less syntactic here, so teams often build internal generator patterns with channels or callbacks.&lt;/p&gt;
&lt;p&gt;Go 1.23 lets you stop doing that.&lt;/p&gt;
&lt;p&gt;Custom iterators are the sweet spot for “generator-like” behavior in Go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can define reusable iteration sources.&lt;/li&gt;
&lt;li&gt;You can chain transforms cleanly.&lt;/li&gt;
&lt;li&gt;You can avoid goroutines unless you truly want concurrency.&lt;/li&gt;
&lt;li&gt;You can keep the consumer side boring and idiomatic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also how you avoid accidental complexity in teams. Instead of every codebase rolling its own &lt;code&gt;yield&lt;/code&gt; abstraction (with subtle differences, error handling quirks, and inconsistent naming), you can standardize on iterator functions that work with &lt;code&gt;for range&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And yes, that also makes code review easier: reviewers can reason about &lt;code&gt;for range&lt;/code&gt; semantics without tracing channel lifetimes or callback invocation order.&lt;/p&gt;
&lt;h2 id="practical-guidance-what-to-build-what-to-avoid"&gt;Practical guidance: what to build, what to avoid&lt;/h2&gt;
&lt;p&gt;To take advantage of this feature without turning your codebase into an iterator maze, keep these principles in mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use iterators for lazy pipelines, not for everything.&lt;/strong&gt;&lt;br&gt;
If you already need all results, building slices directly may be clearer and faster than chaining iterator adapters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prefer early termination.&lt;/strong&gt;&lt;br&gt;
The biggest practical benefit of laziness shows up when consumers can stop early. Design iterators so &lt;code&gt;break&lt;/code&gt; stops upstream work.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Don’t hide side effects in iteration.&lt;/strong&gt;&lt;br&gt;
Iteration should generally be about traversal and transformation, not orchestration. If you must do side effects, keep them explicit in the consumer loop.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Choose clarity over cleverness.&lt;/strong&gt;&lt;br&gt;
Iterator chains can become hard to debug if you go too abstract. Use small, well-named adapter functions—especially in exported APIs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use channels when concurrency is the point.&lt;/strong&gt;&lt;br&gt;
If you’re doing parallel work or coordinating asynchronous producers/consumers, channels are still the right tool. Iterators are for expressive, composable single-pass iteration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you adopt those rules, Go 1.23’s range-over-function-types don’t just add new capability—they help you keep your code honest.&lt;/p&gt;
&lt;h2 id="conclusion-go-is-getting-more-expressive-without-losing-itself"&gt;Conclusion: Go is getting more expressive without losing itself&lt;/h2&gt;
&lt;p&gt;Go’s design promise has always been simple: keep the language understandable, and add power in ways that fit the existing mental model. Range-over-function types are a perfect example. They don’t replace Go’s core iteration constructs—they extend them so custom iterators can participate as first-class citizens.&lt;/p&gt;
&lt;p&gt;The result is a practical shift: you can build lazy filtering pipelines, generator-style sequences, and reduce-style folds without channels, without goroutine choreography, and without callbacks pretending to be control flow. The &lt;code&gt;iter&lt;/code&gt; package gets more useful, but the bigger win is architectural: Go becomes more composable for everyday data transformation tasks.&lt;/p&gt;
&lt;p&gt;In other words, it’s not a “minor feature.” It’s a better way to write the code you already write—just with fewer compromises.&lt;/p&gt;</content></item><item><title>Claude Just Became My Default AI Coding Partner</title><link>https://decastro.work/blog/claude-default-ai-coding-partner/</link><pubDate>Sun, 29 Sep 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/claude-default-ai-coding-partner/</guid><description>&lt;p&gt;I didn’t switch to Claude because it sounded impressive—I switched because it stopped breaking my workflow. After months of juggling ChatGPT, Copilot, and a rotating cast of open-source assistants, I landed on Anthropic’s Claude for one specific job: code-heavy work where you can’t afford the model to “mostly get it right.” The difference isn’t marketing. It’s how it handles context, instruction-following, and—most importantly—uncertainty.&lt;/p&gt;
&lt;h2 id="context-isnt-a-feature-its-the-product"&gt;Context Isn’t a Feature. It’s the Product.&lt;/h2&gt;
&lt;p&gt;Most AI coding assistants feel like smart autocomplete until your codebase gets real. Then you hit the classic failure mode: you paste the file, the model answers as if it read the whole thing, and then you discover it quietly ignored key parts—imports, edge cases, types, or the one function you &lt;em&gt;actually&lt;/em&gt; cared about.&lt;/p&gt;</description><content>&lt;p&gt;I didn’t switch to Claude because it sounded impressive—I switched because it stopped breaking my workflow. After months of juggling ChatGPT, Copilot, and a rotating cast of open-source assistants, I landed on Anthropic’s Claude for one specific job: code-heavy work where you can’t afford the model to “mostly get it right.” The difference isn’t marketing. It’s how it handles context, instruction-following, and—most importantly—uncertainty.&lt;/p&gt;
&lt;h2 id="context-isnt-a-feature-its-the-product"&gt;Context Isn’t a Feature. It’s the Product.&lt;/h2&gt;
&lt;p&gt;Most AI coding assistants feel like smart autocomplete until your codebase gets real. Then you hit the classic failure mode: you paste the file, the model answers as if it read the whole thing, and then you discover it quietly ignored key parts—imports, edge cases, types, or the one function you &lt;em&gt;actually&lt;/em&gt; cared about.&lt;/p&gt;
&lt;p&gt;Claude’s standout strength, at least in my experience, is its ability to ingest a lot of code at once without immediately dropping the thread. A large context window (I routinely work with entire modules in a single chat) changes the nature of the interaction. Instead of “here’s a snippet, please guess,” it becomes “here’s the system—respond within it.”&lt;/p&gt;
&lt;h3 id="a-practical-example-refactoring-without-guesswork"&gt;A practical example: refactoring without guesswork&lt;/h3&gt;
&lt;p&gt;Imagine you’re refactoring a service layer that spans multiple files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;UserService&lt;/code&gt; calls &lt;code&gt;UserRepository&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UserRepository&lt;/code&gt; uses a query builder&lt;/li&gt;
&lt;li&gt;Validation rules live in shared utilities&lt;/li&gt;
&lt;li&gt;Error handling depends on a couple of conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With a small-context model, you’ll end up sending fragments and asking follow-up questions like, “Wait, did you see the retry policy in this other module?” With Claude, I can often paste the relevant functions and associated helpers in one go, then ask for a structured refactor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify where behavior changes risk regression&lt;/li&gt;
&lt;li&gt;Propose a new interface&lt;/li&gt;
&lt;li&gt;Update call sites&lt;/li&gt;
&lt;li&gt;List tests I should run&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That shift sounds subtle, but it’s enormous in practice: you spend less time re-teaching the model your architecture, and more time iterating on the actual change.&lt;/p&gt;
&lt;h2 id="instruction-following-that-doesnt-melt-under-complexity"&gt;Instruction-Following That Doesn’t Melt Under Complexity&lt;/h2&gt;
&lt;p&gt;Plenty of tools can generate code. Fewer can reliably execute on &lt;em&gt;constraints&lt;/em&gt;. In real refactors, you don’t just want “a correct implementation.” You want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Preserve public API shape&lt;/li&gt;
&lt;li&gt;Keep function signatures stable&lt;/li&gt;
&lt;li&gt;Maintain error semantics (especially around retries and idempotency)&lt;/li&gt;
&lt;li&gt;Avoid changing database query behavior&lt;/li&gt;
&lt;li&gt;Don’t touch unrelated modules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Claude has earned my trust because it stays readable to constraints. When I ask for a refactor plan followed by a patch-style change, it doesn’t treat my instructions like vibes. It follows them in a way that’s consistent across multiple iterations.&lt;/p&gt;
&lt;h3 id="example-keep-behavior-identical-prompts"&gt;Example: “Keep behavior identical” prompts&lt;/h3&gt;
&lt;p&gt;Instead of saying “refactor this,” I ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Refactor for clarity without changing runtime behavior.”&lt;/li&gt;
&lt;li&gt;“Do not alter the signature of &lt;code&gt;getUserById&lt;/code&gt;.”&lt;/li&gt;
&lt;li&gt;“Keep all thrown exceptions exactly as-is.”&lt;/li&gt;
&lt;li&gt;“If any behavior can’t be guaranteed, stop and ask questions.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In return, I get answers that are structured, where changes are localized and reasoning is explicit. That matters when you’re working with code that already has landmines: implicit ordering, side effects, and subtle invariants.&lt;/p&gt;
&lt;p&gt;And yes, this is where other assistants often disappoint—not by being malicious, but by being too eager. They’ll “improve” things you didn’t ask for, or they’ll rewrite logic with a casual confidence that feels like a trap.&lt;/p&gt;
&lt;h2 id="the-best-part-claude-admits-uncertainty"&gt;The Best Part: Claude Admits Uncertainty&lt;/h2&gt;
&lt;p&gt;Here’s the truth: I don’t want a coding assistant that sounds sure when it isn’t. I’ve seen too many AI outputs that read like authoritative explanations while being subtly wrong. The worst ones are the ones that don’t hesitate.&lt;/p&gt;
&lt;p&gt;Claude, in my experience, is more likely to say something like: “I’m not certain how X is handled in your code,” or “I need to see the implementation of Y to avoid breaking behavior.” That posture is a gift.&lt;/p&gt;
&lt;p&gt;Why? Because software engineering is risk management. When an assistant can’t guarantee correctness, the correct response is to ask for more information—not to fake certainty.&lt;/p&gt;
&lt;h3 id="example-when-the-model-should-stop"&gt;Example: when the model should stop&lt;/h3&gt;
&lt;p&gt;Suppose I’m modifying pagination logic, and the codebase has two different paging strategies depending on request headers. If I paste only one module, a less careful assistant might produce “a working answer” that accidentally breaks the alternate path.&lt;/p&gt;
&lt;p&gt;When Claude isn’t sure, it pushes the right interaction forward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Can you paste the handler that sets the paging mode?”&lt;/li&gt;
&lt;li&gt;“Show the function that interprets &lt;code&gt;cursor&lt;/code&gt; vs &lt;code&gt;offset&lt;/code&gt;.”&lt;/li&gt;
&lt;li&gt;“Confirm how you treat empty result sets.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That keeps me from deploying a plausible bug.&lt;/p&gt;
&lt;h2 id="my-workflow-how-i-use-claude-without-losing-time"&gt;My Workflow: How I Use Claude Without Losing Time&lt;/h2&gt;
&lt;p&gt;A big context window doesn’t mean “dump everything and pray.” The workflow matters. Here’s the approach that made Claude my default instead of just another tool.&lt;/p&gt;
&lt;h3 id="1-give-it-the-problem-boundary-not-just-the-code"&gt;1) Give it the problem boundary, not just the code&lt;/h3&gt;
&lt;p&gt;I start with a short briefing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Language + frameworks involved&lt;/li&gt;
&lt;li&gt;What “done” looks like&lt;/li&gt;
&lt;li&gt;What must not change (API shape, behavior, performance constraints)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I paste the relevant modules.&lt;/p&gt;
&lt;h3 id="2-ask-for-an-explicit-plan-before-code"&gt;2) Ask for an explicit plan before code&lt;/h3&gt;
&lt;p&gt;For complex refactors, I ask for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A step-by-step plan&lt;/li&gt;
&lt;li&gt;A list of assumptions&lt;/li&gt;
&lt;li&gt;A list of files/sections that will change&lt;/li&gt;
&lt;li&gt;A test checklist&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This prevents “instant code generation” from becoming “instant nonsense.”&lt;/p&gt;
&lt;h3 id="3-use-iterations-like-a-human-review-loop"&gt;3) Use iterations like a human review loop&lt;/h3&gt;
&lt;p&gt;If the model proposes changes that touch too much, I say so. If it misses a constraint, I point it out. I don’t treat it like an oracle; I treat it like a strong pair programmer that can digest large chunks of code faster than I can search manually.&lt;/p&gt;
&lt;h3 id="4-require-uncertainty-explicitly"&gt;4) Require uncertainty, explicitly&lt;/h3&gt;
&lt;p&gt;In the prompts I’ll include a line like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“If you can’t guarantee correctness, ask questions.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not a trick—it aligns incentives with my actual needs: safer change proposals.&lt;/p&gt;
&lt;h2 id="why-the-market-still-cant-beat-the-context--honesty-combo"&gt;Why the Market Still Can’t Beat the Context + Honesty Combo&lt;/h2&gt;
&lt;p&gt;The AI coding assistant space is fierce, and every contender promises “better code.” But what I care about isn’t raw creativity. It’s operational reliability.&lt;/p&gt;
&lt;p&gt;A smaller context window forces a constant back-and-forth that slows you down and increases the chance of mismatch. Meanwhile, assistants that confidently hallucinate—however fluent their writing—introduce a risk profile that’s hard to manage under time pressure.&lt;/p&gt;
&lt;p&gt;Claude’s combination—large-context handling for whole modules, precise instruction-following during complex edits, and a willingness to flag uncertainty—fits the way I actually build software. It reduces thrash. It increases traceability. And it makes refactoring feel like engineering instead of gambling.&lt;/p&gt;
&lt;p&gt;In other words: capability matters, but context and epistemic humility matter more.&lt;/p&gt;
&lt;h2 id="conclusion-claude-isnt-just-smarterits-safer-for-code-heavy-work"&gt;Conclusion: Claude Isn’t Just Smarter—It’s Safer for Code-Heavy Work&lt;/h2&gt;
&lt;p&gt;After months of switching between tools, I kept returning to one realization: the best AI coding partner isn’t the one that can generate the most impressive snippet—it’s the one that understands enough of your code to make fewer dangerous assumptions. Claude earned my default status because it handles large code context cleanly, follows constraints in complex tasks, and treats uncertainty as a first-class input rather than a defect.&lt;/p&gt;
&lt;p&gt;If your work involves real modules, real refactors, and real risk, Claude is the one I’d bet on.&lt;/p&gt;</content></item><item><title>Edge Computing Isn't a Buzzword Anymore—It's Where Your Code Runs</title><link>https://decastro.work/blog/edge-computing-not-buzzword-code-runs-next/</link><pubDate>Tue, 17 Sep 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/edge-computing-not-buzzword-code-runs-next/</guid><description>&lt;p&gt;For years, “move it to the edge” sounded like marketing—something for CDNs and caching layers. But edge runtimes have quietly crossed a threshold: they now run real application code globally, with enough maturity to change how you design, deploy, and ship. If your users feel latency, you don’t need a new idea—you need a different execution location.&lt;/p&gt;
&lt;h2 id="why-proximity-beats-cleverness"&gt;Why proximity beats cleverness&lt;/h2&gt;
&lt;p&gt;The simplest way to understand edge computing is also the least romantic: the closer your code runs to the user, the less time it takes for the request to come back.&lt;/p&gt;</description><content>&lt;p&gt;For years, “move it to the edge” sounded like marketing—something for CDNs and caching layers. But edge runtimes have quietly crossed a threshold: they now run real application code globally, with enough maturity to change how you design, deploy, and ship. If your users feel latency, you don’t need a new idea—you need a different execution location.&lt;/p&gt;
&lt;h2 id="why-proximity-beats-cleverness"&gt;Why proximity beats cleverness&lt;/h2&gt;
&lt;p&gt;The simplest way to understand edge computing is also the least romantic: the closer your code runs to the user, the less time it takes for the request to come back.&lt;/p&gt;
&lt;p&gt;In practice, that means your app stops paying a round-trip tax to a single origin region. When traffic is worldwide, a “single-region” architecture often turns every request into a tiny negotiation: browser → region → upstream services → region → browser. Even if your origin is optimized, the physical distance remains.&lt;/p&gt;
&lt;p&gt;Edge platforms flip the model. Instead of treating compute as something you “maybe” add near your CDN, edge runtimes treat compute as part of the distribution layer. The result is straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Interactive UX improves&lt;/strong&gt; because the response arrives faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIs feel more reliable&lt;/strong&gt; because you reduce long-tail delays caused by routing and congestion toward a distant origin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global scalability becomes default&lt;/strong&gt; because your code is already distributed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why the latency gap between “one region” and “hundreds of edge locations” is more than a number—it’s the difference between a UI that feels responsive and one that feels fragile.&lt;/p&gt;
&lt;h2 id="edge-has-matured-from-caching-scripts-to-application-logic"&gt;Edge has matured: from caching scripts to application logic&lt;/h2&gt;
&lt;p&gt;The old edge story was caching and routing. That still matters, but it’s not the interesting part. The real shift is that edge runtimes are now capable of &lt;strong&gt;executing business-relevant logic&lt;/strong&gt; without you building an entirely separate backend for the edge.&lt;/p&gt;
&lt;p&gt;Cloudflare Workers, Deno Deploy, and Vercel Edge Functions share a common design philosophy: start fast, run on a lightweight global runtime, and expose enough platform APIs to handle common web patterns. You’re no longer limited to “rewrite this header” or “serve that file.” You can implement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lightweight &lt;strong&gt;API proxies&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;request authentication and lightweight authorization checks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tenant-aware routing&lt;/strong&gt; (e.g., based on hostname)&lt;/li&gt;
&lt;li&gt;response shaping (masking fields, adding computed metadata)&lt;/li&gt;
&lt;li&gt;cache-control decisions based on request context&lt;/li&gt;
&lt;li&gt;integration glue between edge clients and your origin services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical example: imagine a global onboarding flow that calls an API to validate an invitation token and returns a user profile. Today, you might route to your origin, validate there, and return the result. With an edge function, you can validate the token at the edge (or at least validate format + short-circuit obvious failures), attach tenant context, and then forward only the necessary upstream calls. Even when you still hit the origin, the user experiences less waiting—because you’ve reduced the number of round-trips and the work done far away.&lt;/p&gt;
&lt;h2 id="three-edge-runtimes-one-outcome-ship-closer"&gt;Three edge runtimes, one outcome: ship closer&lt;/h2&gt;
&lt;p&gt;Edge ecosystems differ in developer experience and runtime capabilities, but the goal is the same: get code running near the user, fast.&lt;/p&gt;
&lt;h3 id="cloudflare-workers-global-by-default-v8-isolation-in-the-mix"&gt;Cloudflare Workers: global by default, V8 isolation in the mix&lt;/h3&gt;
&lt;p&gt;Cloudflare Workers are built around the idea that the platform should handle distribution and scaling for you. Under the hood, Workers run in &lt;strong&gt;V8 isolates&lt;/strong&gt;, which helps keep execution fast and predictable for lightweight application logic.&lt;/p&gt;
&lt;p&gt;Where Workers shine in real deployments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-heavy request paths&lt;/strong&gt; where you want low latency more than long compute&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API gateway patterns&lt;/strong&gt; (authentication checks, routing, response normalization)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;streaming and proxying&lt;/strong&gt; patterns where a CDN alone isn’t enough&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common pattern is to implement a thin “edge API façade” in Workers that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;inspects the request (headers, cookies, path)&lt;/li&gt;
&lt;li&gt;decides routing and cache strategy&lt;/li&gt;
&lt;li&gt;forwards to your origin or other services&lt;/li&gt;
&lt;li&gt;returns a tailored response&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="deno-deploy-a-globally-distributed-deno-runtime"&gt;Deno Deploy: a globally distributed Deno runtime&lt;/h3&gt;
&lt;p&gt;Deno Deploy brings Deno’s developer ergonomics to a distributed runtime. If you already like Deno’s module approach and its “batteries-in-the-right-place” feel, edge deployments become less of a context switch.&lt;/p&gt;
&lt;p&gt;Deno Deploy is a strong choice when you want edge code that resembles the rest of your Deno-based stack. That matters because edge functions often evolve quickly: today it’s a simple redirect, next week it’s an auth layer, and the week after it’s a request transformer with caching rules.&lt;/p&gt;
&lt;p&gt;In other words, the best edge architecture isn’t the one you planned—it’s the one you can iterate on safely.&lt;/p&gt;
&lt;h3 id="vercel-edge-functions-edge-first-integration-for-modern-web-apps"&gt;Vercel Edge Functions: edge-first integration for modern web apps&lt;/h3&gt;
&lt;p&gt;Vercel Edge Functions fit naturally into the Vercel workflow, which is a big deal for teams that ship web apps continuously. If your product is already aligned with Vercel’s deployment model, edge functions become an extension of your existing pipeline rather than a new infrastructure project.&lt;/p&gt;
&lt;p&gt;Vercel Edge Functions are especially compelling for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API endpoints that support your frontend&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;request/response logic tightly coupled to the web experience&lt;/li&gt;
&lt;li&gt;incremental edge adoption (move one endpoint at a time)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A useful mental model: start by moving your most latency-sensitive endpoints. Your app gets the biggest UX payoff first, and you avoid premature refactors.&lt;/p&gt;
&lt;h2 id="the-tradeoff-fewer-runtime-apis-more-design-discipline"&gt;The tradeoff: fewer runtime APIs, more design discipline&lt;/h2&gt;
&lt;p&gt;Edge runtimes are fast, but they’re not a clone of your server environment. You typically get &lt;strong&gt;limited runtime APIs&lt;/strong&gt;, constrained execution environments, and different patterns for networking, storage, and heavy background work.&lt;/p&gt;
&lt;p&gt;That constraint is not a dealbreaker—it’s a forcing function.&lt;/p&gt;
&lt;h3 id="design-edge-logic-like-its-a-high-frequency-gate"&gt;Design edge logic like it’s a high-frequency gate&lt;/h3&gt;
&lt;p&gt;Edge works best for code that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;makes &lt;strong&gt;quick decisions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;reads data efficiently&lt;/li&gt;
&lt;li&gt;minimizes heavy computation&lt;/li&gt;
&lt;li&gt;delegates expensive work to origins or specialized services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good rule of thumb: if your edge function can answer the request with minimal dependencies and short logic paths, it’s a candidate. If it needs long-running tasks, heavy CPU, or complex stateful workflows, it likely belongs elsewhere.&lt;/p&gt;
&lt;h3 id="example-cache-aware-response-shaping"&gt;Example: cache-aware response shaping&lt;/h3&gt;
&lt;p&gt;Consider a read-mostly endpoint like &lt;code&gt;/api/catalog&lt;/code&gt;. Your edge function can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;check query params and headers to decide cache keys&lt;/li&gt;
&lt;li&gt;serve from cache when possible&lt;/li&gt;
&lt;li&gt;attach small computed fields (like user-specific visibility rules) without rewriting the whole backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if the edge has to occasionally call the origin, the “fast path” keeps UX snappy and protects your origin from spikes.&lt;/p&gt;
&lt;h3 id="example-edge-auth-as-a-first-filter"&gt;Example: edge auth as a first filter&lt;/h3&gt;
&lt;p&gt;You can implement a first-pass authorization check at the edge:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;verify token presence and basic claims structure&lt;/li&gt;
&lt;li&gt;block obvious invalid sessions&lt;/li&gt;
&lt;li&gt;pass through only authorized requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The origin still validates thoroughly, but the edge removes needless load and reduces time-to-failure for unauthenticated users.&lt;/p&gt;
&lt;h2 id="practical-migration-strategy-dont-boil-the-ocean"&gt;Practical migration strategy: don’t boil the ocean&lt;/h2&gt;
&lt;p&gt;Edge is most effective when you adopt it intentionally. Here’s how to move without breaking everything.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick one user-visible endpoint&lt;/strong&gt;&lt;br&gt;
Start with the route that dominates UX timing—search, feed retrieval, or any API your frontend calls for initial render.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Map the critical path&lt;/strong&gt;&lt;br&gt;
Identify what happens before the response can be produced. If there’s logic you can do earlier—or decisions you can make closer to the user—edge is worth it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Keep edge responsibilities narrow&lt;/strong&gt;&lt;br&gt;
Treat the edge function as a fast gatekeeper and router. Push heavy compute and long workflows to your origin or background workers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add observability early&lt;/strong&gt;&lt;br&gt;
Edge debugging can be different from server debugging. Ensure you log request IDs, capture error context, and measure latency by route and status. If you can’t tell what’s happening, you can’t iterate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use a “fast path” + “fallback” model&lt;/strong&gt;&lt;br&gt;
The edge should handle the common case quickly. When it can’t, it forwards to the origin. This gives you performance now without betting the farm on perfect edge coverage.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Iterate endpoint by endpoint&lt;/strong&gt;&lt;br&gt;
Edge adoption shouldn’t be a migration project; it should be a continuous improvement loop.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="conclusion-edge-is-where-ux-is-won"&gt;Conclusion: edge is where UX is won&lt;/h2&gt;
&lt;p&gt;Edge computing isn’t a trend anymore—it’s a deployment location with real architectural consequences. Cloudflare Workers, Deno Deploy, and Vercel Edge Functions all push the same outcome: your code runs closer to users, reducing latency and improving perceived performance.&lt;/p&gt;
&lt;p&gt;The best teams won’t “move everything to the edge.” They’ll move the parts that matter most—read-heavy request paths, API proxies, and fast decision logic—while keeping complex work where it belongs. Done right, edge isn’t just faster. It changes how your product feels.&lt;/p&gt;</content></item><item><title>Local AI Development Environments Are a Game Changer</title><link>https://decastro.work/blog/local-ai-development-environments-game-changer/</link><pubDate>Wed, 11 Sep 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/local-ai-development-environments-game-changer/</guid><description>&lt;p&gt;If you’ve ever “spent your way” through prompt engineering, you already know the real tax on AI development isn’t compute—it’s friction. Local-first tooling changes that. With the right stack, you can iterate instantly, keep your data where it belongs, and—when you’re ready—move to production with almost no refactoring.&lt;/p&gt;
&lt;p&gt;This post lays out a practical local AI development environment using &lt;strong&gt;Ollama + Open WebUI + LiteLLM + Docker Compose&lt;/strong&gt;. The goal is simple: a full AI dev stack on your laptop that behaves like a real service, but costs you nothing during iteration and doesn’t leak your experiments.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever “spent your way” through prompt engineering, you already know the real tax on AI development isn’t compute—it’s friction. Local-first tooling changes that. With the right stack, you can iterate instantly, keep your data where it belongs, and—when you’re ready—move to production with almost no refactoring.&lt;/p&gt;
&lt;p&gt;This post lays out a practical local AI development environment using &lt;strong&gt;Ollama + Open WebUI + LiteLLM + Docker Compose&lt;/strong&gt;. The goal is simple: a full AI dev stack on your laptop that behaves like a real service, but costs you nothing during iteration and doesn’t leak your experiments.&lt;/p&gt;
&lt;h2 id="the-three-hidden-costs-of-just-use-an-api"&gt;The three hidden costs of “just use an API”&lt;/h2&gt;
&lt;p&gt;Most teams don’t struggle because they can’t build AI features. They struggle because early iteration is expensive in ways dashboards don’t capture.&lt;/p&gt;
&lt;h3 id="1-api-costs-during-iteration"&gt;1) API costs during iteration&lt;/h3&gt;
&lt;p&gt;Prompt tweaks are cheap—until every tweak triggers an API call you can’t stop. Even if your project has a budget, you’ll burn it on exploratory runs, regression testing, and “just one more” rephrasing.&lt;/p&gt;
&lt;h3 id="2-latency-during-prompt-engineering"&gt;2) Latency during prompt engineering&lt;/h3&gt;
&lt;p&gt;When each response takes seconds, you stop thinking in terms of conversation and start thinking in terms of waiting. That slows down debugging and makes iteration feel like pulling teeth.&lt;/p&gt;
&lt;h3 id="3-privacy-concerns-during-testing"&gt;3) Privacy concerns during testing&lt;/h3&gt;
&lt;p&gt;Teams love to say “it’s just test data,” then paste real customer context, internal documents, or proprietary workflows into prompts. You might be careful, but software doesn’t care about intent—it only moves bytes. Local tools let you develop without that anxiety.&lt;/p&gt;
&lt;p&gt;The fix isn’t “be more disciplined.” The fix is to build locally.&lt;/p&gt;
&lt;h2 id="the-architecture-local-models-web-ui-and-an-openai-compatible-proxy"&gt;The architecture: local models, web UI, and an OpenAI-compatible proxy&lt;/h2&gt;
&lt;p&gt;Here’s the backbone of the system, and why each component matters.&lt;/p&gt;
&lt;h3 id="ollama-run-models-on-your-machine"&gt;Ollama: run models on your machine&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ollama&lt;/strong&gt; is the local model runner. Instead of calling a hosted model, it pulls and runs LLMs locally (GPU if you have it, CPU if you don’t).&lt;/p&gt;
&lt;p&gt;Practical payoff: you can iterate on prompts with near-real-time feedback—because you’re not waiting on an external network and billing meter.&lt;/p&gt;
&lt;p&gt;Example use case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generate a draft for a PR description template&lt;/li&gt;
&lt;li&gt;Summarize internal docs&lt;/li&gt;
&lt;li&gt;Rewrite safety-sensitive text according to your own rules
All without sending that content to a third party.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="open-webui-a-chatgpt-like-interface-for-developers"&gt;Open WebUI: a ChatGPT-like interface for developers&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Open WebUI&lt;/strong&gt; gives you a familiar chat UI—prompting, conversation history, and model selection—without requiring you to build a front-end.&lt;/p&gt;
&lt;p&gt;Practical payoff: it accelerates everyone. Backend engineers can test prompts without context switching into scripts, and product folks can validate behavior without asking for exports.&lt;/p&gt;
&lt;h3 id="litellm-an-openai-compatible-proxy"&gt;LiteLLM: an OpenAI-compatible proxy&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;LiteLLM&lt;/strong&gt; is the glue that makes your local setup look like an OpenAI-style API. That’s the key to not rewriting your application later.&lt;/p&gt;
&lt;p&gt;Instead of building your app around Ollama’s specific endpoints, you point your app at LiteLLM using OpenAI-compatible settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;base_url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api_key&lt;/code&gt; (often a dummy value in local setups)&lt;/li&gt;
&lt;li&gt;model name mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical payoff: the move from local development to production becomes an environment variable swap, not a migration project.&lt;/p&gt;
&lt;h2 id="docker-compose-turn-the-pieces-into-a-real-stack"&gt;Docker Compose: turn the pieces into a real stack&lt;/h2&gt;
&lt;p&gt;You can run these tools separately, but the real win is consistency. Docker Compose makes “works on my machine” less common and onboarding dramatically faster.&lt;/p&gt;
&lt;p&gt;A typical local &lt;code&gt;docker-compose.yml&lt;/code&gt; includes four services:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ollama&lt;/strong&gt; (model runtime)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;open-webui&lt;/strong&gt; (chat UI)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;litellm&lt;/strong&gt; (OpenAI-compatible proxy)&lt;/li&gt;
&lt;li&gt;Your app or a test runner service (optional)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A skeleton example (trimmed for clarity) looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ollama&lt;/code&gt; exposes the local model API to the Compose network.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open-webui&lt;/code&gt; connects to Ollama as its model backend.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;litellm&lt;/code&gt; connects to Ollama and exposes an OpenAI-compatible endpoint for your application.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From a developer’s perspective, you’ll end up with two “entry points”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A browser URL for chat (Open WebUI)&lt;/li&gt;
&lt;li&gt;An HTTP endpoint for your code (LiteLLM)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use named volumes for anything stateful (like Open WebUI settings).&lt;/li&gt;
&lt;li&gt;Pin container versions so your environment doesn’t change under you mid-sprint.&lt;/li&gt;
&lt;li&gt;Keep your Compose file in the repo so the team shares the same setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="workflow-iterate-like-a-normal-developer-not-a-prompt-scientist"&gt;Workflow: iterate like a normal developer, not a prompt scientist&lt;/h2&gt;
&lt;p&gt;Once the stack is up, you should be able to move through a loop that feels boring—in the best way.&lt;/p&gt;
&lt;h3 id="step-1-validate-the-model-behavior-in-open-webui"&gt;Step 1: Validate the model behavior in Open WebUI&lt;/h3&gt;
&lt;p&gt;Start with a “conversation contract”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The task you want&lt;/li&gt;
&lt;li&gt;The tone/style constraints&lt;/li&gt;
&lt;li&gt;Any format requirements (JSON, bullet points, etc.)&lt;/li&gt;
&lt;li&gt;A few representative inputs that mirror your real data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, suppose you’re building a feature that drafts customer support replies. Your prompt contract might include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Write as the agent, ask one clarifying question if intent is ambiguous.”&lt;/li&gt;
&lt;li&gt;“Never mention internal systems.”&lt;/li&gt;
&lt;li&gt;“Return JSON with &lt;code&gt;reply&lt;/code&gt;, &lt;code&gt;next_question&lt;/code&gt;, and &lt;code&gt;confidence&lt;/code&gt;.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Open WebUI, you can quickly test formatting and refusal behavior before writing code.&lt;/p&gt;
&lt;h3 id="step-2-test-through-your-app-using-the-openai-compatible-endpoint"&gt;Step 2: Test through your app using the OpenAI-compatible endpoint&lt;/h3&gt;
&lt;p&gt;Now point your application to LiteLLM. For local development, you’ll set something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;base_url=http://localhost:&amp;lt;litellm-port&amp;gt;/v1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api_key=local&lt;/code&gt; (or whatever LiteLLM expects)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The critical thing: your app code shouldn’t care whether the model is local or remote. You’re exercising the same interface you’ll use in production—just with a different host.&lt;/p&gt;
&lt;h3 id="step-3-regression-test-prompts-with-deterministic-fixtures"&gt;Step 3: Regression-test prompts with deterministic fixtures&lt;/h3&gt;
&lt;p&gt;Don’t rely on manual chat sessions forever. Build a small test harness:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store input prompts and expected output structure (not necessarily exact wording)&lt;/li&gt;
&lt;li&gt;Validate JSON schemas&lt;/li&gt;
&lt;li&gt;Check that required fields exist&lt;/li&gt;
&lt;li&gt;Ensure safety rules are followed (e.g., “don’t output PII” in your own checks)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical tip: assert structure and key content rather than exact strings. LLMs are probabilistic; your tests should reflect that.&lt;/p&gt;
&lt;h2 id="the-production-switch-one-environment-variable-real-deployment-confidence"&gt;The production switch: one environment variable, real deployment confidence&lt;/h2&gt;
&lt;p&gt;Local-first development only matters if it doesn’t trap you. The point of LiteLLM’s OpenAI compatibility is that you can swap backends cleanly.&lt;/p&gt;
&lt;p&gt;When you’re satisfied with prompt quality, tool calling, and formatting:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set your production &lt;code&gt;base_url&lt;/code&gt; to your chosen hosted provider&lt;/li&gt;
&lt;li&gt;Keep the rest of your app configuration the same&lt;/li&gt;
&lt;li&gt;Redeploy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You haven’t rewritten code to match a different API. You’ve validated behavior under realistic constraints and moved forward with confidence.&lt;/p&gt;
&lt;p&gt;Practical advice before you switch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep model names abstract in config (e.g., &lt;code&gt;LLM_MODEL=chat-dev&lt;/code&gt; mapped to a specific underlying model locally vs. prod).&lt;/li&gt;
&lt;li&gt;Re-run your regression suite against production early. The failure modes are different when you change model families.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="common-pitfalls-and-how-to-avoid-them"&gt;Common pitfalls (and how to avoid them)&lt;/h2&gt;
&lt;p&gt;Local setups are straightforward—until they aren’t. Here are the issues that commonly waste time:&lt;/p&gt;
&lt;h3 id="it-works-in-open-webui-but-not-in-my-app"&gt;“It works in Open WebUI, but not in my app”&lt;/h3&gt;
&lt;p&gt;This usually means your app prompt differs from your chat prompt, or your output parsing is too strict. Fix by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copying the exact system and user messages into your app&lt;/li&gt;
&lt;li&gt;Validating output with the same schema checks you used locally&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="latency-is-still-annoying"&gt;“Latency is still annoying”&lt;/h3&gt;
&lt;p&gt;Make sure you’re running a model size your machine can handle comfortably. If you’re on CPU, choose smaller models for development loops. You can always test larger models selectively.&lt;/p&gt;
&lt;h3 id="docker-networking-confusion"&gt;“Docker networking confusion”&lt;/h3&gt;
&lt;p&gt;If your app container can’t reach LiteLLM, stop guessing and confirm:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the exposed port&lt;/li&gt;
&lt;li&gt;the service name on the Compose network&lt;/li&gt;
&lt;li&gt;whether you’re using &lt;code&gt;localhost&lt;/code&gt; inside a container (often the wrong target)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion-build-locally-ship-faster-sleep-better"&gt;Conclusion: build locally, ship faster, sleep better&lt;/h2&gt;
&lt;p&gt;A local AI development environment isn’t just a convenience—it’s a strategic advantage. &lt;strong&gt;Ollama&lt;/strong&gt; gives you instant iteration, &lt;strong&gt;Open WebUI&lt;/strong&gt; gives your team a shared interface, and &lt;strong&gt;LiteLLM&lt;/strong&gt; gives your application a stable OpenAI-compatible contract. &lt;strong&gt;Docker Compose&lt;/strong&gt; ties it all together into a repeatable stack.&lt;/p&gt;
&lt;p&gt;The best part is the real-world payoff: you don’t waste budget or patience during prompt engineering, you reduce privacy risk during testing, and you move to production with minimal friction. Start local. Iterate quickly. Then ship.&lt;/p&gt;</content></item><item><title>Why I'm Mass-Migrating Projects from npm to pnpm</title><link>https://decastro.work/blog/mass-migrating-projects-npm-to-pnpm/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/mass-migrating-projects-npm-to-pnpm/</guid><description>&lt;p&gt;For years I treated &lt;code&gt;node_modules/&lt;/code&gt; like a necessary evil: bloated, inconsistent, and occasionally cursed. Then I met pnpm and realized I’d been wasting time—and disk space—for reasons that weren’t technical at all. My npm projects worked… right up until they didn’t. pnpm fixes the problems I actually feel day to day: faster installs, stricter dependency behavior, and disk usage that doesn’t balloon quietly in the background.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-npm-it-works-until-it-doesnt"&gt;The real problem with npm: it “works” until it doesn’t&lt;/h2&gt;
&lt;p&gt;Let’s be honest: npm’s classic approach makes it easy to get moving, but it also makes it easy to accumulate technical debt without noticing. In many teams and many repos, &lt;code&gt;node_modules&lt;/code&gt; becomes a behavioral dependency. A package might accidentally reach for a transitive dependency that &lt;em&gt;happens&lt;/em&gt; to exist because of how npm flattened the tree. That’s not “functionality.” That’s coincidence.&lt;/p&gt;</description><content>&lt;p&gt;For years I treated &lt;code&gt;node_modules/&lt;/code&gt; like a necessary evil: bloated, inconsistent, and occasionally cursed. Then I met pnpm and realized I’d been wasting time—and disk space—for reasons that weren’t technical at all. My npm projects worked… right up until they didn’t. pnpm fixes the problems I actually feel day to day: faster installs, stricter dependency behavior, and disk usage that doesn’t balloon quietly in the background.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-npm-it-works-until-it-doesnt"&gt;The real problem with npm: it “works” until it doesn’t&lt;/h2&gt;
&lt;p&gt;Let’s be honest: npm’s classic approach makes it easy to get moving, but it also makes it easy to accumulate technical debt without noticing. In many teams and many repos, &lt;code&gt;node_modules&lt;/code&gt; becomes a behavioral dependency. A package might accidentally reach for a transitive dependency that &lt;em&gt;happens&lt;/em&gt; to exist because of how npm flattened the tree. That’s not “functionality.” That’s coincidence.&lt;/p&gt;
&lt;p&gt;The most frustrating bugs are the ones that only reproduce on “fresh” machines or after a clean install. You’ll see errors like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cannot find module 'x'&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;runtime failures caused by missing nested packages&lt;/li&gt;
&lt;li&gt;tests that pass locally because the dependency was present “somewhere” in the flattened structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words: npm can mask dependency issues rather than surfacing them early.&lt;/p&gt;
&lt;p&gt;I don’t want my build system to be a scavenger hunt. I want it to be boring—and correct.&lt;/p&gt;
&lt;h2 id="pnpms-storage-model-fewer-duplicates-more-sanity"&gt;pnpm’s storage model: fewer duplicates, more sanity&lt;/h2&gt;
&lt;p&gt;The headline difference most people miss is not the speed—it’s the storage strategy. pnpm uses &lt;strong&gt;content-addressable storage&lt;/strong&gt; (store once, reuse everywhere) and then &lt;strong&gt;hard-links&lt;/strong&gt; dependencies into each project.&lt;/p&gt;
&lt;p&gt;What that means in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If 20 projects use the same version of &lt;code&gt;react&lt;/code&gt;, you don’t keep 20 physical copies of &lt;code&gt;react&lt;/code&gt;’s files.&lt;/li&gt;
&lt;li&gt;pnpm keeps a global store and links what each project needs.&lt;/li&gt;
&lt;li&gt;On developer machines that run lots of repositories, this is the difference between “my disk is fine” and “why is my laptop suddenly out of space?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’ve experienced this firsthand while cleaning up after a long npm era: folders that looked harmless turned into multi-gigabyte &lt;code&gt;node_modules&lt;/code&gt; duplicates. pnpm doesn’t eliminate disk usage (dependencies still exist), but it prevents the &lt;strong&gt;quiet multiplication&lt;/strong&gt; that feels unavoidable with npm.&lt;/p&gt;
&lt;p&gt;If you manage monorepos, CI caches, or a personal machine with a dozen repos, this becomes immediately tangible. My installs aren’t just faster—they’re less wasteful. That matters because storage pressure is the silent tax that slows everything else down: backups, indexing, container builds, and even editor performance.&lt;/p&gt;
&lt;h2 id="faster-installs-not-magic-just-better-mechanics"&gt;Faster installs: not magic, just better mechanics&lt;/h2&gt;
&lt;p&gt;The second reason I’m migrating is simple: pnpm installs faster for me, and the gap has been consistent enough that I stopped doubting it.&lt;/p&gt;
&lt;p&gt;Two mechanisms drive that improvement:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reuse from the global store.&lt;/strong&gt; If the package versions already exist in the store, pnpm can link rather than re-download and re-write everything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic resolution.&lt;/strong&gt; pnpm knows exactly what should be installed for a project, instead of leaning on whatever npm happens to flatten or hoist.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you want a practical feel, run this on a clean environment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create two temporary directories: one for npm, one for pnpm.&lt;/li&gt;
&lt;li&gt;Copy the same &lt;code&gt;package.json&lt;/code&gt;/lockfile setup.&lt;/li&gt;
&lt;li&gt;Blow away caches as needed (or use a fresh node environment).&lt;/li&gt;
&lt;li&gt;Measure install time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You don’t have to chase microbenchmarks. The goal is confidence: “Will this be faster every time I start work?” In my experience, yes. And once you’re used to it, npm’s slower churn starts to feel like dragging a chain.&lt;/p&gt;
&lt;h2 id="strict-dependency-resolution-pnpm-catches-the-phantoms"&gt;Strict dependency resolution: pnpm catches the “phantoms”&lt;/h2&gt;
&lt;p&gt;Here’s the part I care about most: pnpm forces dependency truth.&lt;/p&gt;
&lt;p&gt;With npm’s permissive hoisting/flattening behavior, you can accidentally rely on packages you never declared. The dependency might appear because another package pulled it in somewhere else in the tree. npm can make that feel “fine” until the dependency graph changes—perhaps after an unrelated upgrade.&lt;/p&gt;
&lt;p&gt;pnpm’s strictness changes the incentive structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If your code imports &lt;code&gt;left-pad&lt;/code&gt;, you must declare &lt;code&gt;left-pad&lt;/code&gt; (or the package providing it).&lt;/li&gt;
&lt;li&gt;If a tool expects a peer dependency, pnpm pushes you toward the correct setup instead of silently tolerating ambiguity.&lt;/li&gt;
&lt;li&gt;If something is missing, pnpm fails early during install rather than letting it explode later in runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not just pedantry. It’s how you prevent “works on my machine” from becoming a lifestyle.&lt;/p&gt;
&lt;h3 id="a-concrete-example-accidental-transitive-reliance"&gt;A concrete example: accidental transitive reliance&lt;/h3&gt;
&lt;p&gt;Suppose you’re building a tool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;somePlugin&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;some-plugin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your &lt;code&gt;package.json&lt;/code&gt; includes &lt;code&gt;some-plugin&lt;/code&gt; as a dependency, sure. But your code might also implicitly rely on &lt;code&gt;some-plugin&lt;/code&gt; pulling in &lt;code&gt;some-utils&lt;/code&gt;, and you never declared &lt;code&gt;some-utils&lt;/code&gt; yourself.&lt;/p&gt;
&lt;p&gt;With npm, &lt;code&gt;some-utils&lt;/code&gt; might end up available via hoisting and flattening. With pnpm, you’ll discover the dependency is not actually part of your project contract. You’ll either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add &lt;code&gt;some-utils&lt;/code&gt; explicitly, or&lt;/li&gt;
&lt;li&gt;update how you import/use things so the dependency is truly not needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s the point: pnpm makes dependency boundaries real.&lt;/p&gt;
&lt;h2 id="the-migration-path-painless-for-most-projects-and-worth-it"&gt;The migration path: painless for most projects (and worth it)&lt;/h2&gt;
&lt;p&gt;Let’s talk about what “mass-migration” actually looks like in the real world. I’m not suggesting you rewrite your entire ecosystem overnight. The migration for most projects is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick pnpm and install it&lt;/strong&gt;&lt;br&gt;
If you’re using Corepack, you can enable it; otherwise install pnpm globally.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate a lockfile&lt;/strong&gt;&lt;br&gt;
Run &lt;code&gt;pnpm install&lt;/code&gt; to create &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; based on your existing &lt;code&gt;package.json&lt;/code&gt;. You’ll keep your current semantic intent; you’re just switching the installer and the lock format.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update scripts with confidence&lt;/strong&gt;&lt;br&gt;
Most scripts (&lt;code&gt;test&lt;/code&gt;, &lt;code&gt;build&lt;/code&gt;, &lt;code&gt;lint&lt;/code&gt;) don’t care whether npm or pnpm runs them. The command runner remains Node-centric. Where differences pop up, it’s usually around assumptions about module layout—which pnpm is intentionally stricter about.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fix the few errors that reveal hidden dependency bugs&lt;/strong&gt;&lt;br&gt;
The first pnpm install after migration is often an opportunity, not a crisis. If something fails, it’s because the project relied on undeclared transitive dependencies. Add the missing dependency, fix peer dependency declarations, or adjust tooling configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update CI and caching&lt;/strong&gt;&lt;br&gt;
Cache pnpm’s store rather than caching &lt;code&gt;node_modules&lt;/code&gt; blobs. This is a major win for repeat builds and keeps CI artifacts lean.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For monorepos, you’ll also want to standardize how workspaces are configured. The upside is that once pnpm is consistent across repos, the whole fleet starts behaving predictably.&lt;/p&gt;
&lt;h3 id="what-about-edge-cases"&gt;What about edge cases?&lt;/h3&gt;
&lt;p&gt;The only reason you might struggle is if your project has grown into a “dependency soup” where code imports things it never declared. That’s not a pnpm problem—it’s a hygiene problem waiting to be exposed.&lt;/p&gt;
&lt;p&gt;In practice, these fixes are usually small: add a dependency, correct a peer dependency declaration, or update a toolchain package. pnpm doesn’t just move files around; it teaches your project to be honest.&lt;/p&gt;
&lt;h2 id="why-inertia-is-the-only-reason-npm-still-wins"&gt;Why inertia is the only reason npm still wins&lt;/h2&gt;
&lt;p&gt;There’s a reflex in the JavaScript ecosystem: npm is “default,” so it must be the safe choice. But defaults aren’t merit. They’re momentum.&lt;/p&gt;
&lt;p&gt;pnpm offers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Faster installs&lt;/strong&gt; driven by reuse and deterministic behavior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict dependency resolution&lt;/strong&gt; that catches phantom dependencies early&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disk space savings&lt;/strong&gt; from global content-addressable storage and hard-linking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A more predictable developer experience&lt;/strong&gt; across clean installs and new machines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your organization values stability and correctness, pnpm is the better contract. If you’re optimizing for “it happens to work today,” npm’s permissiveness can feel convenient—but that convenience comes due later, usually during upgrades or onboarding.&lt;/p&gt;
&lt;p&gt;Mass migration isn’t about switching for novelty. It’s about removing a source of accidental complexity that has been slowing you down quietly for years.&lt;/p&gt;
&lt;h2 id="conclusion-make-dependency-truth-and-faster-installs-your-default"&gt;Conclusion: make dependency truth and faster installs your default&lt;/h2&gt;
&lt;p&gt;I migrated because I was tired of invisible fragility: installs that took longer than they should, disks filling with duplicated dependencies, and bugs that only appeared after clean installs. pnpm gave me a better setup with practical benefits I can feel every day: less wasted disk, faster installs, and dependency resolution that forces correctness instead of guessing.&lt;/p&gt;
&lt;p&gt;If you have multiple projects—or even a single repo that keeps growing—start the migration. You’ll likely fix a handful of undeclared dependencies, replace guesswork with guarantees, and wonder why you didn’t do it sooner.&lt;/p&gt;</content></item><item><title>Rust for the Web Is No Longer a Meme</title><link>https://decastro.work/blog/rust-for-web-no-longer-meme/</link><pubDate>Sat, 24 Aug 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rust-for-web-no-longer-meme/</guid><description>&lt;p&gt;For years, “Rust for the web” sounded like a dare—an eccentric choice for developers who wanted to prove a point. Today it reads more like a category: Leptos for full-stack UI, Axum for pragmatic HTTP services, and a growing set of libraries that make the whole stack feel less like a science project and more like an engineering option. Rust didn’t just wander onto the web; it brought the same habits that made it dominant in systems programming—ownership, correctness tooling, and predictable performance.&lt;/p&gt;</description><content>&lt;p&gt;For years, “Rust for the web” sounded like a dare—an eccentric choice for developers who wanted to prove a point. Today it reads more like a category: Leptos for full-stack UI, Axum for pragmatic HTTP services, and a growing set of libraries that make the whole stack feel less like a science project and more like an engineering option. Rust didn’t just wander onto the web; it brought the same habits that made it dominant in systems programming—ownership, correctness tooling, and predictable performance.&lt;/p&gt;
&lt;p&gt;This is not a call to rewrite your Next.js app because the zeitgeist demands it. It’s a case for paying attention. If you’re starting something new and reliability matters as much as speed, Rust has quietly become one of the most credible paths available.&lt;/p&gt;
&lt;h2 id="from-punchline-to-platform-the-practical-shift"&gt;From punchline to platform: the practical shift&lt;/h2&gt;
&lt;p&gt;The old argument against Rust on the web was simple: “You’ll be slower, and you’ll fight the tooling.” For a long time, that was true. Rust’s strengths—memory safety and fearless concurrency—were convincing, but the web ecosystem lagged in developer-experience polish.&lt;/p&gt;
&lt;p&gt;What’s changed isn’t that Rust suddenly became “easy.” It’s that the major pieces now fit together with fewer gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leptos&lt;/strong&gt; offers a coherent approach to full-stack Rust apps: render to the frontend (including &lt;strong&gt;WebAssembly&lt;/strong&gt;) and run &lt;strong&gt;SSR&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Axum&lt;/strong&gt; brings a clean, composable model for HTTP servers and middleware built on the idea of “towers”—a structure that keeps request handling modular.&lt;/li&gt;
&lt;li&gt;The broader ecosystem—logging, async runtimes, serialization, error handling—has matured enough that you can build without constantly duct-taping.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you previously tried Rust web frameworks and felt like you were bleeding time into scaffolding, you’ll notice the difference: the “happy path” is clearer now. The best way to see it is not in benchmarks or hot takes—it’s building a small feature end-to-end.&lt;/p&gt;
&lt;h2 id="leptos-full-stack-rust-without-giving-up-the-frontend"&gt;Leptos: full-stack Rust without giving up the frontend&lt;/h2&gt;
&lt;p&gt;Leptos is one of the most compelling “real apps” stories in Rust web. The core idea is straightforward: write your UI logic in Rust, keep it reactive, and then use the right compilation target—&lt;strong&gt;WASM&lt;/strong&gt; for the browser and &lt;strong&gt;server-side rendering&lt;/strong&gt; for the backend.&lt;/p&gt;
&lt;p&gt;Here’s why that matters in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SSR that feels native.&lt;/strong&gt; If you render on the server, you can serve initial HTML immediately and avoid the “blank page while JS hydrates” experience. Leptos keeps the mental model coherent by letting the server produce UI output rather than treating it as an afterthought.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single language across the stack.&lt;/strong&gt; That sounds like a developer-experience tagline until you experience how often you end up duplicating types between frontend and backend. With Rust on both sides, sharing data structures and validation rules becomes less painful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance as a default, not a promise.&lt;/strong&gt; The Rust toolchain nudges you toward efficient data handling and predictable costs. You still need to profile, but you’re less likely to accidentally create an unbounded mess.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Consider a typical product workflow: a page that displays a dashboard for the user, and can update part of the UI when filters change. In a Leptos-based app, you can keep the reactive UI logic in Rust, and choose whether updates happen through SSR refresh patterns, client-side hydration, or targeted interactivity. The point isn’t that Leptos replaces every existing web technique; it’s that it doesn’t force you into the worst parts of the ecosystem to get started.&lt;/p&gt;
&lt;p&gt;Practical framing: if your team already understands Rust, Leptos reduces the “two-language” tax. If you don’t, don’t pretend this becomes instantly beginner-friendly. But it becomes easier to justify because you can ship more reliably with fewer moving parts—especially when correctness and maintainability are non-negotiable.&lt;/p&gt;
&lt;h2 id="axum-composable-http-services-that-dont-feel-like-spaghetti"&gt;Axum: composable HTTP services that don’t feel like spaghetti&lt;/h2&gt;
&lt;p&gt;On the backend, Axum’s biggest contribution is that it feels &lt;em&gt;architected&lt;/em&gt;. Many web frameworks grow organically around routing and controllers. Axum grows around request processing as a pipeline, using the &lt;strong&gt;tower&lt;/strong&gt; ecosystem model.&lt;/p&gt;
&lt;p&gt;In practical terms, that means middleware and handlers compose cleanly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt; can wrap routes without being entangled with business logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; can be applied at the appropriate layers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracing/logging&lt;/strong&gt; can capture consistent context for every request.&lt;/li&gt;
&lt;li&gt;Error handling can be centralized without turning into a nested conditional jungle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever tried to retrofit observability into a web service and ended up rewriting half the codebase, you already understand why this matters. With Axum, it’s natural to set up middleware once, then reuse it across routers and services.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine an API with endpoints under &lt;code&gt;/api/v1/*&lt;/code&gt;. You can structure your app so that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A first middleware extracts and validates a session token.&lt;/li&gt;
&lt;li&gt;A second middleware enforces rate limits per user or per IP.&lt;/li&gt;
&lt;li&gt;Tracing middleware attaches request IDs and user IDs to spans.&lt;/li&gt;
&lt;li&gt;Handlers focus on transforming validated input into domain actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t just get clean code—you get a system where changing policy (say, tweaking limits or auth rules) is localized. That’s a reliability win you feel months later, not just a stylistic preference.&lt;/p&gt;
&lt;h2 id="the-rust-web-stack-mature-enough-to-bet-on"&gt;The Rust web stack: mature enough to bet on&lt;/h2&gt;
&lt;p&gt;The most honest way to evaluate a framework is to ask: “When I hit the edges, will I have to rewrite everything?” Rust’s web ecosystem is now mature enough that most common edges are navigable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Serialization and validation&lt;/strong&gt; are well-trodden. You can structure request and response types without turning your codebase into a stringly-typed mess.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async and concurrency&lt;/strong&gt; are first-class. Rust’s async story is not magic, but it’s consistent and supported by the ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error handling&lt;/strong&gt; is ergonomic when you adopt a pattern early. The best Rust web code usually has a predictable error type strategy rather than ad-hoc panics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You should still expect some roughness if you go looking for the most bleeding-edge patterns. But if you stick to the mainstream crates and patterns, the experience is less “reinvent the wheel” and more “build the wheel you need.”&lt;/p&gt;
&lt;p&gt;A useful litmus test: if you can implement “create user,” “login,” and “fetch protected resource” in a week—with logs, tracing, and meaningful errors—you’re not in a science project anymore. Rust web apps can do that now, without requiring heroic effort.&lt;/p&gt;
&lt;h2 id="should-you-rewrite-your-nextjs-app-no"&gt;Should you rewrite your Next.js app? (No.)&lt;/h2&gt;
&lt;p&gt;Let’s kill the wrong project. Rewriting an existing Next.js app in Rust is rarely the right move, because the problem isn’t the technology—it’s the cost of change.&lt;/p&gt;
&lt;p&gt;You should consider Rust for the web when one of these is true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You’re starting a new product&lt;/strong&gt; and want a long runway where correctness and reliability are part of the architecture, not a post-launch scramble.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency and resource efficiency&lt;/strong&gt; matter—think backend services that need to handle sustained traffic predictably.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security and robustness&lt;/strong&gt; are core requirements, and you want the compiler and tooling to eliminate entire classes of bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your team already ships in Rust&lt;/strong&gt; and can leverage shared expertise instead of forcing a full re-training cycle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But if you already have a mature frontend, a working deployment pipeline, and a healthy developer workflow, Rust isn’t a reason to blow it up. Interoperability is also real here: you can gradually adopt Rust for specific services (APIs, workers, performance-sensitive components) and leave your frontend in its current shape. The web is big enough for polyglot systems—just don’t let “polyglot” become “we never finished deciding.”&lt;/p&gt;
&lt;p&gt;A good compromise is architectural: keep Next.js for the UI if that’s where your team shines, and use Rust services behind it where performance, safety, or concurrency dominate. That yields tangible benefits without risking a full rewrite.&lt;/p&gt;
&lt;h2 id="the-bottom-line-rust-is-a-legitimate-web-choice"&gt;The bottom line: Rust is a legitimate web choice&lt;/h2&gt;
&lt;p&gt;Rust for the web is no longer a meme because it’s no longer purely theoretical. With &lt;strong&gt;Leptos&lt;/strong&gt;, you can build full-stack apps with SSR and WebAssembly-based frontend interactivity while staying in Rust. With &lt;strong&gt;Axum&lt;/strong&gt;, you get a backend architecture that supports composable middleware, clean request pipelines, and maintainable error handling.&lt;/p&gt;
&lt;p&gt;Here’s the opinionated guidance: don’t rewrite what works. But if you’re choosing a stack for something new—especially something that must be stable under load—Rust deserves a serious place on your shortlist.&lt;/p&gt;
&lt;p&gt;The language that conquered systems programming is finally taking the web personally. And this time, it brought the tools to back it up.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Rust for the web has crossed the line from curiosity to credible platform. Leptos and Axum demonstrate that you can build modern, full-stack applications with composable architecture and production-oriented correctness. If performance and reliability are part of your requirements—not slogans—Rust is a smart, defensible choice for new projects.&lt;/p&gt;</content></item><item><title>The Quiet Rise of Server-Sent Events Over WebSockets</title><link>https://decastro.work/blog/quiet-rise-server-sent-events-over-websockets/</link><pubDate>Mon, 12 Aug 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/quiet-rise-server-sent-events-over-websockets/</guid><description>&lt;p&gt;Most “real-time” apps don’t need real-time in both directions. They need the server to reliably tell the browser what’s happening—notifications, progress, live updates, streaming responses—without the operational overhead of bidirectional sockets. That’s exactly why Server-Sent Events (SSE) have been steadily winning mindshare. They do a surprising amount of what WebSockets do, with a fraction of the complexity.&lt;/p&gt;
&lt;h2 id="what-most-apps-actually-mean-by-real-time"&gt;What most apps actually mean by “real-time”&lt;/h2&gt;
&lt;p&gt;When teams say “we need WebSockets,” they often mean one narrow thing: the server should push updates to the client.&lt;/p&gt;</description><content>&lt;p&gt;Most “real-time” apps don’t need real-time in both directions. They need the server to reliably tell the browser what’s happening—notifications, progress, live updates, streaming responses—without the operational overhead of bidirectional sockets. That’s exactly why Server-Sent Events (SSE) have been steadily winning mindshare. They do a surprising amount of what WebSockets do, with a fraction of the complexity.&lt;/p&gt;
&lt;h2 id="what-most-apps-actually-mean-by-real-time"&gt;What most apps actually mean by “real-time”&lt;/h2&gt;
&lt;p&gt;When teams say “we need WebSockets,” they often mean one narrow thing: the server should push updates to the client.&lt;/p&gt;
&lt;p&gt;Think about the common list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Notifications&lt;/strong&gt; (“your report is ready”)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live dashboards&lt;/strong&gt; (new events, status changes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upload or job progress&lt;/strong&gt; (percent complete, logs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming AI responses&lt;/strong&gt; (tokens as they’re generated)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-driven UI updates&lt;/strong&gt; (ban someone, expire a session, refresh a card)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In all of these, the client rarely needs to send a continuous stream to the server over the same channel. Sure, the client will sometimes click a button or acknowledge something—but that’s request/response territory, not a persistent two-way socket conversation.&lt;/p&gt;
&lt;p&gt;That mismatch is the core reason SSE keeps showing up in production architectures: it matches the problem shape.&lt;/p&gt;
&lt;h2 id="sse-vs-websockets-same-outcome-different-cost"&gt;SSE vs WebSockets: same outcome, different cost&lt;/h2&gt;
&lt;p&gt;At a high level:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WebSockets&lt;/strong&gt; establish a full-duplex connection: both sides can send messages at any time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSE&lt;/strong&gt; establishes a &lt;strong&gt;server-to-client&lt;/strong&gt; stream over plain HTTP semantics: the browser opens a connection, and the server writes events down that pipe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the practical difference that matters:&lt;/p&gt;
&lt;h3 id="sse-is-designed-for-one-way-streams"&gt;SSE is designed for one-way streams&lt;/h3&gt;
&lt;p&gt;If your “real-time” feature is inherently unidirectional, SSE is the clean fit. You don’t spend engineering time building message routing, buffering, or client-side state machines for traffic you don’t actually need.&lt;/p&gt;
&lt;h3 id="sse-plays-nicely-with-the-web-platform"&gt;SSE plays nicely with the web platform&lt;/h3&gt;
&lt;p&gt;SSE uses HTTP, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It works naturally with typical web infrastructure.&lt;/li&gt;
&lt;li&gt;It can reuse existing routing, authentication patterns, and logging.&lt;/li&gt;
&lt;li&gt;It tends to be easier to reason about when something goes wrong.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;WebSockets can still be absolutely right—especially for truly interactive, bidirectional workflows like multiplayer games or collaborative editors. But those are a smaller slice of the “real-time” pie than most teams admit during planning.&lt;/p&gt;
&lt;h2 id="a-real-world-pattern-live-progress-without-a-socket"&gt;A real-world pattern: live progress without a socket&lt;/h2&gt;
&lt;p&gt;Let’s take a concrete example: a user starts a long-running job (say, generating a report). The UI needs to update the progress bar and display incremental log lines.&lt;/p&gt;
&lt;p&gt;With SSE, you can do this cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The browser requests &lt;code&gt;/jobs/{id}/events&lt;/code&gt; to open the stream.&lt;/li&gt;
&lt;li&gt;Your server writes events like &lt;code&gt;progress&lt;/code&gt; and &lt;code&gt;log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The client updates the UI as messages arrive.&lt;/li&gt;
&lt;li&gt;If the connection drops, SSE can reconnect and continue.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On the server side (Node/Express-style pseudo-implementation):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Flush headers immediately&lt;/li&gt;
&lt;li&gt;Write lines in SSE format:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;event: progress&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data: { &amp;quot;percent&amp;quot;: 42 }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Repeat as progress changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the client side, you use the built-in &lt;code&gt;EventSource&lt;/code&gt; API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open the stream&lt;/li&gt;
&lt;li&gt;Listen for named events&lt;/li&gt;
&lt;li&gt;Update UI components in real time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key benefit isn’t just fewer lines of code—it’s &lt;strong&gt;fewer moving parts&lt;/strong&gt;. You avoid designing a protocol for bi-directional messages when all you need is “server tells client, reliably.”&lt;/p&gt;
&lt;h2 id="reliability-and-reconnection-it-should-come-back-is-built-in"&gt;Reliability and reconnection: “it should come back” is built in&lt;/h2&gt;
&lt;p&gt;One of the underappreciated reasons SSE is popular is that it embraces a reality most teams eventually face: connections fail.&lt;/p&gt;
&lt;p&gt;Mobile networks drop. Load balancers recycle connections. Deploys happen. Browsers navigate away and back. The question becomes: what do you do when the stream interrupts?&lt;/p&gt;
&lt;p&gt;SSE’s model is built around reconnection behavior. You can take advantage of that by using &lt;strong&gt;event IDs&lt;/strong&gt; and the &lt;code&gt;Last-Event-ID&lt;/code&gt; header to let clients resume from where they left off.&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include an &lt;code&gt;id:&lt;/code&gt; field per event when ordering matters.&lt;/li&gt;
&lt;li&gt;Make your server store (or recompute) recent event history for a short window.&lt;/li&gt;
&lt;li&gt;Design the client to treat the stream as “eventually consistent,” not a perfectly uninterrupted transcript.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach drastically reduces the “we lost the message” edge cases that haunt real-time systems. With WebSockets, you can implement reconnection too, but you’re doing more work and owning more failure modes.&lt;/p&gt;
&lt;h2 id="infrastructure-reality-sse-tends-to-be-boring-in-a-good-way"&gt;Infrastructure reality: SSE tends to be boring (in a good way)&lt;/h2&gt;
&lt;p&gt;Boring infrastructure is a competitive advantage. SSE generally works over HTTP and therefore tends to require less special handling at the edges—especially when compared to WebSocket-specific plumbing.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Load balancers&lt;/strong&gt; often behave more predictably with HTTP connections than with protocol-upgraded channels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reverse proxies&lt;/strong&gt; usually route SSE traffic using existing HTTP rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt; (logs, metrics, traces) often becomes simpler because everything is still fundamentally HTTP traffic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That doesn’t mean SSE is magic. You still need to think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connection limits on servers and proxies&lt;/li&gt;
&lt;li&gt;Timeouts for long-lived HTTP connections&lt;/li&gt;
&lt;li&gt;Scaling strategies (more on that below)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But if your team has ever spent time untangling “why does WebSocket fail only in staging behind this proxy,” SSE can feel like a timeout governor: you just get fewer surprises.&lt;/p&gt;
&lt;h2 id="scaling-and-load-what-to-plan-for-with-sse"&gt;Scaling and load: what to plan for with SSE&lt;/h2&gt;
&lt;p&gt;Long-lived connections aren’t free. Regardless of technology, you have to plan for how many concurrent clients you’ll support and how your system will behave under load.&lt;/p&gt;
&lt;p&gt;The good news: SSE has a straightforward scaling story.&lt;/p&gt;
&lt;h3 id="use-horizontal-scaling-friendly-patterns"&gt;Use horizontal scaling-friendly patterns&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Run your SSE endpoint behind your normal HTTP load balancing.&lt;/li&gt;
&lt;li&gt;Ensure sticky sessions are &lt;strong&gt;not&lt;/strong&gt; required for correctness. If they are, you’ll feel it quickly during scaling events.&lt;/li&gt;
&lt;li&gt;Avoid “one stream per worker” designs that require shared in-memory state unless you’re very careful.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="externalize-event-distribution"&gt;Externalize event distribution&lt;/h3&gt;
&lt;p&gt;If multiple app instances must deliver events for the same client stream, you’ll likely want a shared pub/sub mechanism (or a queue-based architecture) to route events to the right instances.&lt;/p&gt;
&lt;h3 id="keep-event-payloads-small"&gt;Keep event payloads small&lt;/h3&gt;
&lt;p&gt;Streaming is not a license to send megabytes per message. For progress updates, send compact updates and let the client fetch larger data on demand.&lt;/p&gt;
&lt;h3 id="handle-backpressure-intentionally"&gt;Handle backpressure intentionally&lt;/h3&gt;
&lt;p&gt;If the client can’t process events quickly, you can end up building buffers. Treat your SSE stream as a “live feed,” not a guaranteed delivery log unless you implement buffering and replay semantics yourself.&lt;/p&gt;
&lt;p&gt;A solid heuristic: design the event stream so that missing intermediate updates doesn’t break the UI. For example, a progress bar can usually jump from 40% to 55% without needing every in-between step.&lt;/p&gt;
&lt;h2 id="when-websockets-are-still-the-right-call"&gt;When WebSockets are still the right call&lt;/h2&gt;
&lt;p&gt;SSE is not a blanket replacement. WebSockets win when you truly need the bidirectional behavior.&lt;/p&gt;
&lt;p&gt;Choose WebSockets when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Client-to-server streaming&lt;/strong&gt; (e.g., real-time input streams)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collaborative editing&lt;/strong&gt; patterns where both sides send frequent updates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-latency command/control&lt;/strong&gt; where you want the flexibility of full-duplex messaging&lt;/li&gt;
&lt;li&gt;A single persistent channel that carries multiple message types in both directions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even then, it’s worth asking whether you can split responsibilities. A common pattern is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;SSE for server → client updates&lt;/strong&gt; (status, notifications, streaming output)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;regular HTTP requests&lt;/strong&gt; for client → server actions (commands, mutations)&lt;/li&gt;
&lt;li&gt;Reserve WebSockets only for the interactive bidirectional subset&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That hybrid approach often reduces the blast radius of failures and simplifies your system architecture without losing functionality.&lt;/p&gt;
&lt;h2 id="conclusion-sse-is-the-pragmatic-default-for-server-to-client-real-time"&gt;Conclusion: SSE is the pragmatic default for server-to-client “real-time”&lt;/h2&gt;
&lt;p&gt;WebSockets get the headlines, but SSE has the better fit for most real-time features. If your primary job is server-to-client updates—notifications, feeds, progress, and streaming—SSE delivers that value with a simpler mental model, more natural integration with the web stack, and built-in reconnection behavior.&lt;/p&gt;
&lt;p&gt;The quiet rise of SSE isn’t a fad. It’s teams choosing reliability and simplicity over over-engineering.&lt;/p&gt;</content></item><item><title>The State of WebAssembly in 2024: Finally Beyond the Hype Cycle</title><link>https://decastro.work/blog/state-webassembly-2024-beyond-hype/</link><pubDate>Tue, 06 Aug 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/state-webassembly-2024-beyond-hype/</guid><description>&lt;p&gt;WebAssembly stopped being a promise the moment teams started shipping it—not as a science project, but as a dependable part of production systems. In 2024, the center of gravity is clear: not “replace JavaScript,” but “run safe, portable code anywhere”—with predictable performance and much less operational drama than you get from native binaries or server-side polyglot sprawl.&lt;/p&gt;
&lt;p&gt;The hype cycle didn’t break WebAssembly; it just delayed the boring parts that matter: stable runtimes, interoperable modules, and an ecosystem good enough to trust with real workloads. What’s different now is that the pieces are finally converging.&lt;/p&gt;</description><content>&lt;p&gt;WebAssembly stopped being a promise the moment teams started shipping it—not as a science project, but as a dependable part of production systems. In 2024, the center of gravity is clear: not “replace JavaScript,” but “run safe, portable code anywhere”—with predictable performance and much less operational drama than you get from native binaries or server-side polyglot sprawl.&lt;/p&gt;
&lt;p&gt;The hype cycle didn’t break WebAssembly; it just delayed the boring parts that matter: stable runtimes, interoperable modules, and an ecosystem good enough to trust with real workloads. What’s different now is that the pieces are finally converging.&lt;/p&gt;
&lt;h2 id="why-webassembly-went-through-the-trough-and-what-changed"&gt;Why WebAssembly Went Through the Trough (and What Changed)&lt;/h2&gt;
&lt;p&gt;If you’ve followed the story, you remember the early pitch: compile to WASM, ship everywhere, get near-native performance in the browser. The problem wasn’t that WASM was bad—it was that too many use cases depended on assumptions that weren’t ready.&lt;/p&gt;
&lt;p&gt;Early friction clustered around a few themes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The runtime boundary was unclear.&lt;/strong&gt; WASM itself is a binary format, but “running code” still depends on how it interacts with the outside world (files, networking, clocks, environment variables).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling and interoperability were inconsistent.&lt;/strong&gt; Teams could compile a module, but composing multiple languages/modules into a coherent system was still messy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance arguments weren’t always actionable.&lt;/strong&gt; “Near-native” is true in the right circumstances, but production systems care about latency budgets, startup costs, memory behavior, and operational overhead—not benchmark trivia.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In 2024, those problems are shrinking. WASI preview 2 is an attempt to make “system interfaces” more capable and standardized. The component model is an attempt to make module composition less brittle and more language-agnostic. Together, they address the core operational question: &lt;em&gt;how do you safely run code in a sandbox without turning every integration into a bespoke engineering project?&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="wasi-preview-2-the-missing-piece-between-sandbox-and-system"&gt;WASI Preview 2: The Missing Piece Between “Sandbox” and “System”&lt;/h2&gt;
&lt;p&gt;WASI—the WebAssembly System Interface—has always been the bridge between “pure WASM” and something closer to a real application. But older iterations left a gap for production workloads that need richer, more predictable interactions with their host environment.&lt;/p&gt;
&lt;p&gt;WASI preview 2 matters because it aims to improve how WASM code imports capabilities from the host. Instead of treating the host as a vague black box, you can think in terms of &lt;strong&gt;capabilities and well-defined imports&lt;/strong&gt;. That shift is crucial when you’re trying to run untrusted or semi-trusted code safely.&lt;/p&gt;
&lt;p&gt;Here’s the practical difference: in a production platform, you don’t want every WASM module to come with a custom runtime shim. You want a consistent contract. When WASI gets that contract closer to “good enough for real systems,” teams can focus on what matters—business logic, security boundaries, and performance—rather than on integration archaeology.&lt;/p&gt;
&lt;p&gt;A good mental model is this: WASI preview 2 doesn’t magically eliminate all complexity, but it reduces the number of ways “running WASM” can mean “doing a one-off port.” That’s how you get from prototypes to repeatable deployments.&lt;/p&gt;
&lt;h2 id="the-component-model-stop-treating-wasm-like-a-binary-blob"&gt;The Component Model: Stop Treating WASM Like a Binary Blob&lt;/h2&gt;
&lt;p&gt;WASM modules aren’t just bytecode; they’re artifacts that need to interoperate. The early reality was that composition often meant “wire together exports and imports, hope signatures match, and pray you didn’t miss a subtle ABI detail.”&lt;/p&gt;
&lt;p&gt;The component model is the response. It’s designed to make WebAssembly interoperation more structured—so you can treat WASM units like components with explicit interfaces rather than low-level binary expectations.&lt;/p&gt;
&lt;p&gt;Why this changes the game:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fewer integration surprises.&lt;/strong&gt; When interfaces are clearer, you don’t end up debugging “why doesn’t this call land correctly?” at the worst possible time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More reliable multi-language ecosystems.&lt;/strong&gt; Teams can mix languages and toolchains more confidently when the boundary contract is standardized.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner composition for platforms.&lt;/strong&gt; If your infrastructure expects components, you can build catalogs, versioning strategies, and compatibility policies that don’t rely on tribal knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider a storefront extension system. You might have modules compiled from different languages—some authored by internal teams, others contributed by partners. Without strong interface discipline, every module becomes a bespoke integration. With the component model approach, you can create guardrails: stable interface versions, consistent data shapes, and predictable host calls.&lt;/p&gt;
&lt;p&gt;The result isn’t just developer convenience. It’s operational sanity—fewer risky updates, easier rollbacks, and a system that scales beyond a single team’s preferences.&lt;/p&gt;
&lt;h2 id="production-proof-points-platforms-that-put-wasm-to-work"&gt;Production Proof Points: Platforms That Put WASM to Work&lt;/h2&gt;
&lt;p&gt;The real story of WebAssembly in 2024 isn’t that “it’s possible.” It’s that major platforms are using it for specific, high-value scenarios where WASM’s constraints are actually advantages.&lt;/p&gt;
&lt;h3 id="shopify-safe-storefront-extensions-with-wasm-modules"&gt;Shopify: Safe storefront extensions with WASM modules&lt;/h3&gt;
&lt;p&gt;Shopify’s use of WASM for storefront extensions highlights a key strength: &lt;strong&gt;sandboxing with portability&lt;/strong&gt;. Storefront customization is inherently extensible—and inherently risky. Running extensions in WASM lets platforms limit what extensions can do, while still achieving low-latency execution compared to heavier isolation mechanisms.&lt;/p&gt;
&lt;p&gt;In other words, Shopify isn’t trying to turn every storefront feature into WASM. It’s using WASM where it provides leverage: safe extensibility without giving every extension the keys to the kingdom.&lt;/p&gt;
&lt;h3 id="figma-performance-sensitive-behavior-benefits-from-wasm"&gt;Figma: Performance-sensitive behavior benefits from WASM&lt;/h3&gt;
&lt;p&gt;Figma’s performance depends on interactive workloads where milliseconds matter. WASM is a compelling option when you want deterministic performance characteristics and the ability to run compute-heavy logic without forcing the entire product into a single language stack.&lt;/p&gt;
&lt;p&gt;The important nuance: WASM becomes most valuable when it targets a “hot path”—the part of the app that truly benefits from compiled performance and consistent execution behavior.&lt;/p&gt;
&lt;h3 id="cloudflare-workers-wasm-for-portable-server-side-workloads"&gt;Cloudflare Workers: WASM for portable server-side workloads&lt;/h3&gt;
&lt;p&gt;Cloudflare Workers is a canonical example of “run code safely, fast, and consistently at the edge.” WASM support for workloads here aligns with a broader trend: edge platforms need portability across runtime environments, while still enforcing isolation boundaries.&lt;/p&gt;
&lt;p&gt;Again, the pitch isn’t “WASM instead of JavaScript.” It’s “WASM for workloads that benefit from predictable sandboxing and language flexibility.”&lt;/p&gt;
&lt;h3 id="fermyon-spin-wasm-as-a-microservice-deployment-primitive"&gt;Fermyon Spin: WASM as a microservice deployment primitive&lt;/h3&gt;
&lt;p&gt;Fermyon’s Spin framework takes the “WASM beyond the browser” idea and turns it into a deployable microservice model. The advantage isn’t only isolation—it’s also packaging and portability. When you can ship services as WASM artifacts with consistent host semantics, you reduce drift between environments.&lt;/p&gt;
&lt;p&gt;This is what operational teams care about: reproducibility. If your service behavior depends on a particular runtime and interface contract, you want that contract to be stable across staging and production.&lt;/p&gt;
&lt;h2 id="practical-guidance-when-wasm-is-a-smart-bet-and-when-it-isnt"&gt;Practical Guidance: When WASM Is a Smart Bet (and When It Isn’t)&lt;/h2&gt;
&lt;p&gt;Here’s my blunt take: WASM is not a universal migration strategy. It’s a precision tool.&lt;/p&gt;
&lt;h3 id="use-wasm-when"&gt;Use WASM when…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You need strong sandboxing for third-party or untrusted code.&lt;/strong&gt; Storefront extensions and plugin architectures are the obvious fits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want language freedom without operational chaos.&lt;/strong&gt; Teams can use different ecosystems and still deploy a common runtime artifact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You have performance-critical compute.&lt;/strong&gt; Think parsing, transformation pipelines, media processing, optimization loops, or other workloads where compiled execution can reduce overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You care about portability across hosts.&lt;/strong&gt; WASM gives you a path to move code between environments more cleanly than native binaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="dont-use-wasm-when"&gt;Don’t use WASM when…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your work is mostly I/O orchestration.&lt;/strong&gt; If you’re mostly waiting on network calls or driving UI logic, the cost/benefit may not justify the boundary complexity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your app needs deep, bespoke OS integration.&lt;/strong&gt; WASI is evolving, but you still have to design around what the host interfaces provide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can’t commit to interface contracts.&lt;/strong&gt; The component model helps, but you still need discipline in data formats, versioning, and compatibility strategy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pragmatic workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Identify one subsystem&lt;/strong&gt; that is compute-heavy or security-sensitive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Define the boundary&lt;/strong&gt;: what inputs/outputs cross the WASM/host line, and what capabilities the module requires.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standardize interfaces early&lt;/strong&gt;—especially if multiple teams or partners will contribute modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure end-to-end latency&lt;/strong&gt;, not just microbenchmarks. Startup time, memory behavior, and invocation frequency often matter more than raw execution speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat compatibility like a product feature.&lt;/strong&gt; Version your component interfaces and document the upgrade path.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-conclusion-wasm-has-earned-its-seatbut-only-in-the-right-rooms"&gt;The Conclusion: WASM Has Earned Its Seat—But Only in the Right Rooms&lt;/h2&gt;
&lt;p&gt;The state of WebAssembly in 2024 isn’t “everything will be WASM.” It’s more interesting than that: WASM is finally reaching the stage where platforms can use it as a reliable deployment boundary, not a novelty.&lt;/p&gt;
&lt;p&gt;WASI preview 2 and the component model are the quiet workhorses making this practical. And the production examples—Shopify’s extensions, Figma’s performance needs, Cloudflare Workers’ edge execution, and Fermyon Spin’s microservice deployment—show the pattern: WebAssembly wins when you need &lt;strong&gt;safe portability with near-native performance&lt;/strong&gt;, applied to focused, high-value workloads.&lt;/p&gt;
&lt;p&gt;If you’re considering WASM today, don’t ask whether it can replace your stack. Ask what piece of your system benefits from a tighter sandbox, cleaner interfaces, and predictable execution. That’s where WebAssembly stops being hype and becomes engineering.&lt;/p&gt;</content></item><item><title>Terraform's Dominance Is Slipping and Pulumi Knows Why</title><link>https://decastro.work/blog/terraform-dominance-slipping-pulumi-knows-why/</link><pubDate>Wed, 31 Jul 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/terraform-dominance-slipping-pulumi-knows-why/</guid><description>&lt;p&gt;For years, Terraform rode a wave of “configuration as truth”: describe the desired state in HCL, let the engine reconcile reality, and move on. It worked—because the industry needed standardization more than it needed elegance. But as teams matured, a different problem emerged: infrastructure isn’t just configuration. It’s software. And that’s exactly where Pulumi starts to win.&lt;/p&gt;
&lt;h2 id="hcl-solved-a-problemthen-developers-hit-the-wall"&gt;HCL Solved a Problem—Then Developers Hit the Wall&lt;/h2&gt;
&lt;p&gt;HCL is a reasonable idea. Terraform’s declarative model, and its promise that you can model infrastructure resources in a dedicated language, made it approachable and portable. When you’re wiring up a few services, modules feel clean, state management is predictable, and the learning curve is manageable.&lt;/p&gt;</description><content>&lt;p&gt;For years, Terraform rode a wave of “configuration as truth”: describe the desired state in HCL, let the engine reconcile reality, and move on. It worked—because the industry needed standardization more than it needed elegance. But as teams matured, a different problem emerged: infrastructure isn’t just configuration. It’s software. And that’s exactly where Pulumi starts to win.&lt;/p&gt;
&lt;h2 id="hcl-solved-a-problemthen-developers-hit-the-wall"&gt;HCL Solved a Problem—Then Developers Hit the Wall&lt;/h2&gt;
&lt;p&gt;HCL is a reasonable idea. Terraform’s declarative model, and its promise that you can model infrastructure resources in a dedicated language, made it approachable and portable. When you’re wiring up a few services, modules feel clean, state management is predictable, and the learning curve is manageable.&lt;/p&gt;
&lt;p&gt;The wall comes when the infrastructure complexity stops being “a bunch of resources” and becomes “a system with behavior.”&lt;/p&gt;
&lt;p&gt;Real teams don’t build static stacks. They build conditional deployments (prod vs. staging), multi-tenant environments, environment-dependent naming rules, conditional networking, and resource graphs that vary depending on runtime inputs. In Terraform, this logic exists—but it’s awkward. You end up encoding software patterns using a declarative toolchain with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expressions that get hard to read and debug&lt;/li&gt;
&lt;li&gt;Repeated “plumbing” for maps, locals, and dynamic blocks&lt;/li&gt;
&lt;li&gt;Modules that grow into mini-programs with limited abstraction power&lt;/li&gt;
&lt;li&gt;Copy/paste patterns that resist refactoring because the language fights you&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;HCL isn’t wrong; it just isn’t optimized for how developers naturally reason. When your infrastructure begins to look like application code—because it effectively &lt;em&gt;is&lt;/em&gt; application code—you feel the friction.&lt;/p&gt;
&lt;h2 id="infrastructure-as-configuration-becomes-a-productivity-tax"&gt;“Infrastructure as Configuration” Becomes a Productivity Tax&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable truth: infrastructure-as-configuration treats the most valuable part of modern engineering—iteration—as an afterthought.&lt;/p&gt;
&lt;p&gt;Most engineering teams expect to do the following routinely:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;write small, testable units&lt;/li&gt;
&lt;li&gt;factor shared logic into reusable abstractions&lt;/li&gt;
&lt;li&gt;refactor safely with IDE support&lt;/li&gt;
&lt;li&gt;use existing language tooling for linting, formatting, and static analysis&lt;/li&gt;
&lt;li&gt;apply conventional control flow patterns (loops, conditionals, early returns)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Terraform can do some of this, but it doesn’t do it in the way software engineers expect. Instead, you’re limited to the constructs Terraform provides, and “logic” often becomes data transformation inside expressions. That can be fine until you need robust branching, non-trivial orchestration, or you just want the simplest possible way to validate inputs and invariants.&lt;/p&gt;
&lt;p&gt;Practical example: multi-tenant provisioning&lt;br&gt;
Imagine a platform that creates a network, deploys a database, and configures application access per tenant. Each tenant has features toggled based on a configuration file—some tenants need private endpoints, others don’t; some require extra security controls; others have simpler defaults.&lt;/p&gt;
&lt;p&gt;In a configuration-first approach, you’ll represent this as deeply nested variables and conditionals. The resulting code becomes a maze of maps, merges, and dynamic blocks. Even if it’s correct today, future changes are expensive because the “program” isn’t truly a program—it’s a declaration expressed in a constrained DSL.&lt;/p&gt;
&lt;p&gt;Once you feel that pain, the argument stops being philosophical. It becomes economic: time spent deciphering infrastructure code isn’t building value. It’s paying a tax.&lt;/p&gt;
&lt;h2 id="pulumi-treats-infrastructure-like-codebecause-it-is"&gt;Pulumi Treats Infrastructure Like Code—Because It Is&lt;/h2&gt;
&lt;p&gt;Pulumi’s core bet is straightforward: define infrastructure in real programming languages—TypeScript, Python, Go, and others—so your infrastructure enjoys the full power of the software toolchain.&lt;/p&gt;
&lt;p&gt;That means you get real loops, real conditionals, and real functions. But more importantly, you get &lt;em&gt;structure&lt;/em&gt;. You can create abstractions that match how developers think rather than forcing everything through the grammar of HCL.&lt;/p&gt;
&lt;p&gt;Consider the same multi-tenant scenario in a general-purpose language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can model the tenant configuration as data structures.&lt;/li&gt;
&lt;li&gt;You can write a function that returns the desired resources for a tenant.&lt;/li&gt;
&lt;li&gt;You can reuse shared logic naturally.&lt;/li&gt;
&lt;li&gt;You can validate inputs early (before deployment) and fail fast.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example (TypeScript-style pseudocode):&lt;br&gt;
You might have a &lt;code&gt;provisionTenant(tenantSpec)&lt;/code&gt; function that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;computes naming and tags&lt;/li&gt;
&lt;li&gt;decides whether to attach private networking&lt;/li&gt;
&lt;li&gt;creates the database with the right configuration&lt;/li&gt;
&lt;li&gt;outputs connection details&lt;/li&gt;
&lt;li&gt;optionally enables extra security resources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not just “easier.” It’s more maintainable. When the team grows, onboarding gets faster because infrastructure code reads like code. When requirements change, refactoring isn’t a gamble.&lt;/p&gt;
&lt;p&gt;And because Pulumi supports unit testing around your infrastructure logic, you can test the behavior you care about. You’re no longer restricted to hoping a deployment succeeds as your primary validation strategy.&lt;/p&gt;
&lt;h2 id="tooling-compounds-refactor-test-and-review-faster"&gt;Tooling Compounds: Refactor, Test, and Review Faster&lt;/h2&gt;
&lt;p&gt;The strongest argument for Pulumi isn’t that it can express infrastructure differently—it’s that it plugs into the development workflow teams already have.&lt;/p&gt;
&lt;p&gt;When your infrastructure is written in a real language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IDE refactoring works. Rename a function or extract a module and trust your tooling to update call sites.&lt;/li&gt;
&lt;li&gt;Static analysis works. Catch obvious issues before you run CI/CD.&lt;/li&gt;
&lt;li&gt;Unit tests are possible. You can test logic that computes resource properties, naming conventions, and policy decisions.&lt;/li&gt;
&lt;li&gt;Code review becomes sharper. Reviewers can reason about control flow, helper functions, and invariants instead of parsing nested expressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters in practice because infrastructure failures are rarely “random.” They’re almost always caused by changes: new tenants, new regions, updated credentials, toggled features, or altered security requirements. If your infrastructure logic is difficult to validate locally, you end up moving mistakes downstream—into the apply step—where feedback is slower and the blast radius is larger.&lt;/p&gt;
&lt;p&gt;Pulumi’s approach doesn’t eliminate the need for careful review and deployment discipline. It reduces the chance that you can’t even reason about the system you’re about to deploy.&lt;/p&gt;
&lt;h2 id="terraforms-ecosystem-is-realbut-pulumis-model-is-the-point"&gt;Terraform’s Ecosystem Is Real—But Pulumi’s Model Is the Point&lt;/h2&gt;
&lt;p&gt;Terraform’s ecosystem is undeniably large. There are countless modules, a huge base of existing knowledge, and mature patterns for state management and dependency graphs. For many teams, that ecosystem has been enough to justify the tradeoffs.&lt;/p&gt;
&lt;p&gt;But the tradeoffs are why dominance is slipping.&lt;/p&gt;
&lt;p&gt;When infrastructure engineering moves from “write once, deploy many” to “continuously evolve,” the ability to treat infrastructure as a maintainable software codebase becomes a strategic advantage. Pulumi’s promise isn’t only flexibility—it’s that your infrastructure code can be engineered like the rest of your systems.&lt;/p&gt;
&lt;p&gt;And the market has been quietly acknowledging this. CDK for Terraform (CDKTF) is HashiCorp’s admission that HCL wasn’t enough for many developers. CDKTF lets you define Terraform configurations using a programming language and generates HCL under the hood. In other words: even Terraform is moving toward the “actual code” model because teams want familiar abstractions, reusable libraries, and real control flow.&lt;/p&gt;
&lt;p&gt;That doesn’t mean HCL will disappear. It means the center of gravity is shifting. People want the Terraform engine and module ecosystem sometimes, but they increasingly want the developer experience benefits that come from writing infrastructure the way developers actually write software.&lt;/p&gt;
&lt;h2 id="choosing-the-right-approach-a-practical-decision-checklist"&gt;Choosing the Right Approach: A Practical Decision Checklist&lt;/h2&gt;
&lt;p&gt;If you’re evaluating whether to adopt Pulumi (or whether to stick with Terraform), don’t get distracted by debates about “purity.” Use a checklist that reflects how your team builds.&lt;/p&gt;
&lt;p&gt;Choose Pulumi if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your infrastructure logic is already complex (conditional resources, multi-tenant patterns, environment-specific branching).&lt;/li&gt;
&lt;li&gt;You want to reuse abstractions and refactor confidently with IDE tooling.&lt;/li&gt;
&lt;li&gt;You would benefit from unit tests for infrastructure decisions (naming, policy enforcement, computed configuration).&lt;/li&gt;
&lt;li&gt;Your team is staffed by developers who expect real code workflows, not DSL workarounds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stick with Terraform if:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your organization relies heavily on existing HCL modules and workflows, and migration would be costly.&lt;/li&gt;
&lt;li&gt;Your deployments are mostly static and the declarative model stays readable.&lt;/li&gt;
&lt;li&gt;You prefer the current ecosystem’s module-driven development and your teams are already productive with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here’s the compromise that often works: if you’re committed to Terraform but feel the pain of HCL logic, consider reducing logic density—push computation into fewer, cleaner modules, standardize naming and tagging, and aggressively document module contracts. If that still isn’t enough, Pulumi’s model may be the real fix.&lt;/p&gt;
&lt;h2 id="conclusion-infrastructure-is-softwarestop-treating-it-like-configuration"&gt;Conclusion: Infrastructure Is Software—Stop Treating It Like Configuration&lt;/h2&gt;
&lt;p&gt;Terraform’s HCL was a milestone: it made infrastructure approachable and standardized. But software teams didn’t stop at “approachable.” They kept building systems that behave, evolve, and grow. At that point, infrastructure isn’t configuration—it’s software engineering.&lt;/p&gt;
&lt;p&gt;Pulumi wins because it aligns infrastructure with how developers actually work: real programming languages, real abstractions, unit-testable logic, and refactoring support that doesn’t punish you for being a modern team. Terraform’s ecosystem will keep it relevant, but the direction of travel is clear: infrastructure-as-actual-code beats infrastructure-as-configuration every time.&lt;/p&gt;</content></item><item><title>FastAPI Is Eating Flask's Lunch and Django Should Pay Attention</title><link>https://decastro.work/blog/fastapi-eating-flask-lunch-django-pay-attention/</link><pubDate>Fri, 19 Jul 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/fastapi-eating-flask-lunch-django-pay-attention/</guid><description>&lt;p&gt;Python’s web ecosystem has always moved fast, but lately it feels like one framework is quietly turning “best practices” into defaults. FastAPI takes the things developers already reach for—type hints, async/await, and Pydantic models—and then does something Flask and Django users often have to cobble together: it turns that structure into request validation, predictable serialization, and excellent documentation automatically. If you’re building APIs in 2024, you’re either choosing FastAPI because it fits, or you’re choosing something else for reasons that must be painfully specific.&lt;/p&gt;</description><content>&lt;p&gt;Python’s web ecosystem has always moved fast, but lately it feels like one framework is quietly turning “best practices” into defaults. FastAPI takes the things developers already reach for—type hints, async/await, and Pydantic models—and then does something Flask and Django users often have to cobble together: it turns that structure into request validation, predictable serialization, and excellent documentation automatically. If you’re building APIs in 2024, you’re either choosing FastAPI because it fits, or you’re choosing something else for reasons that must be painfully specific.&lt;/p&gt;
&lt;h2 id="the-real-shift-type-hints-stop-being-decoration"&gt;The real shift: type hints stop being decoration&lt;/h2&gt;
&lt;p&gt;Flask is flexible, which is another way of saying it leaves responsibility on the developer. You can validate input, generate schemas, document endpoints, and serialize responses—but you build that stack yourself. Django can be even more “batteries included,” but its API story tends to be oriented around forms, models, templates, and server-rendered workflows—then adapted for APIs.&lt;/p&gt;
&lt;p&gt;FastAPI flips the philosophy: it treats type hints as part of the application contract. Instead of “Here’s a function that receives JSON,” the code says “Here’s a function that receives a &lt;em&gt;specific&lt;/em&gt; model, and here’s what it returns.” That contract is then used for three practical outcomes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Automatic request validation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic response serialization&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic API documentation&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Consider an endpoint that creates a “user” resource. With FastAPI, you define a request model and a response model, and the framework enforces and documents them without extra glue code.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; FastAPI
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pydantic &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BaseModel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; uuid &lt;span style="color:#f92672"&gt;import&lt;/span&gt; UUID, uuid4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserIn&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email: str
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; age: int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserOut&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id: UUID
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email: str
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; age: int
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fake_db &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/users&amp;#34;&lt;/span&gt;, response_model&lt;span style="color:#f92672"&gt;=&lt;/span&gt;UserOut)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;create_user&lt;/span&gt;(payload: UserIn) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; UserOut:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; uuid4()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user &lt;span style="color:#f92672"&gt;=&lt;/span&gt; UserOut(id&lt;span style="color:#f92672"&gt;=&lt;/span&gt;user_id, email&lt;span style="color:#f92672"&gt;=&lt;/span&gt;payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;email, age&lt;span style="color:#f92672"&gt;=&lt;/span&gt;payload&lt;span style="color:#f92672"&gt;.&lt;/span&gt;age)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fake_db[str(user_id)] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; user
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; user
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’re not writing “validation logic.” You’re writing &lt;em&gt;data shapes&lt;/em&gt;, and FastAPI does the rest. If a client sends &lt;code&gt;&amp;quot;age&amp;quot;: &amp;quot;thirty&amp;quot;&lt;/code&gt;, it fails fast with a structured error. That’s the difference between a framework that assists and a framework that enforces.&lt;/p&gt;
&lt;h2 id="automatic-docs-arent-a-luxurytheyre-leverage"&gt;Automatic docs aren’t a luxury—they’re leverage&lt;/h2&gt;
&lt;p&gt;Swagger UI and OpenAPI documentation are often treated as a checkbox. FastAPI makes them a byproduct of doing real modeling work.&lt;/p&gt;
&lt;p&gt;When you define endpoints with typed inputs and &lt;code&gt;response_model&lt;/code&gt;, FastAPI can generate an OpenAPI schema and interactive docs automatically. That matters because documentation becomes part of the workflow, not a follow-up chore. Your API stops being “something in a README” and becomes something clients can explore and test immediately.&lt;/p&gt;
&lt;p&gt;Try this mindset shift: imagine your team shipping an API to another team that needs examples, field meanings, and error formats. With Flask, you either maintain docs manually or integrate a documentation library. With Django, you often rely on Django REST Framework tooling and serializers—useful, but heavier. With FastAPI, the contract you already coded becomes the contract the docs show.&lt;/p&gt;
&lt;p&gt;Even better: the docs tend to stay accurate because the source of truth is the Python typing and the Pydantic models. That’s how you reduce the classic “docs drift” problem: you don’t need discipline so much as you need the framework to make correctness the path of least resistance.&lt;/p&gt;
&lt;h2 id="validation-that-reduces-back-and-forth-and-incidents"&gt;Validation that reduces back-and-forth (and incidents)&lt;/h2&gt;
&lt;p&gt;Most API failures aren’t dramatic—they’re mundane. A client sends the wrong field name. A number arrives as a string. A nested object is missing. Suddenly you’re parsing errors from logs, guessing what the caller meant, and rebuilding a tiny spec in a Slack thread.&lt;/p&gt;
&lt;p&gt;FastAPI’s validation pipeline is one of its most practical advantages. Because Pydantic models define constraints and types, FastAPI can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reject invalid requests before they hit your business logic&lt;/li&gt;
&lt;li&gt;produce consistent error responses&lt;/li&gt;
&lt;li&gt;keep serialization predictable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also express constraints directly in the model. For example, if an email must be non-empty and age must be within a range, say so in code and let the framework enforce it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pydantic &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BaseModel, Field
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserIn&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; email: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Field(min_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; age: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Field(ge&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, le&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;120&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now your API becomes harder to misuse. That’s not “developer experience” in a fluffy sense; it’s fewer broken requests reaching production. It’s less time spent writing guardrails, because you’ve already encoded them.&lt;/p&gt;
&lt;h2 id="async-in-the-places-that-matter"&gt;Async in the places that matter&lt;/h2&gt;
&lt;p&gt;FastAPI also benefits from async/await being a first-class citizen rather than an afterthought. Async is most valuable when your API spends time waiting: database calls, HTTP requests to other services, file I/O, and streaming responses.&lt;/p&gt;
&lt;p&gt;Flask can work with async-like patterns, but it historically hasn’t made the async model feel native. Django can do async views, but using Django for high-throughput API services often feels like dragging a feature-rich environment into a job it wasn’t primarily designed for.&lt;/p&gt;
&lt;p&gt;In FastAPI, async endpoints feel straightforward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; FastAPI
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; httpx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/weather&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;get_weather&lt;/span&gt;(city: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; httpx&lt;span style="color:#f92672"&gt;.&lt;/span&gt;AsyncClient() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; client:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;https://example.com/weather?city=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;city&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; resp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;: city, &lt;span style="color:#e6db74"&gt;&amp;#34;forecast&amp;#34;&lt;/span&gt;: data}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isn’t about chasing “async hype.” It’s about having a framework that doesn’t fight you when you need I/O concurrency. If your API integrates with other services (which most do), async can improve responsiveness and reduce resource burn.&lt;/p&gt;
&lt;h2 id="flask-feels-primitivebecause-youre-rebuilding-infrastructure"&gt;Flask feels primitive—because you’re rebuilding infrastructure&lt;/h2&gt;
&lt;p&gt;Flask isn’t a bad framework. It’s lightweight. But lightweight cuts both ways: when your application grows into an API platform, you end up stitching together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request parsing and validation&lt;/li&gt;
&lt;li&gt;serialization&lt;/li&gt;
&lt;li&gt;error handling patterns&lt;/li&gt;
&lt;li&gt;OpenAPI schema generation&lt;/li&gt;
&lt;li&gt;dependency injection for common concerns (auth, database sessions, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;FastAPI collapses much of that into the framework itself. The result is not just faster development—it’s fewer mismatched conventions across endpoints.&lt;/p&gt;
&lt;p&gt;A practical example: authentication and dependency injection.&lt;/p&gt;
&lt;p&gt;With FastAPI, you typically model authentication as a dependency and reuse it across endpoints. That encourages consistency: every endpoint that needs auth declares it clearly in the function signature, and the framework calls the dependency appropriately.&lt;/p&gt;
&lt;p&gt;You can keep Flask for simple services, internal tools, or projects where the API surface is tiny and unlikely to grow. But if you’re building a serious API product—one that needs validation, documentation, and predictable contracts—FastAPI’s approach tends to win because it keeps the complexity where it belongs: in the framework, not in your codebase.&lt;/p&gt;
&lt;h2 id="django-shouldnt-ignore-apis-anymore"&gt;Django shouldn’t ignore APIs anymore&lt;/h2&gt;
&lt;p&gt;Django’s “batteries included” identity is real. But that strength can become friction for API teams who want speed, clean typing, and automatic schema-driven behavior. Django’s API ecosystem (often through Django REST Framework) is capable, but it can feel heavy when you’re trying to move quickly and keep interfaces explicit.&lt;/p&gt;
&lt;p&gt;Here’s the honest trade: Django is an excellent general-purpose web platform. FastAPI is a surgical tool for API development. When you combine typing-driven contracts with automatic validation and docs, you get a development loop that’s hard to beat—especially when your team cares about correctness and client integration.&lt;/p&gt;
&lt;p&gt;Django can—and should—learn from that philosophy. Even if you keep Django for the core site, FastAPI is increasingly the “API front door” in modern architectures. Common patterns include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django for admin and core business workflows&lt;/li&gt;
&lt;li&gt;FastAPI for high-velocity external API endpoints&lt;/li&gt;
&lt;li&gt;Shared domain logic where it makes sense, without forcing one framework to become the other&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re a Django shop, this doesn’t mean abandoning Django. It means acknowledging that an API-centric workflow is now a first-class requirement, and FastAPI is delivering it with less friction.&lt;/p&gt;
&lt;h2 id="conclusion-choose-the-framework-that-turns-contracts-into-behavior"&gt;Conclusion: choose the framework that turns contracts into behavior&lt;/h2&gt;
&lt;p&gt;FastAPI isn’t “winning” because it’s trendy. It’s winning because it treats the API contract as executable code—using type hints and Pydantic models to generate validation, serialization, and documentation. Flask’s flexibility and Django’s batteries are both valuable, but if you’re building new APIs in 2024, you should ask a blunt question: do you want to build the plumbing yourself, or do you want your code’s structure to drive correctness?&lt;/p&gt;
&lt;p&gt;If your answer is the latter, FastAPI is the default choice. And if you’re still investing in Flask or Django for API work, make sure you can justify the extra effort—because clients increasingly prefer APIs that are self-describing, strictly validated, and effortless to integrate.&lt;/p&gt;</content></item><item><title>Why I'm Bullish on Elixir for AI Applications</title><link>https://decastro.work/blog/bullish-elixir-ai-applications/</link><pubDate>Sat, 13 Jul 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/bullish-elixir-ai-applications/</guid><description>&lt;p&gt;AI backends are no longer “a call, then a response.” They’re now a choreography problem: juggling multiple LLM requests at once, streaming partial output to the user, recovering from failures without collapsing the whole service, and staying polite with rate limits. After years of watching teams rebuild orchestration logic in brittle ways, I’m increasingly bullish on Elixir—not because it’s fashionable, but because the BEAM VM was built for this exact kind of concurrency.&lt;/p&gt;</description><content>&lt;p&gt;AI backends are no longer “a call, then a response.” They’re now a choreography problem: juggling multiple LLM requests at once, streaming partial output to the user, recovering from failures without collapsing the whole service, and staying polite with rate limits. After years of watching teams rebuild orchestration logic in brittle ways, I’m increasingly bullish on Elixir—not because it’s fashionable, but because the BEAM VM was built for this exact kind of concurrency.&lt;/p&gt;
&lt;h2 id="the-orchestration-problem-ai-creates-and-why-most-stacks-feel-bolted-on"&gt;The orchestration problem AI creates (and why most stacks feel bolted on)&lt;/h2&gt;
&lt;p&gt;If you’ve built an AI feature, you’ve already felt the complexity that comes with “just call the model.” A real assistant might:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request multiple LLM calls in parallel (e.g., rewrite + tool plan + answer draft),&lt;/li&gt;
&lt;li&gt;stream tokens back to the UI as they arrive,&lt;/li&gt;
&lt;li&gt;retry or fail over when one provider times out,&lt;/li&gt;
&lt;li&gt;enforce per-provider rate limits and connection limits,&lt;/li&gt;
&lt;li&gt;keep state consistent even when requests complete out of order.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most backends solve this with a patchwork of threads, async callbacks, job queues, and custom retry logic. You can make it work—but the implementation often ends up tightly coupled to one specific provider’s behavior or one specific streaming format. When requirements evolve (new models, new vendors, new streaming semantics), the glue code becomes the product.&lt;/p&gt;
&lt;p&gt;Elixir starts from a different premise: concurrency is a first-class runtime feature, not an add-on.&lt;/p&gt;
&lt;h2 id="the-beam-is-basically-an-ai-orchestration-engine-in-disguise"&gt;The BEAM is basically an AI orchestration engine in disguise&lt;/h2&gt;
&lt;p&gt;Here’s the pitch I keep coming back to: the BEAM VM’s concurrency model isn’t a “general-purpose” solution. It’s tailor-made for orchestrating lots of concurrent, independent tasks.&lt;/p&gt;
&lt;p&gt;Elixir gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lightweight processes&lt;/strong&gt;: create thousands (or more) without micromanaging threads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message passing&lt;/strong&gt;: tasks coordinate by sending messages, not by contending over shared mutable state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supervision trees&lt;/strong&gt;: failures are contained and recovered in predictable ways.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fault-tolerant design patterns&lt;/strong&gt;: the runtime encourages architectures where components can crash safely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That maps beautifully to an AI backend.&lt;/p&gt;
&lt;h3 id="a-concrete-example-parallel-llm-calls-with-deterministic-coordination"&gt;A concrete example: parallel LLM calls with deterministic coordination&lt;/h3&gt;
&lt;p&gt;Imagine an endpoint that must:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ask for “plan” from Model A,&lt;/li&gt;
&lt;li&gt;Ask for “critique” from Model B,&lt;/li&gt;
&lt;li&gt;Ask for “final answer” from Model C,&lt;/li&gt;
&lt;li&gt;Stream the final answer as tokens arrive,&lt;/li&gt;
&lt;li&gt;If critique fails, still produce an answer (but with fewer safeguards).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In Elixir, you can structure each LLM request as a separate process. Each process sends results back to a coordinator process as messages arrive. If one child process crashes due to a timeout, the supervisor can restart it (or replace it) according to a policy you define—without bringing down the whole request flow.&lt;/p&gt;
&lt;p&gt;This is a mindset shift: you aren’t trying to prevent failures; you’re designing for them.&lt;/p&gt;
&lt;h2 id="streaming-is-not-an-afterthoughtits-the-shape-of-the-solution"&gt;Streaming is not an afterthought—it&amp;rsquo;s the shape of the solution&lt;/h2&gt;
&lt;p&gt;For AI UX, streaming isn’t optional. Users don’t want to wait for “the whole paragraph.” They want momentum: tokens appearing, thoughts forming, partial results updating.&lt;/p&gt;
&lt;p&gt;Elixir’s concurrency model supports streaming naturally because you can treat “token arrival” as an event stream. Instead of buffering everything before responding, you can forward partial data as it comes in.&lt;/p&gt;
&lt;p&gt;Then comes the practical magic: if you’re using &lt;strong&gt;Phoenix LiveView&lt;/strong&gt;, you can stream updates from the server to the browser in real time. In practical terms, your backend doesn’t need to invent yet another websocket layer or reconcile UI state manually. LiveView is already built around incremental UI updates driven by server events.&lt;/p&gt;
&lt;h3 id="what-this-looks-like-in-practice"&gt;What this looks like in practice&lt;/h3&gt;
&lt;p&gt;Say your system streams tokens from the provider to your backend, and your backend streams them to the UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A “streaming” process consumes provider chunks (tokens, deltas, tool events).&lt;/li&gt;
&lt;li&gt;It sends each chunk to a coordinator (or directly to a UI state process).&lt;/li&gt;
&lt;li&gt;LiveView receives events and updates the DOM incrementally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the stream is interrupted, you can decide whether to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retry from the last known state,&lt;/li&gt;
&lt;li&gt;fall back to a non-streaming response,&lt;/li&gt;
&lt;li&gt;or gracefully stop and show a “partial output” message.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s exactly where supervision and message passing shine: resilience without spaghetti.&lt;/p&gt;
&lt;h2 id="rate-limits-connection-pools-and-provider-chaosmanaged-not-endured"&gt;Rate limits, connection pools, and provider chaos—managed, not endured&lt;/h2&gt;
&lt;p&gt;LLM providers are not predictable. One vendor throttles aggressively. Another occasionally hangs instead of timing out cleanly. Some have streaming quirks. Others return malformed partial chunks. And they all have different rate limits.&lt;/p&gt;
&lt;p&gt;A mature AI backend needs to enforce constraints systematically—per provider, per key, sometimes per tenant.&lt;/p&gt;
&lt;p&gt;In Elixir, the runtime encourages you to centralize control. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Connection pooling&lt;/strong&gt; can be managed by dedicated processes responsible for HTTP clients.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; can be enforced by a token-bucket process per provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; can be modeled as processes that track failures and temporarily refuse new work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retries&lt;/strong&gt; can be explicit and bounded, tied to supervision policies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the opinionated part: if your provider orchestration logic is scattered across controllers and random utility functions, your future self will hate you. In Elixir, you can concentrate orchestration in the processes that own the policy.&lt;/p&gt;
&lt;h3 id="practical-tip-keep-coordination-separate-from-io"&gt;Practical tip: keep coordination separate from I/O&lt;/h3&gt;
&lt;p&gt;A clean pattern is to separate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the process that talks to the LLM API (I/O + parsing),&lt;/li&gt;
&lt;li&gt;the process that coordinates workflow (what to do when which result arrives),&lt;/li&gt;
&lt;li&gt;the process that manages policy (rate limiting, retries, fallback routing).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reduces coupling and makes it easy to swap providers without rewriting your whole application.&lt;/p&gt;
&lt;h2 id="the-ai-stack-in-elixir-isnt-one-thingits-a-coherent-ecosystem"&gt;The “AI stack” in Elixir isn’t one thing—it’s a coherent ecosystem&lt;/h2&gt;
&lt;p&gt;It’s tempting to claim Elixir is a complete AI solution, but that’s not the real advantage. The real advantage is that Elixir plays well with an evolving AI ecosystem while staying strong where it matters: concurrency, orchestration, and streaming.&lt;/p&gt;
&lt;p&gt;A few components worth calling out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nx&lt;/strong&gt; for numerical computing when you need tensor operations or lightweight model-related work in Elixir land.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bumblebee&lt;/strong&gt; for running models locally—useful when you want edge inference or to prototype without constantly paying API costs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LiveView&lt;/strong&gt; for streaming results directly to the browser, turning “AI output” into an interactive experience rather than a delayed blob.&lt;/li&gt;
&lt;li&gt;Plus the BEAM runtime primitives (processes, supervision, message passing) that make the whole system resilient.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The takeaway: even when inference happens outside BEAM, BEAM can still be the conductor. And orchestration is where most AI backends get messy.&lt;/p&gt;
&lt;h2 id="designing-an-ai-backend-with-beam-friendly-architecture"&gt;Designing an AI backend with BEAM-friendly architecture&lt;/h2&gt;
&lt;p&gt;If you want to get real benefits (instead of just writing Elixir “because”), treat your AI backend as a set of supervised components.&lt;/p&gt;
&lt;h3 id="1-use-a-supervisor-tree-that-mirrors-your-failure-domains"&gt;1) Use a supervisor tree that mirrors your failure domains&lt;/h3&gt;
&lt;p&gt;Examples of failure domains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;per-provider HTTP client failures,&lt;/li&gt;
&lt;li&gt;per-model streaming failures,&lt;/li&gt;
&lt;li&gt;per-tenant rate limiting,&lt;/li&gt;
&lt;li&gt;per-workflow orchestration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When something breaks, you want it to restart within the right boundary. Not everything should restart because one provider returned a bad chunk.&lt;/p&gt;
&lt;h3 id="2-model-the-workflow-as-messages-not-shared-state"&gt;2) Model the workflow as messages, not shared state&lt;/h3&gt;
&lt;p&gt;Coordinator processes that receive “plan ready,” “critique ready,” and “stream token” messages tend to be simpler than shared-state approaches. You can keep request-scoped state inside the coordinator and drop it when the workflow completes.&lt;/p&gt;
&lt;h3 id="3-make-fallback-behavior-explicit"&gt;3) Make fallback behavior explicit&lt;/h3&gt;
&lt;p&gt;Don’t bury fallback logic in random rescue blocks. Decide up front what happens when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stream ends early,&lt;/li&gt;
&lt;li&gt;a call times out,&lt;/li&gt;
&lt;li&gt;one model is down,&lt;/li&gt;
&lt;li&gt;a provider returns invalid output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then implement those choices in the coordinator and/or policy processes.&lt;/p&gt;
&lt;h3 id="4-stream-early-confirm-later"&gt;4) Stream early, confirm later&lt;/h3&gt;
&lt;p&gt;If you can provide partial output quickly, do it. But keep validation and post-processing separate so you can keep streaming while you check. Users experience responsiveness; your system maintains correctness.&lt;/p&gt;
&lt;p&gt;This is where BEAM’s event-driven feel is a competitive advantage.&lt;/p&gt;
&lt;h2 id="conclusion-the-beam-world-arrived-earlyand-ai-finally-caught-up"&gt;Conclusion: the BEAM world arrived early—and AI finally caught up&lt;/h2&gt;
&lt;p&gt;I’m bullish on Elixir for AI applications because it doesn’t merely “support concurrency.” The BEAM was built for high-concurrency, fault-tolerant systems with message-driven orchestration and streaming-friendly workflows. AI backends are now forced to behave like those systems—running many parallel operations, streaming incremental results, and handling provider unreliability without collapsing.&lt;/p&gt;
&lt;p&gt;Elixir gives you a runtime that makes those requirements feel natural rather than heroic. And when you combine that with a practical web layer like Phoenix LiveView, you don’t just build an AI backend—you build an interactive, resilient AI product.&lt;/p&gt;
&lt;p&gt;The BEAM was designed for the world we’re entering. Elixir just makes that world easier to ship.&lt;/p&gt;</content></item><item><title>Why Every Backend Developer Should Learn Some Systems Programming</title><link>https://decastro.work/blog/backend-developer-learn-systems-programming/</link><pubDate>Sun, 07 Jul 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/backend-developer-learn-systems-programming/</guid><description>&lt;p&gt;Backend work lives at the boundary between “my code” and “the machine.” Most of the time, that boundary is a comfort blanket—runtime abstractions smooth over complexity until something goes wrong. Then you’re left staring at a container that gets OOM-killed, a Node server that crawls under load, or a Go benchmark that “should” be faster but isn’t. If you learn a bit of systems programming, those failures become explainable—and fixable.&lt;/p&gt;</description><content>&lt;p&gt;Backend work lives at the boundary between “my code” and “the machine.” Most of the time, that boundary is a comfort blanket—runtime abstractions smooth over complexity until something goes wrong. Then you’re left staring at a container that gets OOM-killed, a Node server that crawls under load, or a Go benchmark that “should” be faster but isn’t. If you learn a bit of systems programming, those failures become explainable—and fixable.&lt;/p&gt;
&lt;p&gt;You don’t need to write C professionally. You need to understand what your runtime is doing on your behalf: memory layout, threads, syscalls, and I/O behavior. Once those fundamentals click, Go, Python, and Node stop feeling like black boxes and start behaving like tools.&lt;/p&gt;
&lt;h2 id="memory-why-it-fits-locally-can-die-in-a-container"&gt;Memory: Why “it fits” locally can die in a container&lt;/h2&gt;
&lt;p&gt;Memory bugs aren’t always bugs. Often they’re mismatches between assumptions and reality—especially in containers.&lt;/p&gt;
&lt;p&gt;At a systems level, every process has a virtual address space: memory pages that the OS can map to physical RAM, swap, or file-backed regions. Your language runtime (Go GC, Python allocator, Node’s V8 + heap) builds its own memory model on top of that. When people say “it’s a memory leak,” what they sometimes really mean is “I didn’t account for the shape of allocations and the way the OS enforces limits.”&lt;/p&gt;
&lt;p&gt;Two practical examples:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OOM-kill surprise from allocator behavior&lt;/strong&gt;&lt;br&gt;
Suppose your Go service loads a large JSON payload, parses it into structs, and holds it for a while. Locally you have plenty of RAM. In production you set a container memory limit. Even if your app’s “heap usage” looks reasonable in logs, the OS may still kill the process if the RSS (resident set size) exceeds the limit. Why? Runtimes often reserve memory, grow arenas, and keep freed memory for reuse rather than returning it to the OS immediately. Virtual memory and RSS aren’t the same thing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; read both “heap” metrics and OS-level memory (like RSS), and learn how your runtime returns memory (or doesn’t). In Go, for example, GC behavior and arena growth matter. In Node, V8 heap limits and native allocations matter.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Copying and fragmentation from hidden allocations&lt;/strong&gt;&lt;br&gt;
A common backend anti-pattern is “harmless” string/byte transformations in hot paths. In systems terms, copies cost memory bandwidth and create temporary allocations that increase peak memory. The OS will happily give you virtual memory until the moment your working set spikes beyond the container limit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; profile allocation rates and reduce intermediate buffers. In many workloads, the biggest win is stopping accidental copies (e.g., avoiding unnecessary conversions between strings and byte arrays, streaming instead of buffering, using buffer pools where appropriate).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you understand pages, RSS, and allocation patterns, you stop guessing when memory alarms fire. You start predicting which code changes will shift peak memory—and by how much.&lt;/p&gt;
&lt;h2 id="threads-and-scheduling-why-your-server-feels-slow-only-sometimes"&gt;Threads and scheduling: Why your server feels slow “only sometimes”&lt;/h2&gt;
&lt;p&gt;Modern servers are concurrency-heavy, but most developers treat concurrency as a property of their language rather than of the OS. That’s why issues appear that seem nondeterministic: requests are fast until traffic rises, then latency spikes, then throughput collapses.&lt;/p&gt;
&lt;p&gt;Under the hood, threads are scheduled by the OS. Even “green threads” (user-space scheduling) ultimately map to real threads and get blocked by kernel events. Thread pools, context switches, and synchronization primitives determine how well your system uses CPU without turning contention into a tax.&lt;/p&gt;
&lt;p&gt;Consider a Node.js service under load. Node’s event loop can look “single-threaded,” but the system still relies on worker threads for certain tasks (like crypto or file operations), plus the kernel handles network I/O and timers. If your app does heavy CPU work in the main thread, your event loop stalls. Requests queue up; timeouts trigger; it feels like the server “crawled” for no reason.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What systems understanding changes:&lt;/strong&gt; you realize “fast I/O” and “slow CPU” are different bottlenecks. CPU-bound work needs to be offloaded or optimized, not just awaited.&lt;/p&gt;
&lt;p&gt;Now think about Go. Go uses M:N scheduling: many goroutines multiplex onto a smaller set of OS threads. That’s powerful, but it doesn’t remove the reality of CPU scheduling. If your goroutines contend on locks, saturate a shared resource, or create a thundering herd of runnable goroutines, the scheduler will work harder and latency will worsen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What to do in practice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn what “blocking” means at the OS level: waiting for sockets, disk, or locks.&lt;/li&gt;
&lt;li&gt;When you see latency spikes, ask: is the bottleneck CPU, contention, or I/O?&lt;/li&gt;
&lt;li&gt;Use runtime profiling tools, but interpret them with a mental model of scheduling and blocking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you never touch kernel APIs, understanding scheduling teaches you to design backpressure, avoid lock contention, and size concurrency limits based on system behavior—not vibes.&lt;/p&gt;
&lt;h2 id="syscalls-and-io-models-benchmarks-lie-when-you-ignore-the-kernel"&gt;Syscalls and I/O models: Benchmarks lie when you ignore the kernel&lt;/h2&gt;
&lt;p&gt;If you’ve ever run a Go microbenchmark and thought, “This can’t be right,” you’ve already met the kernel. Syscalls—calls from your program into the OS—aren’t free. They involve transitions between user space and kernel space, queueing, and synchronization. The cost varies by operation, but the key lesson is stable: excessive syscalls dominate performance long before clever algorithms do.&lt;/p&gt;
&lt;p&gt;The I/O model matters too. Network and disk I/O can be blocking, event-driven, or asynchronous, but the OS always mediates. When you understand that mediation, your benchmark design improves automatically.&lt;/p&gt;
&lt;p&gt;A few common benchmark traps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Measuring overhead, not work&lt;/strong&gt;&lt;br&gt;
If your benchmark performs tiny operations with lots of small reads/writes, you might be benchmarking syscall overhead and buffering behavior rather than your logic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ignoring buffering and batching&lt;/strong&gt;&lt;br&gt;
Many runtimes batch work under the hood, but it’s not guaranteed. If you use naive request-by-request writes without aggregation, you may inflate syscall counts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Testing in a “warm” environment only&lt;/strong&gt;&lt;br&gt;
Page cache effects and JIT/GC warmup can change performance dramatically. Systems knowledge helps you structure benchmarks to separate steady-state from initialization.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical advice for better performance work:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Measure at the right layer: application latency/throughput plus system metrics (CPU, context switches, network retransmits, disk I/O).&lt;/li&gt;
&lt;li&gt;Reduce syscalls in hot paths: favor buffering, batching, and streaming.&lt;/li&gt;
&lt;li&gt;Use load testing that resembles production concurrency—not just single-thread microbench loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you see syscalls as a first-class cost, you stop being surprised by performance cliffs and start designing experiments that answer real questions.&lt;/p&gt;
&lt;h2 id="memory--threads--syscalls-the-oom-killed-and-latency-spike-debugging-loop"&gt;Memory + threads + syscalls: the “OOM killed” and “latency spike” debugging loop&lt;/h2&gt;
&lt;p&gt;Systems programming isn’t about collecting trivia. It’s about shortening the debugging loop.&lt;/p&gt;
&lt;p&gt;Here’s the workflow that becomes natural once you’ve learned the layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Identify the resource pressure:&lt;/strong&gt; memory limit, CPU saturation, queue buildup, or I/O bottleneck.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Map symptoms to mechanisms:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;OOM-kill → virtual memory growth vs RSS, allocator behavior, peak working set.&lt;/li&gt;
&lt;li&gt;Latency spikes → scheduler contention, blocking points, event loop stalls, thread pool saturation.&lt;/li&gt;
&lt;li&gt;Throughput weirdness → syscall overhead, batching, network backpressure, disk cache effects.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instrument the right thing:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Add OS-level visibility alongside runtime metrics.&lt;/li&gt;
&lt;li&gt;Capture allocation profiles, goroutine/block profiles, or event loop delay.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Change code in ways that shift the underlying mechanism:&lt;/strong&gt; streaming instead of buffering, limiting concurrency, avoiding copies, reducing lock contention, or batching I/O.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can do that consistently, your backend engineering becomes less reactive. You don’t just “tune until it works.” You change the system’s behavior in a way that has a predictable effect.&lt;/p&gt;
&lt;h2 id="why-rust-and-zig-make-this-easier-than-you-think"&gt;Why Rust and Zig make this easier than you think&lt;/h2&gt;
&lt;p&gt;There’s a cultural myth that systems programming learning requires pain: long C nights, undefined behavior roulette, and manual memory management without guardrails. You can still learn those concepts safely, but you don’t have to start there.&lt;/p&gt;
&lt;p&gt;Rust and Zig are unusually good gateways for backend developers because they preserve the core systems ideas while reducing avoidable footguns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rust&lt;/strong&gt; makes ownership and borrowing feel like a design tool, not just a language feature. Learn the mental model behind memory lifetimes and you’ll write safer high-performance code—and you’ll better understand why runtimes need GC, arenas, or reference counting in the first place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zig&lt;/strong&gt; encourages explicit control and transparency. When you can see allocations, lifetimes, and calling conventions, it becomes easier to map what you learn back to how OSes and runtimes behave.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And you don’t need to build a kernel to get value. Build small but revealing programs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A tiny HTTP server that streams responses and observe memory behavior.&lt;/li&gt;
&lt;li&gt;A concurrent downloader that limits parallelism and measures latency under load.&lt;/li&gt;
&lt;li&gt;A program that reads from a file in small chunks vs large buffered reads and then compares syscall-heavy patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is not to become a systems engineer overnight. The goal is to internalize the cause-and-effect loop between “what the code asks for” and “what the OS does.”&lt;/p&gt;
&lt;h2 id="conclusion-learn-the-layers-and-your-backend-skills-compound"&gt;Conclusion: Learn the layers, and your backend skills compound&lt;/h2&gt;
&lt;p&gt;Backend development gets easier when you stop treating the runtime as an oracle. Memory, threads, and syscalls are the real contracts your code runs against. Once you understand those contracts, container OOMs become diagnosable, Node latency spikes become explainable, and Go benchmarks become trustworthy.&lt;/p&gt;
&lt;p&gt;Learn enough systems programming to build a mental model. Then keep using it—because every year you’ll work closer to the boundary where abstractions end and reality begins.&lt;/p&gt;</content></item><item><title>Llama, Mistral, and the Open-Source LLM Revolution</title><link>https://decastro.work/blog/llama-mistral-open-source-llm-revolution/</link><pubDate>Tue, 25 Jun 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/llama-mistral-open-source-llm-revolution/</guid><description>&lt;p&gt;For a long time, “building AI” meant negotiating access to proprietary models—paying API tolls, accepting platform rules, and hoping your prompts and data would never become collateral damage. Then something changed: open-source large language models stopped being a curiosity and started looking like infrastructure. Llama and Mistral didn’t just improve chatbots; they detonated the closed-AI business model’s gravity.&lt;/p&gt;
&lt;h2 id="metas-detonation-when-llama-broke-the-monopoly"&gt;Meta’s detonation: when Llama broke the monopoly&lt;/h2&gt;
&lt;p&gt;Meta didn’t set out to end the era of closed models. But when Llama 2 landed, it did the one thing proprietary vendors can’t: it gave developers a credible alternative that didn’t require permission slips.&lt;/p&gt;</description><content>&lt;p&gt;For a long time, “building AI” meant negotiating access to proprietary models—paying API tolls, accepting platform rules, and hoping your prompts and data would never become collateral damage. Then something changed: open-source large language models stopped being a curiosity and started looking like infrastructure. Llama and Mistral didn’t just improve chatbots; they detonated the closed-AI business model’s gravity.&lt;/p&gt;
&lt;h2 id="metas-detonation-when-llama-broke-the-monopoly"&gt;Meta’s detonation: when Llama broke the monopoly&lt;/h2&gt;
&lt;p&gt;Meta didn’t set out to end the era of closed models. But when Llama 2 landed, it did the one thing proprietary vendors can’t: it gave developers a credible alternative that didn’t require permission slips.&lt;/p&gt;
&lt;p&gt;The key shift wasn’t that Llama was “better.” It was that it was &lt;em&gt;useable&lt;/em&gt; by ordinary teams. Once the model weights were available, developers could run them locally, fine-tune them, and integrate them into their own products without relying on an external API as the single point of failure.&lt;/p&gt;
&lt;p&gt;That’s the real threat to vendor lock-in: when an alternative is technically viable, it becomes financially and operationally risky to keep depending on a single provider. API pricing changes. Rate limits appear. Terms of service evolve. Latency spikes. Suddenly, your AI roadmap is at the mercy of someone else’s billing dashboard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical consequence:&lt;/strong&gt; many teams stopped treating their LLM dependency as a product feature and started treating it as a &lt;em&gt;pluggable component&lt;/em&gt;. You can see this mindset in architecture choices—routing requests through an internal service, adding fallback models, and tracking costs per feature rather than per experiment.&lt;/p&gt;
&lt;h2 id="mistrals-accelerant-smaller-models-real-capability"&gt;Mistral’s accelerant: smaller models, real capability&lt;/h2&gt;
&lt;p&gt;If Llama made the open-source case, Mistral made it scalable.&lt;/p&gt;
&lt;p&gt;Mistral’s approach has consistently emphasized performance-per-parameter: models that can deliver high-quality results without demanding the kind of massive infrastructure reserved for frontier labs. In other words, open-source wasn’t just “possible”—it was increasingly “affordable.”&lt;/p&gt;
&lt;p&gt;This matters because the economics of AI products aren’t dominated by the first demo. They’re dominated by iteration speed and steady-state inference cost. A smaller, capable model can mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More frequent releases because you can experiment without waiting for budget approvals.&lt;/li&gt;
&lt;li&gt;More predictable latency for end users.&lt;/li&gt;
&lt;li&gt;Lower inference bills for production workloads.&lt;/li&gt;
&lt;li&gt;Easier deployment on your own hardware (or on cost-efficient providers).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; imagine building a support assistant for an ecommerce site. With a compact model, you can deploy it closer to your app servers, reduce round-trip latency, and tune prompts for your ticket taxonomy. With a heavier model, you might still do that—but only if the cost profile allows it. Open models expand the range of feasible products.&lt;/p&gt;
&lt;p&gt;The sharp truth is that “GPT-level” performance isn’t the only metric that matters. For many businesses, the winning move is &lt;em&gt;good enough at the right cost&lt;/em&gt;, with control over deployment, data flow, and behavior.&lt;/p&gt;
&lt;h2 id="the-quantization-boom-running-smarter-on-consumer-hardware"&gt;The quantization boom: running smarter on consumer hardware&lt;/h2&gt;
&lt;p&gt;Open-source also attracted a builder community, and builders hate waiting. As models became more common, the next bottleneck wasn’t training—it was inference.&lt;/p&gt;
&lt;p&gt;That’s where quantization enters: techniques that reduce a model’s numeric precision (for example, storing weights with fewer bits) to shrink memory usage and speed up computation. Done well, quantization makes it realistic to run capable LLMs on consumer GPUs or even smaller setups—without turning your performance into a slideshow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What this enables in practice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local development workflows:&lt;/strong&gt; Developers can test prompt strategies and tool integrations on their own machines rather than burning API calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy-sensitive deployments:&lt;/strong&gt; If your data can’t leave your network, local inference becomes a non-negotiable requirement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterative fine-tuning and evaluation:&lt;/strong&gt; You can run repeatable experiments faster when the loop doesn’t involve external services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quantization isn’t magic. You trade some quality or stability for efficiency depending on the method and target hardware. But the important shift is cultural: the community started treating model deployment as an engineering problem—not an exclusive privilege.&lt;/p&gt;
&lt;h2 id="the-moat-was-never-the-architectureit-was-the-supply-chain"&gt;The moat was never the architecture—it was the supply chain&lt;/h2&gt;
&lt;p&gt;It’s tempting to say open-source “wins” because models are getting better. But that’s not the real business lesson.&lt;/p&gt;
&lt;p&gt;The moat for AI companies was never only model architecture. It was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Training data access&lt;/strong&gt; at scale and with usable licensing,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute&lt;/strong&gt; to train and iterate quickly,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational tooling&lt;/strong&gt; to keep models reliable in production.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Architecture is comparatively easy to replicate once the community has the recipe. Compute and data are harder—and that’s why open-source threatens the old order so aggressively: it chips away at each layer of dependency.&lt;/p&gt;
&lt;p&gt;When more organizations can host models themselves, the vendor’s “platform tax” becomes optional. When teams can evaluate multiple models side-by-side, a single provider’s advantage erodes. When developers can fine-tune for specific domains, generic model performance becomes less important than your ability to shape behavior and outputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated take:&lt;/strong&gt; the closed-AI model benefited from asymmetry. Developers didn’t just buy inference—they bought uncertainty management: “Trust that the provider will handle it.” Open-source reduces that uncertainty by letting teams own the system end-to-end.&lt;/p&gt;
&lt;h2 id="building-without-lock-in-practical-patterns-that-actually-work"&gt;Building without lock-in: practical patterns that actually work&lt;/h2&gt;
&lt;p&gt;The open-source shift doesn’t just change what model you use—it changes how you design your AI product. Here are practical patterns teams are adopting to avoid painting themselves into a corner:&lt;/p&gt;
&lt;h3 id="1-use-an-internal-model-gateway"&gt;1) Use an internal model gateway&lt;/h3&gt;
&lt;p&gt;Instead of calling a vendor API directly from your app, route all LLM requests through an internal service. This gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;centralized prompt/version management,&lt;/li&gt;
&lt;li&gt;consistent formatting,&lt;/li&gt;
&lt;li&gt;cost tracking,&lt;/li&gt;
&lt;li&gt;model switching and fallbacks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you start with open-source now, you can still migrate later—but you won’t have to rewrite your application to do it.&lt;/p&gt;
&lt;h3 id="2-separate-model-logic-from-product-logic"&gt;2) Separate “model logic” from “product logic”&lt;/h3&gt;
&lt;p&gt;Treat the model as a component that transforms inputs to outputs. Keep your business rules outside the model: validation, policy enforcement, tool selection, and structured formatting. That makes behavior more predictable across different models.&lt;/p&gt;
&lt;p&gt;A common winning approach is to push the model toward structured outputs (JSON schemas, constrained formats) and enforce them at the application layer.&lt;/p&gt;
&lt;h3 id="3-build-retrieval-and-context-like-its-a-first-class-feature"&gt;3) Build retrieval and context like it’s a first-class feature&lt;/h3&gt;
&lt;p&gt;Most real-world LLM systems aren’t “pure chat.” They’re retrieval-augmented generation (RAG), workflows, and constrained agents. Open-source models make it feasible to customize this entire stack—so the advantage shifts from “which model?” to “how well do you feed and verify the output?”&lt;/p&gt;
&lt;h3 id="4-plan-evaluation-like-you-plan-security"&gt;4) Plan evaluation like you plan security&lt;/h3&gt;
&lt;p&gt;Don’t judge by a handful of examples. Create test suites that represent real user queries, failure modes, and regressions. Track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;refusal behavior and safety,&lt;/li&gt;
&lt;li&gt;factuality and citation quality (when using retrieval),&lt;/li&gt;
&lt;li&gt;formatting compliance,&lt;/li&gt;
&lt;li&gt;latency and cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you can’t evaluate reliably, model choice will feel like vibes.&lt;/p&gt;
&lt;h2 id="data-privacy-and-cost-the-unglamorous-wins"&gt;Data privacy and cost: the unglamorous wins&lt;/h2&gt;
&lt;p&gt;The revolution sounds glamorous, but the biggest benefits are boring—and that’s why they matter.&lt;/p&gt;
&lt;p&gt;When you can run LLMs in your own environment, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep sensitive text and documents within your infrastructure,&lt;/li&gt;
&lt;li&gt;control logging policies,&lt;/li&gt;
&lt;li&gt;reduce exposure to third-party retention rules,&lt;/li&gt;
&lt;li&gt;avoid surprising data handling changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And when you’re not paying per token to a vendor for every iteration, you can afford to improve the product instead of optimizing it down to the cheapest prompt that “kind of works.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; a legal or HR workflow assistant often needs strict controls around document handling. With a closed API, every new integration risks a compliance review. With a self-hosted model (plus a well-designed retrieval layer), you can standardize the data path and keep approvals from reinventing themselves every quarter.&lt;/p&gt;
&lt;h2 id="conclusion-open-models-are-becoming-the-default-infrastructure"&gt;Conclusion: open models are becoming the default infrastructure&lt;/h2&gt;
&lt;p&gt;Llama and Mistral didn’t just offer better chat. They shifted the balance of power by making LLM capability portable, testable, and deployable. The open-source ecosystem—quantization, tooling, fine-tuning practices—turned model ownership into a practical engineering option rather than a distant fantasy.&lt;/p&gt;
&lt;p&gt;The closed-AI era didn’t end because proprietary models stopped being good. It ended because the dependency was no longer inevitable. Open-source is eroding the old moat, and it’s doing it in the most consequential way possible: by letting developers build AI features without asking permission every step of the way.&lt;/p&gt;</content></item><item><title>The htmx vs. React Debate Is Missing the Point Entirely</title><link>https://decastro.work/blog/htmx-vs-react-debate-missing-point/</link><pubDate>Thu, 13 Jun 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/htmx-vs-react-debate-missing-point/</guid><description>&lt;p&gt;Every few weeks, the same argument resurfaces: htmx is “the future,” React is “too heavy,” or vice versa. It’s loud, opinionated, and rarely useful. The real problem is simpler—and more practical: both camps often talk past each other because they’re not solving the same kinds of systems.&lt;/p&gt;
&lt;p&gt;htmx and React aren’t rivals in the way people claim. They’re tools optimized for different architectural realities. Pick based on the shape of your application, where state lives, and how your team ships software—not based on who’s winning the comment thread.&lt;/p&gt;</description><content>&lt;p&gt;Every few weeks, the same argument resurfaces: htmx is “the future,” React is “too heavy,” or vice versa. It’s loud, opinionated, and rarely useful. The real problem is simpler—and more practical: both camps often talk past each other because they’re not solving the same kinds of systems.&lt;/p&gt;
&lt;p&gt;htmx and React aren’t rivals in the way people claim. They’re tools optimized for different architectural realities. Pick based on the shape of your application, where state lives, and how your team ships software—not based on who’s winning the comment thread.&lt;/p&gt;
&lt;h2 id="what-the-debate-gets-wrong-better-isnt-a-category"&gt;What the Debate Gets Wrong: “Better” Isn’t a Category&lt;/h2&gt;
&lt;p&gt;“Which is better?” sounds productive until you notice it’s usually not asked in context. Better for what? Better for which constraints? Better for what team composition? Better for which latency model? Better for what maintenance pattern?&lt;/p&gt;
&lt;p&gt;In practice, the decision is less like choosing a sports car and more like choosing a building material. Concrete and steel can both build bridges. But if you’re trying to assemble a temporary bridge quickly, you’ll likely make different tradeoffs than if you’re building something that must survive decades of load.&lt;/p&gt;
&lt;p&gt;The htmx vs. React argument often fails because it treats the UI framework as the primary variable. In reality, the primary variable is the architecture: who owns state, how much logic runs on the server, and how complex interactions need to be.&lt;/p&gt;
&lt;h2 id="when-htmx-wins-backend-owned-state-and-server-as-the-source-of-truth"&gt;When htmx Wins: Backend-Owned State and Server as the Source of Truth&lt;/h2&gt;
&lt;p&gt;htmx shines when the server is the natural home for your data and state. If your application already thinks in terms of requests, resources, permissions, and server-rendered views, htmx lets you keep that mental model while still delivering a modern “app-like” experience.&lt;/p&gt;
&lt;h3 id="concrete-examples-where-htmx-is-a-great-fit"&gt;Concrete examples where htmx is a great fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Admin panels and internal tools:&lt;/strong&gt; You typically have CRUD flows, table views, filters, and form edits. The server already knows what the user is allowed to do. With htmx, you can progressively enhance without building a full client application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content sites with interactive widgets:&lt;/strong&gt; Think comment forms, “load more” sections, search, voting, or admin moderation. Most interactions don’t need a fully client-managed state machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflows tied to backend invariants:&lt;/strong&gt; If a state transition is enforced by business rules—like “you can’t approve until prerequisites are met”—keeping that logic server-side is not just convenient, it’s safer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-the-architecture-matters"&gt;Why the architecture matters&lt;/h3&gt;
&lt;p&gt;htmx encourages you to treat HTML as the UI contract. You return markup that represents the new state, and you swap it into the page. When the backend is already the source of truth, this pattern reduces duplication. You avoid writing two versions of reality: one in the server and one in the browser.&lt;/p&gt;
&lt;p&gt;And you avoid the “SPA tax” that hits teams hard: authentication edge cases, routing complexity, caching, rehydration, and state synchronization. You don’t need all of that if your system’s authority is the server.&lt;/p&gt;
&lt;h2 id="when-react-wins-complex-client-side-state-and-rich-interactive-ux"&gt;When React Wins: Complex Client-Side State and Rich, Interactive UX&lt;/h2&gt;
&lt;p&gt;React excels when the browser must own meaningful local state and coordinate complex UI behavior that doesn’t map cleanly to round-trips.&lt;/p&gt;
&lt;h3 id="concrete-examples-where-react-is-a-great-fit"&gt;Concrete examples where React is a great fit&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Collaborative editors:&lt;/strong&gt; Cursor positions, selections, operational transforms/CRDT updates, and frequent incremental changes benefit from client-managed state and optimized rendering paths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time dashboards:&lt;/strong&gt; Live updates, streaming data, optimistic UI, and intricate layout logic are often easiest when the client maintains a local model of what’s on screen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex component ecosystems:&lt;/strong&gt; If your product needs deeply interactive UI primitives—drag-and-drop, advanced filtering, multi-step forms with intricate validation—React’s component model and local state management become compelling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-the-architecture-matters-1"&gt;Why the architecture matters&lt;/h3&gt;
&lt;p&gt;When the interaction model is “the user is constantly changing something” and those changes need to feel immediate, a server round-trip isn’t just slower—it can be architecturally awkward. You want predictable UI state transitions, local derivations, and a rendering model that updates quickly.&lt;/p&gt;
&lt;p&gt;React doesn’t just provide a view layer. It pushes you toward a design where the UI is a function of state, and that state can be complex and transient. That’s a good thing when the client is the natural engine of the interaction.&lt;/p&gt;
&lt;h2 id="the-real-decision-framework-where-does-state-live"&gt;The Real Decision Framework: Where Does State Live?&lt;/h2&gt;
&lt;p&gt;Here’s the practical way to choose without tribal noise: identify where your application’s state lives and how often it changes.&lt;/p&gt;
&lt;p&gt;Ask these questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is the backend the source of truth?&lt;/strong&gt;&lt;br&gt;
If permissions, validation, and invariants are authoritative on the server, htmx aligns naturally.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do you need rich local state?&lt;/strong&gt;&lt;br&gt;
If the UI depends on client-only details—like ephemeral UI state, optimistic changes, or interaction-heavy workflows—React is built for that.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How “round-trip friendly” are your interactions?&lt;/strong&gt;&lt;br&gt;
CRUD-heavy flows and form submissions are usually fine with request/response patterns. Fine-grained, continuous interaction tends to favor client state.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What’s your team’s operating model?&lt;/strong&gt;&lt;br&gt;
If your team is strongest in server development and wants to ship HTML-driven features quickly, htmx can reduce friction. If your team already lives in the client ecosystem and can maintain a component architecture, React can be a multiplier.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why the debate misses the point: it assumes the UI layer is the main event. The main event is the system boundary between server and client.&lt;/p&gt;
&lt;h2 id="a-better-pattern-combine-them-instead-of-worshipping-one"&gt;A Better Pattern: Combine Them Instead of Worshipping One&lt;/h2&gt;
&lt;p&gt;Here’s the opinionated take that should end most arguments: you can—and often should—use both. Many teams do.&lt;/p&gt;
&lt;p&gt;A common hybrid approach looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;htmx&lt;/strong&gt; for server-rendered pages and incremental updates: pagination, filters, admin actions, form submissions, and small interactive fragments.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;React&lt;/strong&gt; for the parts that genuinely need client-owned complexity: a collaborative editor, a heavily interactive visualization, or a complex multi-step workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t compromise; it’s correctness. You’re choosing the simplest tool that fits each problem. If your “hard part” is isolated, you don’t need to drag the whole product into a single architectural style.&lt;/p&gt;
&lt;h3 id="example-an-admin-tool-with-one-react-island"&gt;Example: an admin tool with one React island&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The admin panel is server-rendered. Tables, editing forms, and status changes use htmx swaps.&lt;/li&gt;
&lt;li&gt;One section—say, an advanced configuration visualizer—uses React because it needs rich client-side state and a responsive UI.&lt;/li&gt;
&lt;li&gt;The boundary stays clean: the React component can fetch data and send updates, while the rest of the system remains consistent with server truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps your product coherent and your engineering effort focused where it matters.&lt;/p&gt;
&lt;h2 id="common-failure-modes-and-how-to-avoid-them"&gt;Common Failure Modes (And How to Avoid Them)&lt;/h2&gt;
&lt;p&gt;Both sides have predictable ways to shoot themselves in the foot.&lt;/p&gt;
&lt;h3 id="failure-mode-for-htmx"&gt;Failure mode for htmx&lt;/h3&gt;
&lt;p&gt;If you try to shoehorn a highly stateful, component-driven UI into htmx patterns, you’ll end up fighting the model. You’ll be forced to invent client-side state synchronization anyway—at which point you may as well ask whether React (or another client state system) would have been simpler.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Treat htmx as a server-centric enhancement tool. When local state complexity grows beyond “swap some markup,” isolate that complexity.&lt;/p&gt;
&lt;h3 id="failure-mode-for-react"&gt;Failure mode for React&lt;/h3&gt;
&lt;p&gt;If you build a full SPA for an app where the server already owns the truth, you’ll spend time recreating things the server could have handled cleanly. The result can be a lot of glue code for routing, caching, permissions, and data consistency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Don’t default to React when your problem is mostly CRUD, server-rendered pages, and backend-enforced invariants. Consider whether a request/response UI would reduce risk and speed up iteration.&lt;/p&gt;
&lt;h2 id="conclusion-stop-debating-tools-start-designing-systems"&gt;Conclusion: Stop Debating Tools, Start Designing Systems&lt;/h2&gt;
&lt;p&gt;The htmx vs. React debate isn’t really about htmx or React. It’s about system design: where state lives, how interactions behave, and what your application must guarantee.&lt;/p&gt;
&lt;p&gt;Pick the tool that matches your architecture, not the one that wins your timeline. If the backend is the source of truth and interactions are mostly request-friendly, htmx will make you faster and your UI more consistent. If the client must manage complex, transient state with highly responsive interactions, React will save you from contortions.&lt;/p&gt;
&lt;p&gt;Make the boundary intentional, and the “war” goes away.&lt;/p&gt;</content></item><item><title>The Database Branching Revolution: Neon, PlanetScale, and the End of Migration Fear</title><link>https://decastro.work/blog/database-branching-revolution-end-migration-fear/</link><pubDate>Fri, 07 Jun 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/database-branching-revolution-end-migration-fear/</guid><description>&lt;p&gt;Deployments should feel like releases, not rituals. Yet for most teams, the scariest moment still isn’t the new UI or the background job—it’s the database migration. That tiny blob of SQL that “should be safe” somehow becomes a roulette wheel: lock timeouts, unexpected data shapes, irreversible changes, and the dreaded rollback plan that only works in theory.&lt;/p&gt;
&lt;p&gt;The good news: the ecosystem finally has a better answer. Neon’s database branching and PlanetScale’s deploy requests (plus preview environments) change the entire psychology of migration work. Instead of betting production, you can test the exact reality of production—schema and data—before you flip the switch.&lt;/p&gt;</description><content>&lt;p&gt;Deployments should feel like releases, not rituals. Yet for most teams, the scariest moment still isn’t the new UI or the background job—it’s the database migration. That tiny blob of SQL that “should be safe” somehow becomes a roulette wheel: lock timeouts, unexpected data shapes, irreversible changes, and the dreaded rollback plan that only works in theory.&lt;/p&gt;
&lt;p&gt;The good news: the ecosystem finally has a better answer. Neon’s database branching and PlanetScale’s deploy requests (plus preview environments) change the entire psychology of migration work. Instead of betting production, you can test the exact reality of production—schema and data—before you flip the switch.&lt;/p&gt;
&lt;h2 id="why-migrations-feel-existential-and-why-they-shouldnt"&gt;Why migrations feel existential (and why they shouldn’t)&lt;/h2&gt;
&lt;p&gt;Migrations are anxiety-inducing for a few practical reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;They’re stateful.&lt;/strong&gt; Schema changes depend on real data. A “harmless” &lt;code&gt;ALTER TABLE&lt;/code&gt; can behave very differently depending on indexing, row counts, and existing constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They’re sequential.&lt;/strong&gt; You can’t always treat migrations like stateless code. They modify shared infrastructure that other requests depend on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They’re hard to validate.&lt;/strong&gt; Unit tests don’t cover production data distributions. Staging often diverges quietly: missing rows, different null rates, different edge-case strings, different query patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback is a myth.&lt;/strong&gt; Many migrations aren’t safely reversible. Even reversible ones often require time, locks, and operational discipline that you only have &lt;em&gt;after&lt;/em&gt; something is already on fire.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a grim engineering loop: you make a best guess, deploy during a low-traffic window, watch metrics, and hope that “should be fine” stays true.&lt;/p&gt;
&lt;p&gt;That’s not a discipline problem. It’s a tooling and workflow problem.&lt;/p&gt;
&lt;h2 id="neons-database-branching-production-grade-previews-without-the-paranoia"&gt;Neon’s database branching: production-grade previews without the paranoia&lt;/h2&gt;
&lt;p&gt;Neon’s database branching is the most important shift because it treats your database like versioned work—not a single mutable blob.&lt;/p&gt;
&lt;p&gt;With database branching, you can create isolated branches of your production database that include both &lt;strong&gt;schema and data&lt;/strong&gt;, then work in those branches safely. In other words: your staging environment stops being a fragile approximation and becomes a faithful sandbox.&lt;/p&gt;
&lt;p&gt;Here’s what that changes in practice.&lt;/p&gt;
&lt;h3 id="replace-staging-is-close-enough-with-staging-is-exact"&gt;Replace “staging is close enough” with “staging is exact”&lt;/h3&gt;
&lt;p&gt;Imagine you’re preparing a migration that changes a column type or adds a constraint. Traditionally, you’d run it on a staging copy and hope it behaves. But staging might not include the edge cases that exist in production—especially with messy historical data.&lt;/p&gt;
&lt;p&gt;With branching, you can create a branch from production and run the migration against &lt;em&gt;real production-shaped data&lt;/em&gt;. That lets you test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whether the migration completes within your operational time budget&lt;/li&gt;
&lt;li&gt;whether backfills (if any) behave correctly&lt;/li&gt;
&lt;li&gt;whether queries and code paths behave as expected with the new schema&lt;/li&gt;
&lt;li&gt;whether unexpected rows violate new constraints&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="think-feature-branch-but-for-your-database"&gt;Think “feature branch,” but for your database&lt;/h3&gt;
&lt;p&gt;This is the mental model that matters. You’re not just “running a migration somewhere.” You’re creating a &lt;strong&gt;branch&lt;/strong&gt; that your team can iterate on.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a Neon branch from production for a new feature (say, &lt;code&gt;orders-v2&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Apply your migration(s) on that branch.&lt;/li&gt;
&lt;li&gt;Deploy the application version that expects the new schema into a preview environment wired to that branch.&lt;/li&gt;
&lt;li&gt;Run tests and manual verification against production-like data.&lt;/li&gt;
&lt;li&gt;When you’re confident, merge the change into the main migration line.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The win isn’t only technical—it’s emotional. Engineers stop treating database changes as a leap of faith and start treating them like normal development work.&lt;/p&gt;
&lt;h3 id="practical-advice-use-branches-to-validate-the-hard-parts"&gt;Practical advice: use branches to validate the &lt;em&gt;hard parts&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;Not every migration deserves equal fear, but some deserve real validation. Branching is especially valuable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backfills and data transformations&lt;/strong&gt; (because they reveal real data pain)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constraint additions&lt;/strong&gt; (&lt;code&gt;NOT NULL&lt;/code&gt;, &lt;code&gt;UNIQUE&lt;/code&gt;, foreign keys) (because they fail on ugly rows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type changes&lt;/strong&gt; (because implicit conversions can be surprising)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index changes&lt;/strong&gt; that can impact query plans (because the data distribution matters)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever stared at a migration and thought “I hope our production data doesn’t do something weird,” you’ve already identified the kinds of changes branching should cover.&lt;/p&gt;
&lt;h2 id="planetscale-deploy-requests-schema-changes-without-stopping-the-world"&gt;PlanetScale deploy requests: schema changes without stopping the world&lt;/h2&gt;
&lt;p&gt;Branching helps you validate—but you still need a safe rollout path. PlanetScale’s deploy requests take care of the operational part by applying schema changes with &lt;strong&gt;zero downtime&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The point here is not just “no downtime” as a headline. It’s that deploys become less binary. When the platform can apply changes in a way that avoids service interruption, you can focus on correctness rather than firefighting.&lt;/p&gt;
&lt;h3 id="how-this-changes-rollout-strategy"&gt;How this changes rollout strategy&lt;/h3&gt;
&lt;p&gt;In many teams, the migration rollout strategy becomes a choreography of fear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deploy schema change in one release&lt;/li&gt;
&lt;li&gt;deploy code change in a later release&lt;/li&gt;
&lt;li&gt;keep backward compatibility windows alive forever&lt;/li&gt;
&lt;li&gt;coordinate carefully to avoid breaking requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zero-downtime schema application doesn’t eliminate compatibility concerns entirely, but it drastically reduces the urgency. You can use deploy requests to move faster without forcing every migration into a slow, two-step process.&lt;/p&gt;
&lt;h3 id="practical-advice-pair-planetscale-deploy-requests-with-explicit-verification"&gt;Practical advice: pair PlanetScale deploy requests with explicit verification&lt;/h3&gt;
&lt;p&gt;Even with zero downtime, correctness still matters. A good rollout workflow looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Neon to validate the migration against production-shaped data in preview environments&lt;/li&gt;
&lt;li&gt;Prepare the PlanetScale deploy request with the schema change&lt;/li&gt;
&lt;li&gt;Roll out with monitoring focused on the specific risks (constraints, query latency, write correctness)&lt;/li&gt;
&lt;li&gt;Keep an eye on application-level signals (not just database health)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zero downtime removes one class of risk. Branching removes another. Together, they shrink the entire surface area of “migration fear.”&lt;/p&gt;
&lt;h2 id="preview-environments-test-the-exact-data-shape-before-you-ship"&gt;Preview environments: test the exact data shape before you ship&lt;/h2&gt;
&lt;p&gt;Branching gives you production-grade isolation; preview environments give you end-to-end confidence. The combo is where migrations stop being existential.&lt;/p&gt;
&lt;p&gt;Instead of asking, “Will this migration break production?”, you ask something more constructive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Does the application behave correctly with the migrated schema?”&lt;/li&gt;
&lt;li&gt;“Do critical flows work with real data shapes?”&lt;/li&gt;
&lt;li&gt;“Do we still handle edge cases correctly after the new constraint or type?”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-concrete-example-adding-a-uniqueness-constraint"&gt;A concrete example: adding a uniqueness constraint&lt;/h3&gt;
&lt;p&gt;Suppose you want to enforce that &lt;code&gt;email&lt;/code&gt; is unique on &lt;code&gt;users&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The scariest part of this migration isn’t the SQL statement—it’s the assumption that there are no duplicates lurking in production. Staging might be clean. Production probably isn’t.&lt;/p&gt;
&lt;p&gt;With Neon branching:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a branch from production.&lt;/li&gt;
&lt;li&gt;Apply the migration adding the unique constraint.&lt;/li&gt;
&lt;li&gt;Run a preview deployment of the code that expects the constraint to exist.&lt;/li&gt;
&lt;li&gt;Let your tests (and a few targeted manual checks) reveal duplicates or broken assumptions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the migration fails on the branch, you fix the issue &lt;em&gt;before&lt;/em&gt; it ever reaches the production runway. The “fear” becomes a routine, solvable engineering task: cleaning data, adjusting the migration approach, or introducing a safe backfill.&lt;/p&gt;
&lt;h2 id="a-new-workflow-git-style-confidence-for-database-changes"&gt;A new workflow: Git-style confidence for database changes&lt;/h2&gt;
&lt;p&gt;The most useful way to think about this revolution is “database changes should work like code changes.”&lt;/p&gt;
&lt;p&gt;That means you treat database schema work the same way you treat application work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create an isolated branch&lt;/li&gt;
&lt;li&gt;build and test in that branch&lt;/li&gt;
&lt;li&gt;validate behavior against production-shaped reality&lt;/li&gt;
&lt;li&gt;promote the change through controlled steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a workflow your team can adopt with minimal drama:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a database branch&lt;/strong&gt; for the migration work (Neon).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apply the migration&lt;/strong&gt; in the branch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploy a preview environment&lt;/strong&gt; using the migrated branch database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run tests&lt;/strong&gt; and perform targeted verification of the risky flows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Promote to rollout&lt;/strong&gt; using PlanetScale deploy requests for safe application-level and schema-level changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitor&lt;/strong&gt; the specific behaviors you validated—because verification isn’t just for confidence; it’s also for faster detection when something deviates.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This workflow flips the default posture from “hope” to “prove.”&lt;/p&gt;
&lt;h2 id="the-end-of-will-this-break-productionand-what-still-matters"&gt;The end of “will this break production?”—and what still matters&lt;/h2&gt;
&lt;p&gt;Let’s be honest: you’ll still write careful migrations. You’ll still think about lock behavior, index builds, and compatibility. You’ll still watch error rates and latencies after deployment.&lt;/p&gt;
&lt;p&gt;But the psychological center of gravity moves.&lt;/p&gt;
&lt;p&gt;You stop relying on vague staging resemblance and start using true production-shaped validation. You stop treating migrations like existential events and start treating them like normal changes with previewable outcomes.&lt;/p&gt;
&lt;p&gt;In practice, this means your team can stop asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Do we feel lucky?”&lt;/li&gt;
&lt;li&gt;“Can we risk this during peak?”&lt;/li&gt;
&lt;li&gt;“What’s our rollback if the data is messy?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And start asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What will fail on the migrated branch?”&lt;/li&gt;
&lt;li&gt;“Which tests and flows must pass to consider this safe?”&lt;/li&gt;
&lt;li&gt;“What is the smallest operationally safe migration plan?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s engineering maturity—not fear avoidance.&lt;/p&gt;
&lt;h2 id="conclusion-migration-fear-is-solvable-engineering-work"&gt;Conclusion: migration fear is solvable engineering work&lt;/h2&gt;
&lt;p&gt;Database migrations don’t have to be the scariest part of deployment. Neon’s database branching lets you create isolated, production-grade copies of schema &lt;em&gt;and data&lt;/em&gt; for development and testing. PlanetScale’s deploy requests apply schema changes with zero downtime. Add preview environments and you can validate the exact shape of production reality before you ship.&lt;/p&gt;
&lt;p&gt;The result is a practical revolution: Git-style confidence for your database lifecycle—where “will this break production?” becomes a question you can test and answer, not a gamble you have to survive.&lt;/p&gt;</content></item><item><title>shadcn/ui Proved Component Libraries Were Doing It Wrong</title><link>https://decastro.work/blog/shadcn-ui-proved-component-libraries-wrong/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/shadcn-ui-proved-component-libraries-wrong/</guid><description>&lt;p&gt;For years, teams have treated UI components like a commodity: install a package, trust the defaults, and hope the library’s opinions match your product’s reality. That strategy fails the moment your “simple” button needs to do something slightly different. &lt;code&gt;shadcn/ui&lt;/code&gt; flips the entire model—by making components something you copy, own, and evolve inside your codebase. It’s not just a better UI stack. It’s a better philosophy.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-component-libraries"&gt;The real problem with “component libraries”&lt;/h2&gt;
&lt;p&gt;Most component libraries ship as npm packages, versioned and maintained separately from your application. That sounds convenient until you hit the friction points every frontend team knows too well:&lt;/p&gt;</description><content>&lt;p&gt;For years, teams have treated UI components like a commodity: install a package, trust the defaults, and hope the library’s opinions match your product’s reality. That strategy fails the moment your “simple” button needs to do something slightly different. &lt;code&gt;shadcn/ui&lt;/code&gt; flips the entire model—by making components something you copy, own, and evolve inside your codebase. It’s not just a better UI stack. It’s a better philosophy.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-component-libraries"&gt;The real problem with “component libraries”&lt;/h2&gt;
&lt;p&gt;Most component libraries ship as npm packages, versioned and maintained separately from your application. That sounds convenient until you hit the friction points every frontend team knows too well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version conflicts&lt;/strong&gt;: You want the latest component fix, but it pulls a dependency chain you’d rather not touch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opinion mismatches&lt;/strong&gt;: The library’s styling and behavior are coherent—until they’re not your product.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature gaps&lt;/strong&gt;: “We don’t support that prop combination” becomes a recurring conversation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The slow loop&lt;/strong&gt;: You wait on maintainers for changes that are obvious in your app context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What’s easy to miss is that these aren’t small annoyances. They’re structural. When the UI is owned by someone else’s package, you spend engineering effort negotiating boundaries rather than shipping product. Even if the library is great, the moment you need to bend it, you’re negotiating with an abstraction.&lt;/p&gt;
&lt;h2 id="what-shadcnui-actually-is-and-why-it-matters"&gt;What shadcn/ui actually is (and why it matters)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;shadcn/ui&lt;/code&gt; is not a typical component library you install and depend on. It’s closer to a curated set of components you &lt;strong&gt;copy into your project&lt;/strong&gt;. You don’t keep it as a third-party runtime dependency. You bring the source code into your repo and you own it.&lt;/p&gt;
&lt;p&gt;That single change changes everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No npm dependency to manage&lt;/strong&gt; for the components themselves.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No version upgrades&lt;/strong&gt; that silently alter behavior across releases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No “but the library doesn’t support my use case” wall&lt;/strong&gt;—because the code is now yours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your team controls the design system timeline&lt;/strong&gt;, not the library’s release schedule.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The radical insight is embarrassingly simple: if you treat UI components like &lt;em&gt;your&lt;/em&gt; code, the “library limitations” problem disappears. You can refactor, extend, or remove logic without waiting for permission.&lt;/p&gt;
&lt;h2 id="copy-paste-beats-dependency-drama"&gt;Copy-paste beats dependency drama&lt;/h2&gt;
&lt;p&gt;Let’s ground this in a scenario that hits almost every team.&lt;/p&gt;
&lt;p&gt;You choose a component library for consistent buttons, dialogs, forms—great. Now a designer asks: “The primary button should show a loading spinner &lt;em&gt;and&lt;/em&gt; disable itself during the async call, but only for certain actions. Also, we need analytics events fired on click—no matter whether the handler is synchronous or async.”&lt;/p&gt;
&lt;p&gt;With an npm-based component, you have options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wrap the component in your own abstraction (more code, more indirection).&lt;/li&gt;
&lt;li&gt;Use library extension points (if they exist, and if they behave exactly how you need).&lt;/li&gt;
&lt;li&gt;Fork the repo (then you own the upkeep forever, just without the clarity of “this is our code”).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;code&gt;shadcn/ui&lt;/code&gt;-style copyable components, the path is direct:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find the button component file in your project.&lt;/li&gt;
&lt;li&gt;Add the spinner + disabled logic where it belongs.&lt;/li&gt;
&lt;li&gt;Ensure the analytics hook is triggered in the right places.&lt;/li&gt;
&lt;li&gt;Commit it like any other feature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t “work around” the library. You implement the button you actually need, inside your codebase, with full control over behavior.&lt;/p&gt;
&lt;p&gt;And because you own it, you can keep changes consistent across the app. You can even standardize patterns: for example, enforce that all async buttons share a single “loading contract” so your UI feels coherent.&lt;/p&gt;
&lt;h2 id="you-can-still-have-a-systemjust-make-it-yours"&gt;You can still have a system—just make it yours&lt;/h2&gt;
&lt;p&gt;Skeptics often say, “Copying components sounds like fragmentation.” That’s a fair concern, but it’s also a process problem—not a tooling problem.&lt;/p&gt;
&lt;p&gt;The solution is to treat your copied components like a real internal design system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralize the source of truth&lt;/strong&gt;: keep components in a predictable folder structure (e.g., &lt;code&gt;src/components/ui/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document the intent&lt;/strong&gt;: explain what variations exist and how they’re supposed to be used.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create local conventions&lt;/strong&gt;: define patterns for props like &lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;size&lt;/code&gt;, &lt;code&gt;asChild&lt;/code&gt;, or any app-specific behaviors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor aggressively&lt;/strong&gt;: once a couple of features prove out, fold them into the component rather than duplicating wrappers everywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, teams adopting this model often end up with the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the speed of starting from beautifully designed defaults, and&lt;/li&gt;
&lt;li&gt;the freedom to evolve the components without downstream dependency politics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your design system becomes a living part of your app, not an external artifact you babysit.&lt;/p&gt;
&lt;h2 id="when-you-should-customize-and-when-you-shouldnt"&gt;When you &lt;em&gt;should&lt;/em&gt; customize (and when you shouldn’t)&lt;/h2&gt;
&lt;p&gt;Owning components is power—but power needs boundaries. Here’s an opinionated rule of thumb:&lt;/p&gt;
&lt;h3 id="customize-when-the-behavior-is-product-specific"&gt;Customize when the behavior is product-specific&lt;/h3&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Analytics events on interaction&lt;/li&gt;
&lt;li&gt;Auth-gated actions&lt;/li&gt;
&lt;li&gt;Routing-aware links (“active” styling based on app state)&lt;/li&gt;
&lt;li&gt;Accessibility behaviors tied to your domain logic (not just generic keyboard support)&lt;/li&gt;
&lt;li&gt;Form validation and error messaging patterns that match your UX&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="dont-customize-when-its-purely-cosmetic-or-superficial"&gt;Don’t customize when it’s purely cosmetic or superficial&lt;/h3&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Small color tweaks that can be handled by theme variables or CSS overrides&lt;/li&gt;
&lt;li&gt;Layout differences that should live in &lt;code&gt;className&lt;/code&gt; composition&lt;/li&gt;
&lt;li&gt;“One-off” experiments that don’t become patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you customize too eagerly, you’ll create a zoo of near-duplicate components. The smarter approach is to customize when you see the same requirement repeating—or when the component’s behavior must align with your product rules.&lt;/p&gt;
&lt;h2 id="what-this-model-teaches-maintainers"&gt;What this model teaches maintainers&lt;/h2&gt;
&lt;p&gt;If you maintain a UI library, &lt;code&gt;shadcn/ui&lt;/code&gt; is uncomfortable reading—because it exposes a mismatch between how maintainers think and how product teams actually build.&lt;/p&gt;
&lt;p&gt;Maintainers optimize for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a coherent API across many users,&lt;/li&gt;
&lt;li&gt;backward compatibility,&lt;/li&gt;
&lt;li&gt;generality,&lt;/li&gt;
&lt;li&gt;and a manageable surface area.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Product teams optimize for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rapid iteration,&lt;/li&gt;
&lt;li&gt;tight integration with app-specific state,&lt;/li&gt;
&lt;li&gt;and predictable behavior under real workflows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those priorities collide the moment your “generic component” becomes “our product’s component.”&lt;/p&gt;
&lt;p&gt;So here’s the takeaway for maintainers: the job isn’t just shipping components. It’s shipping a component model that doesn’t trap users inside your update cycle.&lt;/p&gt;
&lt;p&gt;Even if you don’t adopt a copy-first approach, you can learn from it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Offer clear extension points that don’t require deep forks.&lt;/li&gt;
&lt;li&gt;Minimize breaking changes by stabilizing behavior, not just types.&lt;/li&gt;
&lt;li&gt;Document customization paths that work in real apps, not just demos.&lt;/li&gt;
&lt;li&gt;Design for composition and behavior overrides—because product requirements will always be weird.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;shadcn/ui&lt;/code&gt; didn’t win because it had better UI marketing. It won because it respected ownership.&lt;/p&gt;
&lt;h2 id="conclusion-treat-ui-as-code-you-own"&gt;Conclusion: treat UI as code you own&lt;/h2&gt;
&lt;p&gt;Component libraries should accelerate development, not outsource decision-making. &lt;code&gt;shadcn/ui&lt;/code&gt; proves a simple principle: when you copy components into your project and own the source, you eliminate the core pain of third-party UI—version conflicts, feature gaps, and the constant negotiation between your product and someone else’s assumptions.&lt;/p&gt;
&lt;p&gt;The next time your app needs a button that behaves like a button &lt;em&gt;in your world&lt;/em&gt;, don’t search for a prop. Own the code.&lt;/p&gt;</content></item><item><title>PostgreSQL Is a Vector Database Now, and That Changes Everything</title><link>https://decastro.work/blog/postgresql-vector-database-changes-everything-ai/</link><pubDate>Mon, 20 May 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgresql-vector-database-changes-everything-ai/</guid><description>&lt;p&gt;For years, teams building AI features treated vectors like a separate species of data—store them somewhere else, query them with a different system, and glue everything together with application code. That pattern is expensive, operationally messy, and rarely necessary. The shift is simple: PostgreSQL plus &lt;strong&gt;pgvector&lt;/strong&gt; turns your existing relational database into an AI-native store for embeddings and similarity search—right next to the data you actually care about.&lt;/p&gt;
&lt;p&gt;If you’ve been tempted to spin up a vector database “because that’s what everyone does,” it’s time to reconsider the architecture. In many real-world systems, vectors should live next to your records, not in a parallel universe.&lt;/p&gt;</description><content>&lt;p&gt;For years, teams building AI features treated vectors like a separate species of data—store them somewhere else, query them with a different system, and glue everything together with application code. That pattern is expensive, operationally messy, and rarely necessary. The shift is simple: PostgreSQL plus &lt;strong&gt;pgvector&lt;/strong&gt; turns your existing relational database into an AI-native store for embeddings and similarity search—right next to the data you actually care about.&lt;/p&gt;
&lt;p&gt;If you’ve been tempted to spin up a vector database “because that’s what everyone does,” it’s time to reconsider the architecture. In many real-world systems, vectors should live next to your records, not in a parallel universe.&lt;/p&gt;
&lt;h2 id="the-old-pattern-two-databases-and-a-duct-taped-workflow"&gt;The old pattern: two databases and a duct-taped workflow&lt;/h2&gt;
&lt;p&gt;A typical AI app pipeline used to look like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your application stores canonical data in PostgreSQL (users, documents, tickets, products).&lt;/li&gt;
&lt;li&gt;Your embedding service generates vectors.&lt;/li&gt;
&lt;li&gt;You store vectors in a dedicated vector database (e.g., for similarity search).&lt;/li&gt;
&lt;li&gt;When a user asks a question, you:
&lt;ul&gt;
&lt;li&gt;query the vector store to get nearest matches,&lt;/li&gt;
&lt;li&gt;then make another trip to PostgreSQL to fetch metadata,&lt;/li&gt;
&lt;li&gt;then assemble the final response.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This isn’t just “more infrastructure”—it changes failure modes and performance characteristics. You now have at least two sources of truth, two indexing systems to maintain, and at least one place where permissions or tenant boundaries can drift out of sync.&lt;/p&gt;
&lt;p&gt;Even if you’ve built the integration well, the architecture tends to encourage a frustrating workflow: the vector store becomes the “search brain,” while PostgreSQL remains the “details clerk.” That split often forces extra joins at the application layer and complicates transactional consistency.&lt;/p&gt;
&lt;h2 id="what-pgvector-changes-vectors-become-first-class-citizens-in-postgres"&gt;What pgvector changes: vectors become first-class citizens in Postgres&lt;/h2&gt;
&lt;p&gt;With &lt;strong&gt;pgvector&lt;/strong&gt;, embeddings live in PostgreSQL tables. Similarity search becomes a native query pattern. Instead of shipping data back and forth between systems, you can store vectors alongside the rows they describe.&lt;/p&gt;
&lt;p&gt;The practical win is adjacency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One database&lt;/strong&gt; for both structured data and embeddings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One query surface&lt;/strong&gt; (SQL) to combine similarity matching with filters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One transactional model&lt;/strong&gt; for updates and deletes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One place&lt;/strong&gt; to manage access controls.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple mental model: pgvector adds an efficient way to store vector columns and compute nearest neighbors using similarity operators. From there, Postgres does the rest—indexes, constraints, transactions, and joins—using familiar tooling.&lt;/p&gt;
&lt;p&gt;You can write a query that looks like “find the most similar documents, but only for this customer and only those that are still active,” without manually stitching results across services.&lt;/p&gt;
&lt;h2 id="next-to-your-data-isnt-a-sloganits-an-implementation-advantage"&gt;“Next to your data” isn’t a slogan—it’s an implementation advantage&lt;/h2&gt;
&lt;p&gt;Here’s where the benefits stop being theoretical.&lt;/p&gt;
&lt;h3 id="example-rag-over-document-metadata-without-an-extra-hop"&gt;Example: RAG over document metadata without an extra hop&lt;/h3&gt;
&lt;p&gt;Suppose you’re building retrieval-augmented generation (RAG) for internal docs. Your Postgres table might include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tenant_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content_chunk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;embedding&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With pgvector, a single query can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter by &lt;code&gt;tenant_id&lt;/code&gt; (hard boundary),&lt;/li&gt;
&lt;li&gt;compute similarity between the query embedding and stored chunk embeddings,&lt;/li&gt;
&lt;li&gt;return the top K chunks with their &lt;code&gt;doc_id&lt;/code&gt; and &lt;code&gt;content_chunk&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means your “retrieve context” step can stay inside the database. Your application stops doing “top K from vector DB, then fetch details from Postgres”—and you eliminate a whole class of consistency problems.&lt;/p&gt;
&lt;h3 id="example-search-that-respects-business-rules"&gt;Example: Search that respects business rules&lt;/h3&gt;
&lt;p&gt;Similarity search is rarely the whole story. You almost always want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;only enabled products,&lt;/li&gt;
&lt;li&gt;only non-expired records,&lt;/li&gt;
&lt;li&gt;only items the user is allowed to see,&lt;/li&gt;
&lt;li&gt;only content in a certain language,&lt;/li&gt;
&lt;li&gt;only recent activity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a two-database world, those constraints can become an awkward post-filter step: vector DB retrieves candidates broadly, and then your application or Postgres filters them. With pgvector, you can push those filters directly into the SQL query so the nearest-neighbor work happens within the correct slice of the data.&lt;/p&gt;
&lt;h3 id="example-updates-and-deletes-that-wont-drift"&gt;Example: Updates and deletes that won’t drift&lt;/h3&gt;
&lt;p&gt;Embeddings change when you re-chunk documents, update models, or re-embed after content edits. If vectors live in a separate service, you’ll eventually deal with “ghost results”—vectors that remain after the source document was deleted, or metadata that no longer matches.&lt;/p&gt;
&lt;p&gt;In Postgres, you can tie embedding updates to the lifecycle of the underlying rows. Use transactions, triggers, or background jobs that update both the metadata and embeddings together. Even if you use async embedding generation, you can keep the database as the source of truth and reduce cross-system mismatch.&lt;/p&gt;
&lt;h2 id="practical-advice-design-for-similarity-not-just-storage"&gt;Practical advice: design for similarity, not just storage&lt;/h2&gt;
&lt;p&gt;pgvector is not magic; it’s an enabler. To get solid performance and maintainability, you still need to design your embedding tables and indexing strategy with intent.&lt;/p&gt;
&lt;h3 id="store-chunk-identifiers-and-metadata-youll-filter-on"&gt;Store chunk identifiers and metadata you’ll filter on&lt;/h3&gt;
&lt;p&gt;If you plan to filter by &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;document_type&lt;/code&gt;, or &lt;code&gt;created_at&lt;/code&gt;, include those columns in the same table as the vector. Your future self will thank you. “Store only vectors” sounds clean until you realize nearly every search needs guardrails.&lt;/p&gt;
&lt;h3 id="choose-chunk-sizes-and-keep-them-stable"&gt;Choose chunk sizes and keep them stable&lt;/h3&gt;
&lt;p&gt;For RAG-like workloads, embedding quality often depends more on how you chunk content than on which vector model you use. If chunking changes frequently, expect a churn cycle: re-embed, re-index, and validate retrieval relevance.&lt;/p&gt;
&lt;p&gt;A good approach is to standardize chunking logic early. When you must change it, treat the embedding version as a first-class attribute (e.g., &lt;code&gt;embedding_model&lt;/code&gt;, &lt;code&gt;chunk_version&lt;/code&gt;) so you can roll forward predictably.&lt;/p&gt;
&lt;h3 id="index-like-you-mean-it-and-measure"&gt;Index like you mean it (and measure)&lt;/h3&gt;
&lt;p&gt;Vector search performance depends heavily on indexes and the query operators you use. pgvector supports different index types and similarity functions, and the “best” choice varies by workload: dataset size, dimensionality, and acceptable latency.&lt;/p&gt;
&lt;p&gt;Your practical workflow should look like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with correctness: implement similarity queries first.&lt;/li&gt;
&lt;li&gt;Add an index once you confirm your query patterns.&lt;/li&gt;
&lt;li&gt;Measure latency and recall behavior with your real distributions (tenants, doc lengths, update frequency).&lt;/li&gt;
&lt;li&gt;Iterate on index parameters based on outcomes, not guesses.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’ve ever tuned a search system that only performed well on demo data, you know why this matters.&lt;/p&gt;
&lt;h3 id="keep-your-embedding-generation-pipeline-honest"&gt;Keep your embedding generation pipeline honest&lt;/h3&gt;
&lt;p&gt;Even in a unified architecture, you still need a reliable embedding pipeline. A simple but effective pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store the source data in Postgres.&lt;/li&gt;
&lt;li&gt;Generate embeddings asynchronously.&lt;/li&gt;
&lt;li&gt;Write embeddings back to the same row (or a dedicated embedding table keyed by the row ID).&lt;/li&gt;
&lt;li&gt;Track embedding status and versioning so you don’t serve stale vectors silently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That way, the database remains the operational center while your embedding compute can scale independently.&lt;/p&gt;
&lt;h2 id="the-one-database-argument-not-a-meme-a-systems-decision"&gt;The “one database” argument: not a meme, a systems decision&lt;/h2&gt;
&lt;p&gt;There’s a reason this shift feels inevitable: complexity taxes compound. Every additional service requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deployment and monitoring,&lt;/li&gt;
&lt;li&gt;backups and disaster recovery thinking,&lt;/li&gt;
&lt;li&gt;security reviews,&lt;/li&gt;
&lt;li&gt;data modeling coordination,&lt;/li&gt;
&lt;li&gt;and operational runbooks for “what happened when it went wrong.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When vectors sit beside the data, you can simplify those operational steps dramatically. And you improve developer velocity because the mental model becomes straightforward: “I store and retrieve everything about this entity in one place.”&lt;/p&gt;
&lt;p&gt;But the real advantage is architectural coherence. Similarity search is not an exotic afterthought anymore; it’s just another index-assisted query method. When you can join nearest-neighbor results to structured filters in the same system, you stop treating AI features like a separate product line.&lt;/p&gt;
&lt;p&gt;For many teams, this is the difference between “we built a prototype” and “we can confidently ship and operate this.”&lt;/p&gt;
&lt;h2 id="conclusion-postgresql--pgvector-is-the-ai-native-baseline"&gt;Conclusion: PostgreSQL + pgvector is the AI-native baseline&lt;/h2&gt;
&lt;p&gt;PostgreSQL has always been a database for serious workloads—reliable transactions, robust query planning, and a mature operational ecosystem. pgvector brings an AI capability into that same environment, turning embeddings and nearest-neighbor search into first-class relational concerns.&lt;/p&gt;
&lt;p&gt;If you’re building AI features that need retrieval, ranking, or similarity search—and you already store canonical data in Postgres—then the cleanest architecture is to keep vectors in the same place. Not because “one database” sounds nice, but because it reduces operational risk, improves query expressiveness, and keeps your AI workflows aligned with your actual data model.&lt;/p&gt;
&lt;p&gt;PostgreSQL is now a vector database. The smartest teams will treat that as a baseline, not a novelty.&lt;/p&gt;</content></item><item><title>Docker's Quiet Transformation from Dev Tool to Infrastructure Backbone</title><link>https://decastro.work/blog/docker-quiet-transformation-infrastructure-backbone/</link><pubDate>Wed, 08 May 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/docker-quiet-transformation-infrastructure-backbone/</guid><description>&lt;p&gt;For years, containers were the “cheat code” developers used to escape the dreaded &lt;em&gt;works on my machine&lt;/em&gt; trap. Today, that same idea has quietly evolved into something far bigger: a dependable infrastructure layer teams treat as unremarkable—like DNS or TCP/IP—because it just works. Docker didn’t merely make deployments easier; it made reliability portable.&lt;/p&gt;
&lt;p&gt;This is the story of why containerization stopped being a workaround and started acting like infrastructure: through tooling maturity, better build workflows, and an ecosystem that hardened where it mattered—at the edges of production.&lt;/p&gt;</description><content>&lt;p&gt;For years, containers were the “cheat code” developers used to escape the dreaded &lt;em&gt;works on my machine&lt;/em&gt; trap. Today, that same idea has quietly evolved into something far bigger: a dependable infrastructure layer teams treat as unremarkable—like DNS or TCP/IP—because it just works. Docker didn’t merely make deployments easier; it made reliability portable.&lt;/p&gt;
&lt;p&gt;This is the story of why containerization stopped being a workaround and started acting like infrastructure: through tooling maturity, better build workflows, and an ecosystem that hardened where it mattered—at the edges of production.&lt;/p&gt;
&lt;h2 id="from-dev-convenience-to-platform-expectation"&gt;From “Dev Convenience” to “Platform Expectation”&lt;/h2&gt;
&lt;p&gt;The early container pitch was simple: package your app with its dependencies, ship it, and run it consistently. In practice, that meant fewer “It fails only in production” incidents and fewer late-night environment archaeology sessions.&lt;/p&gt;
&lt;p&gt;But Docker’s real shift wasn’t the container itself. It was the operational habit it enabled.&lt;/p&gt;
&lt;p&gt;Consider the typical progression most teams experienced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Phase 1:&lt;/strong&gt; “We’ll use Docker for local dev.”&lt;br&gt;
Great—until integration tests depend on external services, and the team starts copy-pasting run commands like sacred incantations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 2:&lt;/strong&gt; “We’ll use Docker for staging.”&lt;br&gt;
Now the environment is reproducible, but deployment is still manual and brittle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 3:&lt;/strong&gt; “We’ll standardize on Compose/CI builds.”&lt;br&gt;
The workflow becomes repeatable enough that new services can be created with minimal ceremony.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase 4:&lt;/strong&gt; “We’ll treat containers as the delivery format.”&lt;br&gt;
At this point, containers aren’t a special step; they’re the assumption.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once containers become the assumption, architecture changes. Service boundaries map cleanly to runtime boundaries. Rollbacks become a matter of redeploying known artifacts. Scaling becomes a predictable exercise rather than a bespoke rewrite.&lt;/p&gt;
&lt;p&gt;In other words: Docker didn’t just solve “works on my machine.” It eliminated the &lt;em&gt;need to care&lt;/em&gt; about machine-specific details altogether.&lt;/p&gt;
&lt;h2 id="docker-compose-made-environments-actually-match"&gt;Docker Compose Made Environments Actually Match&lt;/h2&gt;
&lt;p&gt;It’s easy to underestimate the importance of Docker Compose. The headline value is obvious—run multi-container apps locally—but the deeper win is behavioral: it encouraged teams to create environments that mirror each other across development, testing, and staging.&lt;/p&gt;
&lt;p&gt;A practical example: imagine a web service that depends on PostgreSQL, Redis, and a background worker. Without Compose, developers often end up with divergent setups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One developer runs Postgres 12 locally, another runs Postgres 15.&lt;/li&gt;
&lt;li&gt;Someone uses a different Redis configuration.&lt;/li&gt;
&lt;li&gt;Environment variables drift because they’re documented in screenshots, not defined in code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Compose, those differences collapse. You codify the environment: ports, volumes, dependency order, health checks, and even initialization scripts. The result is not just convenience—it’s alignment.&lt;/p&gt;
&lt;p&gt;And that alignment becomes cultural. When the “default way” to run the stack is the same as the “default way” to test it, fewer bugs survive to the point where they’re expensive.&lt;/p&gt;
&lt;p&gt;Compose also enabled a more disciplined approach to service design. If each service can be described as a container, it becomes easier to reason about interfaces, data lifecycles, and failure modes.&lt;/p&gt;
&lt;h2 id="buildkit-and-multi-stage-builds-the-quiet-productivity-revolution"&gt;BuildKit and Multi-Stage Builds: The Quiet Productivity Revolution&lt;/h2&gt;
&lt;p&gt;Containers are only as useful as the build pipeline behind them. The ecosystem moved from “build an image” to “build an artifact you can trust.”&lt;/p&gt;
&lt;p&gt;Two innovations (and the patterns surrounding them) made a massive difference:&lt;/p&gt;
&lt;h3 id="buildkit-faster-smarter-builds"&gt;BuildKit: faster, smarter builds&lt;/h3&gt;
&lt;p&gt;BuildKit improved builds in ways developers can feel immediately: caching gets more effective, parallelism increases throughput, and builds become less “mysterious” when caches miss. Instead of waiting for a full rebuild every time you touch a single file, you get incremental behavior that maps closer to how developers think.&lt;/p&gt;
&lt;h3 id="multi-stage-builds-smaller-images-cleaner-boundaries"&gt;Multi-stage builds: smaller images, cleaner boundaries&lt;/h3&gt;
&lt;p&gt;Multi-stage builds are the practical antidote to the “one giant image” problem. Instead of baking build tools, compilers, and caches into the final runtime image, you split the process into stages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build stage:&lt;/strong&gt; compile the application, install dependencies, run tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime stage:&lt;/strong&gt; copy only the built artifacts and the minimal runtime dependencies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The immediate benefits are straightforward: smaller images and reduced attack surface. But the deeper value is architectural. Your Dockerfile becomes documentation of what the application truly needs to run versus what it needs only to build.&lt;/p&gt;
&lt;p&gt;A good multi-stage pattern looks like this conceptually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lock dependencies early to maximize cache reuse.&lt;/li&gt;
&lt;li&gt;Copy only what’s needed from earlier stages.&lt;/li&gt;
&lt;li&gt;Keep runtime stages free of compilers and build tooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, this turns Docker from a packaging tool into part of a software supply chain you can audit and iterate on confidently.&lt;/p&gt;
&lt;h2 id="the-ecosystem-hardened-from-works-to-reliable"&gt;The Ecosystem Hardened: From “Works” to “Reliable”&lt;/h2&gt;
&lt;p&gt;The shift from dev tool to infrastructure backbone didn’t happen in a single release. It happened as teams demanded reliability and the ecosystem responded.&lt;/p&gt;
&lt;p&gt;What changed?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Better defaults and clearer tooling:&lt;/strong&gt; CI pipelines, registries, and orchestration integrations became smoother, reducing the odds of “it worked locally” drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stronger layering of concerns:&lt;/strong&gt; developers focus on application code; platform tooling handles deployment mechanics. Containers provide the contract between them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeatable deployment primitives:&lt;/strong&gt; images became immutable artifacts, not “things we build on the server.” That’s a foundational reliability step.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, when something breaks, it’s usually because of real issues—dependency changes, misconfigured secrets, a bad migration—not because the environment is secretly different.&lt;/p&gt;
&lt;p&gt;And once reliability becomes the baseline, conversations move on. Teams stop debating whether containers are “worth it” and start discussing higher-level concerns: observability, resilience, data consistency, rollout strategies, and cost.&lt;/p&gt;
&lt;p&gt;That’s the hallmark of infrastructure maturity. The tool stops being the story.&lt;/p&gt;
&lt;h2 id="the-invisibility-principle-containers-as-the-new-tcpip"&gt;The Invisibility Principle: Containers as the New TCP/IP&lt;/h2&gt;
&lt;p&gt;There’s a reason containerization is now so rarely explained from first principles. TCP/IP became inevitable because it solved a routing problem so thoroughly that developers stopped thinking about it. Docker has followed a similar path: the abstraction became so effective that the “how” receded.&lt;/p&gt;
&lt;p&gt;What does that look like day-to-day?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers request services and receive them as deployable images.&lt;/li&gt;
&lt;li&gt;CI systems produce artifacts the same way every time.&lt;/li&gt;
&lt;li&gt;Operations teams treat containerized workloads as standard components—like any other workload type.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Containerization becomes invisible when it earns trust through consistency. When teams can say, “We already know how this runs,” they spend their time improving application quality instead of fighting environment variability.&lt;/p&gt;
&lt;p&gt;This doesn’t mean problems disappeared. It means the container platform handles the predictable parts well enough that the real engineering work can rise to the surface.&lt;/p&gt;
&lt;h2 id="practical-advice-how-to-keep-containers-acting-like-infrastructure"&gt;Practical Advice: How to Keep Containers Acting Like Infrastructure&lt;/h2&gt;
&lt;p&gt;If Docker is now infrastructure in your organization, you should treat it accordingly. Here are the habits that keep containerization from regressing into fragile “magic”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pin versions aggressively&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use lockfiles for app dependencies.&lt;/li&gt;
&lt;li&gt;Pin base images (and update deliberately) rather than relying on implicit tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make builds deterministic&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep Dockerfiles focused.&lt;/li&gt;
&lt;li&gt;Avoid hidden state. If an environment variable is required, define it explicitly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use multi-stage builds by default&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep runtime images lean.&lt;/li&gt;
&lt;li&gt;Ensure build tooling doesn’t leak into production artifacts.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Codify environment setup&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer Compose (or equivalent) for local parity with dev/staging.&lt;/li&gt;
&lt;li&gt;Include health checks and initialization where possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat images as immutable artifacts&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build once in CI, deploy the same artifact everywhere.&lt;/li&gt;
&lt;li&gt;If you need different behavior, use configuration—not ad hoc rebuilds.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Invest in observability early&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Containers make deployment consistent; they don’t make debugging automatic.&lt;/li&gt;
&lt;li&gt;Make logs, metrics, and traces first-class citizens of your setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you do these things, Docker stops being a “tool you use” and becomes a dependable substrate. That’s when the abstraction pays off—repeatedly, without drama.&lt;/p&gt;
&lt;h2 id="conclusion-the-real-transformation-was-trust"&gt;Conclusion: The Real Transformation Was Trust&lt;/h2&gt;
&lt;p&gt;Docker’s quiet transformation—from dev convenience to infrastructure backbone—wasn’t a marketing win. It was an engineering outcome: containers became reliable enough, repeatable enough, and ergonomic enough that teams stopped thinking about them as a special technique.&lt;/p&gt;
&lt;p&gt;Like TCP/IP, the best infrastructure fades into the background. You notice it when it’s missing, and you take it for granted when it’s there. Containers have reached that point. Now the work is to build on top of them with the same seriousness you’d bring to any foundational layer of your system.&lt;/p&gt;</content></item><item><title>Vercel's Lock-In Problem Is Becoming Impossible to Ignore</title><link>https://decastro.work/blog/vercel-lock-in-problem-impossible-ignore/</link><pubDate>Thu, 02 May 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/vercel-lock-in-problem-impossible-ignore/</guid><description>&lt;p&gt;It’s easy to miss lock-in when it looks like convenience. You ship fast, everything renders instantly, and the defaults just “work.” But when your framework, your hosting, and your commercial incentives all come from the same company, the path of least resistance quietly becomes the only practical path. That’s the uncomfortable truth at the center of Next.js + Vercel today—and it’s getting harder to ignore.&lt;/p&gt;
&lt;h2 id="the-cozy-triangle-nextjs-vercel-and-incentives"&gt;The cozy triangle: Next.js, Vercel, and incentives&lt;/h2&gt;
&lt;p&gt;Start with the obvious: Next.js is the most popular React framework, and Vercel is its corporate sponsor. Vercel is also a hosting platform designed around Next.js. That means the tooling, performance features, and “just works” experience are not neutral—they’re aligned with Vercel’s business goals.&lt;/p&gt;</description><content>&lt;p&gt;It’s easy to miss lock-in when it looks like convenience. You ship fast, everything renders instantly, and the defaults just “work.” But when your framework, your hosting, and your commercial incentives all come from the same company, the path of least resistance quietly becomes the only practical path. That’s the uncomfortable truth at the center of Next.js + Vercel today—and it’s getting harder to ignore.&lt;/p&gt;
&lt;h2 id="the-cozy-triangle-nextjs-vercel-and-incentives"&gt;The cozy triangle: Next.js, Vercel, and incentives&lt;/h2&gt;
&lt;p&gt;Start with the obvious: Next.js is the most popular React framework, and Vercel is its corporate sponsor. Vercel is also a hosting platform designed around Next.js. That means the tooling, performance features, and “just works” experience are not neutral—they’re aligned with Vercel’s business goals.&lt;/p&gt;
&lt;p&gt;None of this requires a conspiracy. It’s simply how platform ecosystems evolve. When a company invests heavily in a framework’s fastest lane, it will naturally want that lane to stay paved with its own infrastructure. And when developers are rewarded for taking that lane—better performance, smoother DX, fewer edge cases—competitors are left playing catch-up with imperfect compatibility.&lt;/p&gt;
&lt;p&gt;The result: what begins as a technical choice gradually becomes a vendor dependency.&lt;/p&gt;
&lt;h2 id="why-the-best-features-are-the-ones-that-are-hardest-to-move"&gt;Why the “best features” are the ones that are hardest to move&lt;/h2&gt;
&lt;p&gt;Lock-in doesn’t usually show up as a dramatic switch you can’t make. It shows up as friction—small enough to postpone, large enough to eventually matter. In the Next.js world, that friction often clusters around features that are most mature on Vercel.&lt;/p&gt;
&lt;p&gt;Consider two popular examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ISR (Incremental Static Regeneration)&lt;/strong&gt;: In principle, ISR is part of Next.js. In practice, it behaves best when paired with the platform’s caching strategy, deployment flow, and request handling. When you move to another host, the feature may still exist—but the performance characteristics, invalidation behavior, or integration polish can vary enough to force rewrites, config tweaks, or architectural compromises.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Edge Middleware&lt;/strong&gt;: Middleware is where the “platform gravity” gets most visible. Middleware can run at the edge, but the edge runtime and routing semantics are deeply tied to the provider’s infrastructure. You can sometimes reproduce behavior elsewhere, but it’s rarely as seamless—especially when you rely on specifics like headers, rewrites, routing order, or the provider’s implementation of caching and session-affinity assumptions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the real pattern: the more you build around “the good stuff,” the more your application becomes a composite of Next.js plus Vercel assumptions. And those assumptions tend to be the last things you want to unravel in the middle of growth.&lt;/p&gt;
&lt;h2 id="the-self-hosting-gap-documentation-exists-but-confidence-doesnt"&gt;The self-hosting gap: documentation exists, but confidence doesn’t&lt;/h2&gt;
&lt;p&gt;If you want to believe lock-in isn’t real, the first stop is usually documentation. For self-hosting, you’ll find plenty of Next.js guidance. But you may also notice something subtle: documentation often tells you &lt;em&gt;how to run Next.js&lt;/em&gt;, not how to replicate the &lt;em&gt;full Vercel experience&lt;/em&gt; with confidence.&lt;/p&gt;
&lt;p&gt;That confidence gap matters. Running Next.js isn’t the hard part. Recreating the operational behaviors you’ve come to rely on is. For teams, the cost isn’t just engineering time—it’s uncertainty.&lt;/p&gt;
&lt;p&gt;A concrete example: suppose you built a deployment strategy that assumes Vercel’s preview environments, automatic rollbacks, and content caching behavior are “the way the platform works.” If you switch hosts, even if your builds still succeed, you may discover that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;preview URLs don’t behave identically,&lt;/li&gt;
&lt;li&gt;cache invalidation is less deterministic,&lt;/li&gt;
&lt;li&gt;certain optimizations don’t trigger the same way,&lt;/li&gt;
&lt;li&gt;and diagnosing performance regressions becomes a routine chore.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of that is “impossible,” but it’s expensive. And expense is how lock-in converts from philosophy into engineering reality.&lt;/p&gt;
&lt;h2 id="when-compatibility-breaks-it-breaks-quietlythen-costs-double"&gt;When compatibility breaks, it breaks quietly—then costs double&lt;/h2&gt;
&lt;p&gt;A good lock-in system doesn’t fail loudly. It degrades gradually, so you keep shipping while the exit plan rots.&lt;/p&gt;
&lt;p&gt;Think about what happens when you’re mid-migration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You discover an edge-case difference in middleware behavior.&lt;/li&gt;
&lt;li&gt;You patch it with an abstraction or conditional code path.&lt;/li&gt;
&lt;li&gt;The abstraction depends on internal behavior you didn’t document.&lt;/li&gt;
&lt;li&gt;Six months later, you can’t confidently refactor because no one remembers why the workaround exists.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s how “small incompatibilities” multiply into a full rewrite. And because Next.js adoption is widespread, the hardest parts aren’t always the framework code—they’re the glue between framework features and the hosting platform’s runtime.&lt;/p&gt;
&lt;p&gt;If you’re building authentication, personalization, or URL rewriting logic, you can end up with hidden dependencies on the host’s routing, caching, and header normalization. Even when the application renders, correctness can become inconsistent across environments. That inconsistency is often discovered at the worst possible time: during traffic spikes, marketing launches, or compliance audits.&lt;/p&gt;
&lt;h2 id="the-business-model-is-working-as-designed"&gt;The business model is working as designed&lt;/h2&gt;
&lt;p&gt;So yes—this is a business model. It’s also a rational one. If you operate a hosting platform, you want the ecosystem to treat your infrastructure as the default. If you sponsor the leading framework, you can prioritize the features that differentiate your platform and improve your customers’ outcomes.&lt;/p&gt;
&lt;p&gt;It’s tempting to frame this as unfair. It isn’t. It’s competitive. But it does create an uneven playing field:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alternative platforms may support Next.js, but struggle to match Vercel’s maturity across the full feature surface.&lt;/li&gt;
&lt;li&gt;Developers may get encouraged to use “platform-native” capabilities because they’re the easiest to adopt.&lt;/li&gt;
&lt;li&gt;Funding and tooling momentum then reinforce the same path, making other options feel risky or under-supported.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The irony is that the developer experience is genuinely good on Vercel. The problem isn’t that Vercel is “bad.” The problem is that “good” makes it harder to leave.&lt;/p&gt;
&lt;h2 id="how-to-plan-your-exit-without-slowing-down-shipping"&gt;How to plan your exit without slowing down shipping&lt;/h2&gt;
&lt;p&gt;You don’t need paranoia to protect yourself. You need a strategy. Here are practical steps that reduce lock-in while keeping velocity.&lt;/p&gt;
&lt;h3 id="1-treat-platform-specific-features-as-an-architectural-choice"&gt;1) Treat platform-specific features as an architectural choice&lt;/h3&gt;
&lt;p&gt;When you reach for Edge Middleware or rely heavily on ISR behavior, don’t assume it’s “portable by default.” Make a conscious decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What’s the fallback if that feature behaves differently?&lt;/li&gt;
&lt;li&gt;Can your app degrade gracefully?&lt;/li&gt;
&lt;li&gt;Is the logic essential, or can it be moved to a more portable layer?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the answer is “we’d have to rewrite it,” that’s not a reason to stop—it’s a reason to plan.&lt;/p&gt;
&lt;h3 id="2-isolate-host-assumptions-behind-narrow-interfaces"&gt;2) Isolate “host assumptions” behind narrow interfaces&lt;/h3&gt;
&lt;p&gt;Create a thin internal layer that wraps platform-dependent behavior. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Route rewriting logic can be centralized.&lt;/li&gt;
&lt;li&gt;Cache invalidation hooks can be abstracted behind a small module.&lt;/li&gt;
&lt;li&gt;Middleware decisions can call your own functions rather than embedding provider-specific assumptions everywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re building an escape hatch. The win isn’t only future portability—it’s simpler testing and cleaner refactors.&lt;/p&gt;
&lt;h3 id="3-make-local-and-staging-parity-part-of-your-definition-of-done"&gt;3) Make local and staging parity part of your definition of “done”&lt;/h3&gt;
&lt;p&gt;If you only test “works on Vercel,” you’re not testing. Use staging environments that mimic production as closely as possible, especially around caching, headers, and routing behavior.&lt;/p&gt;
&lt;p&gt;If you can’t get perfect parity, at least measure the differences early. The cost of discovering divergence in week one is dramatically lower than discovering it at month six.&lt;/p&gt;
&lt;h3 id="4-keep-your-infrastructure-story-documentedbefore-you-need-it"&gt;4) Keep your infrastructure story documented—before you need it&lt;/h3&gt;
&lt;p&gt;Write down how deployments work today: environment variables, build steps, caching assumptions, preview flows, and any special integration. Then add an “exit section”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What would change if we moved to a different host?&lt;/li&gt;
&lt;li&gt;Which components are most sensitive?&lt;/li&gt;
&lt;li&gt;What testing would we run to validate the migration?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t overhead. It’s insurance.&lt;/p&gt;
&lt;h3 id="5-periodically-re-evaluate-the-default"&gt;5) Periodically re-evaluate the “default”&lt;/h3&gt;
&lt;p&gt;Framework and tooling evolve. Hosts change. The ecosystem’s balance can shift. Make it a habit to audit whether your current setup still offers the best trade-off—not just whether it’s convenient.&lt;/p&gt;
&lt;p&gt;Lock-in often persists because teams stop asking the question.&lt;/p&gt;
&lt;h2 id="conclusion-choose-nextjs-with-eyes-open"&gt;Conclusion: Choose Next.js with eyes open&lt;/h2&gt;
&lt;p&gt;Next.js can absolutely be a great framework. Vercel can absolutely be a great hosting platform. The problem is when the ecosystem quietly turns into a single-vendor pipeline where the most valuable features are the least transferable.&lt;/p&gt;
&lt;p&gt;If you’re building with Next.js in 2026, the responsible move isn’t to panic—it’s to plan. Treat platform-native capabilities as decisions, isolate assumptions, demand parity in testing, and document your exit path while the project is still easy to change. Convenience is powerful, but so is foresight.&lt;/p&gt;</content></item><item><title>The Surprisingly Mature Ecosystem of Rust CLI Tools Replacing Unix Classics</title><link>https://decastro.work/blog/rust-cli-tools-replacing-unix-classics/</link><pubDate>Fri, 26 Apr 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rust-cli-tools-replacing-unix-classics/</guid><description>&lt;p&gt;Unix shipped a philosophy: small tools, composable outputs, ruthless simplicity. For decades, that meant a handful of classics dominated your terminal—&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, even the awkward parts of navigating a filesystem and reading diffs. Then Rust showed up and did something rare: it didn’t just produce a new language, it produced a new &lt;em&gt;default toolkit&lt;/em&gt;. And the best part is that the “Rust CLI revolution” isn’t hype. It’s quietly mature, aggressively practical, and in many workflows, simply better.&lt;/p&gt;</description><content>&lt;p&gt;Unix shipped a philosophy: small tools, composable outputs, ruthless simplicity. For decades, that meant a handful of classics dominated your terminal—&lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, even the awkward parts of navigating a filesystem and reading diffs. Then Rust showed up and did something rare: it didn’t just produce a new language, it produced a new &lt;em&gt;default toolkit&lt;/em&gt;. And the best part is that the “Rust CLI revolution” isn’t hype. It’s quietly mature, aggressively practical, and in many workflows, simply better.&lt;/p&gt;
&lt;p&gt;This is how the Rust ecosystem rewrites your day-to-day terminal—and why the pattern matters.&lt;/p&gt;
&lt;h2 id="rust-cli-tools-didnt-arrive-as-experiments"&gt;Rust CLI tools didn’t arrive as experiments&lt;/h2&gt;
&lt;p&gt;A lot of ecosystems try to replace Unix classics with “cool” alternatives that break under real usage. The Rust story has been different. Tools like &lt;code&gt;rg&lt;/code&gt; (ripgrep), &lt;code&gt;fd&lt;/code&gt;, &lt;code&gt;bat&lt;/code&gt;, &lt;code&gt;eza&lt;/code&gt;, &lt;code&gt;zoxide&lt;/code&gt;, and &lt;code&gt;delta&lt;/code&gt; don’t feel like prototypes. They feel like they were designed for people who live in the terminal and care about latency, ergonomics, and predictable behavior.&lt;/p&gt;
&lt;p&gt;The maturity shows up in details you only notice once the tools become your default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sensible command defaults that reduce the need for flags.&lt;/li&gt;
&lt;li&gt;Clear, consistent output and paging behavior.&lt;/li&gt;
&lt;li&gt;Strong performance characteristics when repositories get large.&lt;/li&gt;
&lt;li&gt;Compatibility with common developer workflows, including pipelines and editors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can argue about aesthetics, but you can’t argue about friction. If a tool consistently saves you keystrokes and time, it wins. Rust just happened to ship a whole generation of winners.&lt;/p&gt;
&lt;h2 id="ripgrep-vs-grep-search-that-doesnt-punish-you"&gt;ripgrep vs grep: search that doesn’t punish you&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;grep&lt;/code&gt; is a workhorse, but it’s also the kind of tool that teaches you pain. It’s fast enough until it isn’t. You start adding flags. You stop searching whole trees and begin excluding files manually. You learn the dark art of &lt;code&gt;--exclude-dir&lt;/code&gt;, &lt;code&gt;--binary-files&lt;/code&gt;, and other incantations.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ripgrep&lt;/code&gt;—usually invoked as &lt;code&gt;rg&lt;/code&gt;—approaches the same job with a design that assumes you want quick iteration. In practice, that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It respects &lt;code&gt;.gitignore&lt;/code&gt; by default, so you don’t drown in vendor code and generated files.&lt;/li&gt;
&lt;li&gt;It has sane defaults that make “just search” actually work in big repositories.&lt;/li&gt;
&lt;li&gt;It’s built for interactive use: run, scan, adjust, run again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example: instead of:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;grep -R &lt;span style="color:#e6db74"&gt;&amp;#34;TODO&amp;#34;&lt;/span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you’d typically do:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rg &lt;span style="color:#e6db74"&gt;&amp;#34;TODO&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, if you want case-insensitive search:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rg -i &lt;span style="color:#e6db74"&gt;&amp;#34;todo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or search only specific types:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rg &lt;span style="color:#e6db74"&gt;&amp;#34;config&amp;#34;&lt;/span&gt; --glob &lt;span style="color:#e6db74"&gt;&amp;#39;*.yaml&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The key isn’t that &lt;code&gt;rg&lt;/code&gt; is “faster” in a lab. The key is that it makes your &lt;em&gt;mental loop&lt;/em&gt; faster. You stop hesitating before searching. You search more often. And you find things you’d otherwise have missed because your tools made the first attempt too expensive.&lt;/p&gt;
&lt;h2 id="fd-vs-find-fewer-flags-fewer-mistakes"&gt;fd vs find: fewer flags, fewer mistakes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;find&lt;/code&gt; is powerful, but it’s also famously easy to misuse—especially when you’re juggling quoting, precedence, and type filters. &lt;code&gt;fd&lt;/code&gt; keeps the useful parts of &lt;code&gt;find&lt;/code&gt; while removing the parts that turn searches into debugging sessions.&lt;/p&gt;
&lt;p&gt;In day-to-day usage, &lt;code&gt;fd&lt;/code&gt; feels like what you always wanted &lt;code&gt;find&lt;/code&gt; to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It uses more predictable defaults (like not searching hidden files unless you ask).&lt;/li&gt;
&lt;li&gt;It tends to keep commands short and readable.&lt;/li&gt;
&lt;li&gt;It encourages a “filter-first” mindset without a wall of parentheses and operators.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: locate a file name pattern:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find . -name &lt;span style="color:#e6db74"&gt;&amp;#34;nginx.conf&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;becomes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fd nginx.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Search by extension:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fd &lt;span style="color:#e6db74"&gt;&amp;#39;*.rs&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or combine with actions—like previewing matches or piping results into other tools—without drowning in boilerplate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fd &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt; --type f -x rg &lt;span style="color:#e6db74"&gt;&amp;#34;fn main&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The practical win is confidence. If a command is easier to write correctly, you run it more often and with fewer second thoughts.&lt;/p&gt;
&lt;h2 id="bat-vs-cat-readability-as-a-feature-not-a-luxury"&gt;bat vs cat: readability as a feature, not a luxury&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; is fine for dumping content, but it’s optimized for the past. Once your terminals became visual workspaces, “readable output” became part of productivity. &lt;code&gt;bat&lt;/code&gt; steps in here by adding syntax highlighting and paging that behave like a modern CLI viewer.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cat server.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;you reach for:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bat server.go
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What changes immediately?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can scan code and configuration without squinting.&lt;/li&gt;
&lt;li&gt;Syntax highlighting reduces the cognitive load of identifying structure.&lt;/li&gt;
&lt;li&gt;Paging makes long files survivable without redirecting output to &lt;code&gt;less&lt;/code&gt; manually.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one of those upgrades that feels small until you switch back. After a week of &lt;code&gt;bat&lt;/code&gt;, plain &lt;code&gt;cat&lt;/code&gt; looks like you forgot to configure your editor.&lt;/p&gt;
&lt;h2 id="eza-vs-ls-modern-listing-without-losing-the-unix-vibe"&gt;eza vs ls: modern listing without losing the Unix vibe&lt;/h2&gt;
&lt;p&gt;Unix veterans can quote &lt;code&gt;ls&lt;/code&gt; options like scripture. But &lt;code&gt;ls&lt;/code&gt; has always been oddly stuck between “simple directory listing” and “everything you need to configure yourself into usefulness.”&lt;/p&gt;
&lt;p&gt;&lt;code&gt;eza&lt;/code&gt; is essentially a modernization of directory viewing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Better formatting that makes columns and file types more legible.&lt;/li&gt;
&lt;li&gt;More helpful defaults for humans scanning output.&lt;/li&gt;
&lt;li&gt;Still firmly in the “terminal-first” tradition.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Try a default list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ls
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;eza
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll quickly notice that your eyes spend less time decoding and more time deciding what to do next. That matters when you’re sorting through build directories, logs, or “what did this command generate?” moments.&lt;/p&gt;
&lt;p&gt;The important point: &lt;code&gt;eza&lt;/code&gt; doesn’t pretend to be a filesystem explorer. It’s still a CLI listing tool. It just stops treating your terminal like a monochrome environment.&lt;/p&gt;
&lt;h2 id="zoxide-instant-cd-built-from-your-real-habits"&gt;zoxide: instant &lt;code&gt;cd&lt;/code&gt; built from your real habits&lt;/h2&gt;
&lt;p&gt;Navigation is the hidden tax in terminal workflows. Even if you have the perfect search tool, you still lose time every time you have to remember where something lives—or worse, where it used to live.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zoxide&lt;/code&gt; attacks this directly: it learns directory usage patterns and lets you jump there quickly with a short query. Instead of relying on memory, it relies on your behavior.&lt;/p&gt;
&lt;p&gt;A typical flow looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;z src
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and suddenly you’re in the directory that most often matches “src” in your recent activity. It’s not magic; it’s just data-driven convenience.&lt;/p&gt;
&lt;p&gt;The practical benefits show up in exactly the moments you’d otherwise open a file manager:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Where’s that project I touched yesterday?”&lt;/li&gt;
&lt;li&gt;“I know the directory name has ‘client’ somewhere—what was it again?”&lt;/li&gt;
&lt;li&gt;“I keep bouncing between &lt;code&gt;infra/&lt;/code&gt; and &lt;code&gt;services/&lt;/code&gt;—why am I walking there every time?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;code&gt;zoxide&lt;/code&gt;, the answer is: you don’t walk. You jump.&lt;/p&gt;
&lt;h2 id="delta-the-missing-layer-between-git-diff-and-your-brain"&gt;delta: the missing layer between git diff and your brain&lt;/h2&gt;
&lt;p&gt;Version control diffs are a necessary evil. &lt;code&gt;git diff&lt;/code&gt; is technically correct, but it’s not always &lt;em&gt;human-friendly&lt;/em&gt;. When diffs get large, you end up reading patch hunks like they’re ancient scrolls.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;delta&lt;/code&gt; makes diffs substantially more readable by enhancing presentation—coloring, grouping, and highlighting changes so the important parts stand out. If you’ve ever thought “I know what I changed, but why is it so hard to see it in the output?”, you’ll understand what &lt;code&gt;delta&lt;/code&gt; solves.&lt;/p&gt;
&lt;p&gt;A common workflow:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git diff
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;turns into a better experience after configuring your &lt;code&gt;git&lt;/code&gt; pager to use &lt;code&gt;delta&lt;/code&gt;. The immediate payoff is that reviews become faster for you, and debugging becomes less tedious for everyone.&lt;/p&gt;
&lt;p&gt;And importantly, this is not only for pull requests. Even your own local iteration benefits. When you’re trying to validate a change, clarity in diff output reduces the number of times you have to re-run commands or re-open files just to confirm what actually moved.&lt;/p&gt;
&lt;h2 id="the-pattern-rust-isnt-replacing-pythonits-replacing-c"&gt;The pattern: Rust isn’t replacing Python—it’s replacing C&lt;/h2&gt;
&lt;p&gt;Here’s the sharper takeaway: Rust’s sweet spot, in this ecosystem, isn’t “yet another language for everything.” It’s replacing the systems tooling layer—the C-era Unix toolbox that powers your day-to-day performance needs.&lt;/p&gt;
&lt;p&gt;That matters because the Rust ecosystem is built on constraints that fit CLI design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Efficient binaries and predictable runtime behavior.&lt;/li&gt;
&lt;li&gt;Strong tooling for correctness and maintainability.&lt;/li&gt;
&lt;li&gt;A community culture that ships usable defaults and polished output.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can see it across these tools: &lt;code&gt;rg&lt;/code&gt; and &lt;code&gt;fd&lt;/code&gt; reduce wasted time; &lt;code&gt;bat&lt;/code&gt; improves readability; &lt;code&gt;eza&lt;/code&gt; makes directories legible; &lt;code&gt;zoxide&lt;/code&gt; accelerates navigation; &lt;code&gt;delta&lt;/code&gt; turns raw diffs into something you can actually interpret at speed.&lt;/p&gt;
&lt;p&gt;It’s not just a set of trendy commands. It’s an operating system for developers that happens to live in your terminal.&lt;/p&gt;
&lt;h2 id="conclusion-switch-your-defaults-not-just-your-commands"&gt;Conclusion: switch your defaults, not just your commands&lt;/h2&gt;
&lt;p&gt;If your terminal is where you work, you should treat CLI tooling like infrastructure. And the Rust ecosystem has done something rare: it replaced familiar classics with better UX and better performance without breaking the underlying Unix spirit.&lt;/p&gt;
&lt;p&gt;Start small. Pick one pain point—search, finding files, viewing content, listing directories, navigation, or diff readability—and swap in the Rust tool that targets it. Run it for a week. If you’re like most people, you won’t miss the old default—and you’ll wonder how you managed without it.&lt;/p&gt;</content></item><item><title>AI Code Review Is Coming Whether You Like It or Not</title><link>https://decastro.work/blog/ai-code-review-coming-whether-you-like-it/</link><pubDate>Sun, 14 Apr 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ai-code-review-coming-whether-you-like-it/</guid><description>&lt;p&gt;AI code review bots are no longer an experimental curiosity—they’re becoming a default layer of your engineering process. The uncomfortable truth is that this is happening regardless of whether your team trusts the tooling. The only real choice you have is whether you’ll treat AI review as a tool with guardrails (and measurable outcomes) or as a new source of code review theater that clutters your pull requests.&lt;/p&gt;
&lt;p&gt;The good news: modern AI review assistants are genuinely catching real bugs. The bad news: they’re also introducing fresh failure modes—especially around style churn, false positives, and verbose “autopsy reports” of changes everyone can already see. This article lays out what’s actually happening, where the value is real, and how to set policies so AI becomes signal—not noise.&lt;/p&gt;</description><content>&lt;p&gt;AI code review bots are no longer an experimental curiosity—they’re becoming a default layer of your engineering process. The uncomfortable truth is that this is happening regardless of whether your team trusts the tooling. The only real choice you have is whether you’ll treat AI review as a tool with guardrails (and measurable outcomes) or as a new source of code review theater that clutters your pull requests.&lt;/p&gt;
&lt;p&gt;The good news: modern AI review assistants are genuinely catching real bugs. The bad news: they’re also introducing fresh failure modes—especially around style churn, false positives, and verbose “autopsy reports” of changes everyone can already see. This article lays out what’s actually happening, where the value is real, and how to set policies so AI becomes signal—not noise.&lt;/p&gt;
&lt;h2 id="why-ai-reviews-are-suddenly-worth-paying-attention-to"&gt;Why AI reviews are suddenly worth paying attention to&lt;/h2&gt;
&lt;p&gt;Traditional code review is expensive because it’s mostly manual attention. A human reviewer can miss issues under time pressure, and they can’t easily enforce consistent checks across a large codebase. AI-powered review tools sit in the middle: they read diffs, infer intent, and then propose issues or improvements in the same place developers already work—inside pull requests.&lt;/p&gt;
&lt;p&gt;In practice, this often looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Null or invalid-state risks&lt;/strong&gt;: e.g., “&lt;code&gt;user&lt;/code&gt; can be null when passed into &lt;code&gt;calculateDiscount(user)&lt;/code&gt;” or “&lt;code&gt;response.getBody()&lt;/code&gt; may be null in error paths.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Untested edge cases&lt;/strong&gt;: e.g., “What happens when the list is empty?” or “&lt;code&gt;pageToken&lt;/code&gt; is optional, but pagination logic assumes it’s present.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security footguns&lt;/strong&gt;: e.g., “Avoid string concatenation in SQL queries,” “Don’t log secrets,” or “Validate untrusted input before deserialization.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exception and control-flow gaps&lt;/strong&gt;: e.g., “This catch block swallows the error and hides failures from callers.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are the kinds of problems that slip past even careful reviewers—especially when the diff is small but the behavioral impact is large. AI isn’t magical, but it &lt;em&gt;is&lt;/em&gt; fast, consistent, and good at noticing patterns that humans skim past.&lt;/p&gt;
&lt;p&gt;The key change is that the &lt;strong&gt;signal-to-noise ratio is improving&lt;/strong&gt;. Earlier generations of assistants were often verbose and wrong. Now, many teams report that the “this might be a real issue” comments land more often than not—especially in languages and frameworks where common bug patterns repeat frequently.&lt;/p&gt;
&lt;h2 id="the-new-problem-isnt-mistakesits-review-theater"&gt;The new problem isn’t mistakes—it’s review theater&lt;/h2&gt;
&lt;p&gt;As AI reviews become more common, the biggest threat to developer velocity isn’t that bots are wrong. It’s that bots can be &lt;em&gt;confident&lt;/em&gt; while producing comments that don’t deserve engineering time.&lt;/p&gt;
&lt;p&gt;Code review theater looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A bot drops a multi-paragraph summary of a change that the author already knows.&lt;/li&gt;
&lt;li&gt;It recommends renaming variables or reformatting code in ways that conflict with team conventions.&lt;/li&gt;
&lt;li&gt;It flags “issues” that are intentional—like a deliberate empty catch block used to preserve backward compatibility or an exception-throwing pattern that’s enforced by an architectural guideline.&lt;/li&gt;
&lt;li&gt;It repeats the same generic suggestions across many files, turning each PR into a small inbox war.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s a concrete example. Imagine a team uses a particular Java style:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Intentionally empty: we preserve behavior for legacy clients.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (IOException ignored) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An AI reviewer might suggest logging the exception or rethrowing it. Both are reasonable &lt;em&gt;in general&lt;/em&gt;, but if the team policy is to preserve legacy semantics, the review comment becomes noise. If the bot keeps generating these “reasonable but policy-breaking” suggestions, developers start ignoring AI feedback entirely—or worse, argue with it in every PR.&lt;/p&gt;
&lt;p&gt;That’s the new theater category: &lt;strong&gt;review comments that sound technical but don’t map to the team’s actual correctness contract&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="where-ai-actually-helps-most-and-where-it-doesnt"&gt;Where AI actually helps most (and where it doesn’t)&lt;/h2&gt;
&lt;p&gt;AI code review is strongest when the reviewer’s job is to enforce &lt;em&gt;repeatable&lt;/em&gt; constraints and spot &lt;em&gt;known&lt;/em&gt; risk patterns. It’s weakest when the reviewer’s job is to enforce &lt;em&gt;human judgment&lt;/em&gt;—things like architectural tradeoffs, domain-specific invariants, and stylistic consistency that isn’t codified anywhere.&lt;/p&gt;
&lt;h3 id="high-value-targets-for-ai-review"&gt;High-value targets for AI review&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Input validation and trust boundaries&lt;/strong&gt;: “Where does this value come from?” “Is it sanitized before use?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource handling&lt;/strong&gt;: streams, DB connections, timeouts, cancellation tokens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error handling quality&lt;/strong&gt;: swallowing exceptions, missing retries, inconsistent error propagation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency hazards&lt;/strong&gt;: shared state, lock ordering issues, unsafe access patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency and configuration security&lt;/strong&gt;: insecure defaults, weak cipher choices, unsafe file operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="low-value-targets-for-ai-review"&gt;Low-value targets for AI review&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure style preferences&lt;/strong&gt;: line wrapping, naming opinions, formatter debates (unless your team explicitly wants it).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Over-explanations&lt;/strong&gt;: “This change updates validation logic” when the diff already makes that obvious.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Policy conflicts&lt;/strong&gt;: anything that contradicts documented conventions (logging rules, exception patterns, performance constraints).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical takeaway: if you don’t already have a crisp definition of “what counts as correctness,” AI will happily propose what &lt;em&gt;it thinks&lt;/em&gt; correctness means. That’s why the next section matters.&lt;/p&gt;
&lt;h2 id="establish-policies-now-treat-ai-as-an-automated-reviewer-with-rules"&gt;Establish policies now: treat AI as an automated reviewer with rules&lt;/h2&gt;
&lt;p&gt;If you wait until AI becomes ubiquitous in your org, you’ll be forced into reactive cleanup. Start now with a few concrete policies that determine what AI is allowed to comment on, how teams respond, and how the organization measures success.&lt;/p&gt;
&lt;h3 id="1-decide-what-ai-must-never-override"&gt;1) Decide what AI must never override&lt;/h3&gt;
&lt;p&gt;Write down a short list of “AI non-goals.” For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No stylistic refactors unless they are &lt;em&gt;required&lt;/em&gt; for correctness.&lt;/li&gt;
&lt;li&gt;No comments that conflict with documented architecture or legacy compatibility policies.&lt;/li&gt;
&lt;li&gt;No blocking PRs for issues labeled “suggestions” or “considerations,” unless they match an explicit rule set.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then enforce it operationally: require that PR authors can mark AI comments as “won’t fix (policy)” without starting debate.&lt;/p&gt;
&lt;h3 id="2-require-a-classification-bug--risk--style--explanation"&gt;2) Require a classification: “Bug / Risk / Style / Explanation”&lt;/h3&gt;
&lt;p&gt;Good AI tooling already returns structured comments sometimes, but even if it doesn’t, your team can enforce a convention:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bug&lt;/strong&gt;: likely incorrect behavior (null dereference, missing return, incorrect condition).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk&lt;/strong&gt;: likely issue under certain conditions (unhandled edge cases, insecure defaults).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Style&lt;/strong&gt;: readability or formatting improvements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: redundant “what changed” summaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your policy can then say: bugs and risks must be addressed (or explicitly justified). Style and explanation should be ignored unless you opt in.&lt;/p&gt;
&lt;h3 id="3-convert-ai-feedback-into-actionable-diff-changes"&gt;3) Convert AI feedback into “actionable diff changes”&lt;/h3&gt;
&lt;p&gt;A bot that says “Consider improving exception handling” is low value. A bot that says “This path can throw NPE because &lt;code&gt;x&lt;/code&gt; is null; fix by guarding before dereference” is actionable. When reviewing AI output, prioritize comments that point to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the exact location in the diff,&lt;/li&gt;
&lt;li&gt;the reason it’s a risk,&lt;/li&gt;
&lt;li&gt;and the minimal change needed to fix it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If AI can’t do that, it probably belongs in the “theater” bucket.&lt;/p&gt;
&lt;h3 id="4-make-the-bots-output-measurable"&gt;4) Make the bot’s output measurable&lt;/h3&gt;
&lt;p&gt;You don’t need complicated metrics. Just track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Accepted AI findings&lt;/strong&gt;: how many bot comments you actually fix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dismissed AI findings&lt;/strong&gt;: and why (false positive, policy conflict, style).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time cost&lt;/strong&gt;: did it slow down PR merges or add helpful pre-checks?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Within a few weeks, you’ll learn whether your team should tune the tool, change prompt settings (where supported), or adjust which checks are enabled. Most importantly, you’ll stop treating AI as a vibes-based debate.&lt;/p&gt;
&lt;h2 id="integrate-ai-with-cicd-not-as-a-replacement-for-engineering-judgment"&gt;Integrate AI with CI/CD, not as a replacement for engineering judgment&lt;/h2&gt;
&lt;p&gt;The mistake many teams make is trying to make AI “the reviewer of record.” Don’t. AI should complement your existing guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static analysis&lt;/strong&gt;: linters, type checkers, security scanners.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests&lt;/strong&gt;: unit/integration, plus targeted regression tests for previously found classes of bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAST/DAST&lt;/strong&gt;: where appropriate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build-time policies&lt;/strong&gt;: required code owners, required checks, and branch protection.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI should primarily help before code hits these systems: catching obvious mistakes while the change is still cheap to modify. Think of it as the earliest possible triage layer.&lt;/p&gt;
&lt;p&gt;Here’s a practical workflow that tends to work well:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Developer opens PR.&lt;/li&gt;
&lt;li&gt;AI runs and posts comments.&lt;/li&gt;
&lt;li&gt;The author resolves or tags each comment according to your classification policy.&lt;/li&gt;
&lt;li&gt;Humans focus on the remaining high-impact review: architecture, performance, semantics, and testing strategy.&lt;/li&gt;
&lt;li&gt;CI verifies correctness. AI becomes a pre-filter, not a final authority.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach keeps AI from becoming a parallel “review meeting” that never ends.&lt;/p&gt;
&lt;h2 id="what-to-do-in-the-next-quarter-a-rollout-plan-that-wont-break-your-team"&gt;What to do in the next quarter: a rollout plan that won’t break your team&lt;/h2&gt;
&lt;p&gt;If AI review is coming for you, treat the rollout like any other tool adoption—with sequencing, expectations, and escape hatches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Week 1–2: Establish rules.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create the classification scheme (Bug/Risk/Style/Explanation).&lt;/li&gt;
&lt;li&gt;Define “allowed” and “not allowed” comment types.&lt;/li&gt;
&lt;li&gt;Document the expected response format for each category.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Week 3–4: Pilot on a limited scope.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose one repo or a subset of services.&lt;/li&gt;
&lt;li&gt;Start with non-blocking review comments.&lt;/li&gt;
&lt;li&gt;Collect dismissal reasons to spot policy conflicts quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Week 5–8: Tune and integrate.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disable style-heavy suggestions if they dominate the feed.&lt;/li&gt;
&lt;li&gt;Encourage the team to tag false positives so you can adjust later.&lt;/li&gt;
&lt;li&gt;Link AI findings to existing checks (e.g., when it flags insecure patterns, ensure your security scanners confirm them).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Week 9–12: Expand with confidence.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If bug and risk acceptance rates are high &lt;em&gt;and&lt;/em&gt; PR cycle time doesn’t worsen, expand.&lt;/li&gt;
&lt;li&gt;If theater dominates, tighten rules and reduce comment types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most teams don’t fail because the tool is bad. They fail because they try to roll out AI review without first deciding what “good review” means.&lt;/p&gt;
&lt;h2 id="conclusion-the-future-is-automated-reviewso-make-it-automated-correctly"&gt;Conclusion: the future is automated review—so make it automated &lt;em&gt;correctly&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;AI code review is not a question of if; it’s a question of how smoothly you absorb it. The best bots are already finding real bugs and security risks. The worst outcomes happen when teams treat AI comments as a new kind of mandatory ritual, generating theater instead of accountability.&lt;/p&gt;
&lt;p&gt;Your move is simple and urgent: set policies now, classify AI feedback, measure what’s useful, and integrate AI with the rest of your CI/CD guardrails. If you do that, AI won’t replace your reviewers—it’ll make them faster, sharper, and less burdened by the kinds of repetitive mistakes humans are never going to catch every time.&lt;/p&gt;</content></item><item><title>HTMX Crossed the Chasm: Production Adoption Stories from Real Teams</title><link>https://decastro.work/blog/htmx-crossed-chasm-production-adoption/</link><pubDate>Mon, 08 Apr 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/htmx-crossed-chasm-production-adoption/</guid><description>&lt;p&gt;A year ago, htmx was still being debated in the “interesting demo” lane. Now it’s showing up where it matters: internal tools, admin panels, dashboards, and CRUD-heavy apps that need to feel fast—without turning your frontend into a second engineering organization. Teams aren’t adopting htmx because it’s clever. They’re adopting it because it’s pragmatic.&lt;/p&gt;
&lt;p&gt;Below are the patterns that kept repeating across fifteen production teams in 2024—spanning Fortune 500 internal tooling, Django modernization efforts, Go and .NET backends, and even Rails teams rediscovering the power of server-driven UI. The takeaway is simple and slightly contrarian: htmx succeeds when you stop treating server-rendered HTML like a limitation and start treating it like an advantage.&lt;/p&gt;</description><content>&lt;p&gt;A year ago, htmx was still being debated in the “interesting demo” lane. Now it’s showing up where it matters: internal tools, admin panels, dashboards, and CRUD-heavy apps that need to feel fast—without turning your frontend into a second engineering organization. Teams aren’t adopting htmx because it’s clever. They’re adopting it because it’s pragmatic.&lt;/p&gt;
&lt;p&gt;Below are the patterns that kept repeating across fifteen production teams in 2024—spanning Fortune 500 internal tooling, Django modernization efforts, Go and .NET backends, and even Rails teams rediscovering the power of server-driven UI. The takeaway is simple and slightly contrarian: htmx succeeds when you stop treating server-rendered HTML like a limitation and start treating it like an advantage.&lt;/p&gt;
&lt;h2 id="why-htmx-clicked-in-production-environments"&gt;Why htmx “clicked” in production environments&lt;/h2&gt;
&lt;p&gt;Most teams don’t start with htmx as a grand architectural rewrite. They start with a concrete pain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Our admin UI feels sluggish.”&lt;/li&gt;
&lt;li&gt;“We’re shipping too much JavaScript for simple interactions.”&lt;/li&gt;
&lt;li&gt;“We can’t keep the frontend and backend in sync without constant coordination.”&lt;/li&gt;
&lt;li&gt;“The SPA route is expensive—too many states, too many edge cases, too many bugs.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;htmx offers a way out that aligns with how many organizations already build: render HTML on the server, then progressively enhance it with targeted requests. Instead of building a full client-side application shell, you sprinkle interactivity onto server-rendered pages.&lt;/p&gt;
&lt;p&gt;That sounds academic until you watch teams operationalize it. In practice, htmx let them keep their existing backend model layer (Django models, Rails models, Go handlers, .NET controllers) and reuse their rendering templates. The “frontend” became a thin layer of markup + attributes, not a new runtime.&lt;/p&gt;
&lt;h2 id="the-common-migration-path-start-small-get-real-value-in-days"&gt;The common migration path: start small, get real value in days&lt;/h2&gt;
&lt;p&gt;Across the teams that adopted htmx successfully, the story was consistently incremental. They didn’t replace everything at once; they replaced the parts that were slow, brittle, or overly complex.&lt;/p&gt;
&lt;p&gt;A typical sequence looked like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Identify the worst UX hotspots&lt;/strong&gt;&lt;br&gt;
Search for interactions that currently require page reloads or complicated JavaScript—filters, inline edits, status toggles, pagination, dependent dropdowns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert one interaction to htmx&lt;/strong&gt;&lt;br&gt;
For example, replace a “Submit → reload page” flow with an htmx request that updates only the relevant fragment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wire server responses to HTML, not JSON&lt;/strong&gt;&lt;br&gt;
Templates return markup for the updated component. The browser swaps it in.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scale gradually&lt;/strong&gt;&lt;br&gt;
Once the team trusts the pattern, more interactions move to htmx one at a time.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One Django team described converting an admin filter: instead of requesting data via a JS layer and re-rendering tables client-side, they returned the filtered table fragment from Django and let htmx swap it in. The result wasn’t just faster development—it was fewer moving parts. No client-side state machine. No duplicated rendering logic. No synchronization problems.&lt;/p&gt;
&lt;p&gt;In the dataset, the average time to “productive htmx usage” was about &lt;strong&gt;three days&lt;/strong&gt;—not weeks. That speed matters because it prevents the migration from becoming a never-ending design project.&lt;/p&gt;
&lt;h2 id="what-reduced-frontend-complexity-actually-meant"&gt;What “reduced frontend complexity” actually meant&lt;/h2&gt;
&lt;p&gt;Teams reported an average &lt;strong&gt;60–80% reduction in frontend complexity&lt;/strong&gt;. That phrasing can sound vague, so here’s what it usually looked like in real codebases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Less UI duplication&lt;/strong&gt;: with htmx, the server already knows how to render “the truth.” The browser doesn’t need a separate rendering pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer libraries&lt;/strong&gt;: teams leaned on existing template tooling instead of layering state management and routing frameworks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller cognitive surface area&lt;/strong&gt;: rather than reasoning about component trees, hydration, and async state, developers reason about &lt;em&gt;requests&lt;/em&gt; and &lt;em&gt;responses&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider an internal approval workflow. Before htmx, a team might have built a React component hierarchy just to support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Approve/reject buttons&lt;/li&gt;
&lt;li&gt;Reason fields shown conditionally&lt;/li&gt;
&lt;li&gt;Status badges updating instantly&lt;/li&gt;
&lt;li&gt;Activity logs expanding without full reloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With htmx, the page becomes a set of server-rendered components, each with an endpoint that can return updated HTML fragments. The “asynchrony” is handled by HTTP requests and HTML swaps—meaning the mental model stays close to web fundamentals.&lt;/p&gt;
&lt;p&gt;And yes, this reduces bug classes. You still have edge cases, but they’re generally “HTTP and rendering” edge cases, not “client state drift” edge cases.&lt;/p&gt;
&lt;h2 id="the-fortune-500-internal-tools-pattern-security--velocity"&gt;The Fortune 500 internal tools pattern: security + velocity&lt;/h2&gt;
&lt;p&gt;One reason htmx landed early in enterprise internal tooling: it fits how these systems are governed. Many Fortune 500 environments have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mature authentication/authorization patterns&lt;/li&gt;
&lt;li&gt;strict audit requirements&lt;/li&gt;
&lt;li&gt;existing server-side validation&lt;/li&gt;
&lt;li&gt;legacy-ish templates that still run critical workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;htmx made it feasible to modernize without detonating the security model. Instead of building a new SPA that must re-implement authentication flows, authorization checks, and permission logic on the client, teams kept authorization on the server. The browser just asks for HTML fragments.&lt;/p&gt;
&lt;p&gt;A common production decision: &lt;strong&gt;make every htmx request go through the same authorization gates as full-page requests&lt;/strong&gt;. That preserves auditability and reduces “special-case” vulnerabilities where a fragment endpoint might accidentally become less protected than the surrounding page.&lt;/p&gt;
&lt;p&gt;Where teams really felt the impact was speed to iteration. Instead of coordination overhead between backend and a complex frontend team, a full-stack developer could add an interaction by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;updating a template fragment&lt;/li&gt;
&lt;li&gt;wiring an endpoint&lt;/li&gt;
&lt;li&gt;adding an htmx attribute to the triggering element&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not theoretical. It’s the difference between “we’ll add that feature next quarter” and “we shipped it this week.”&lt;/p&gt;
&lt;h2 id="where-teams-struggled-treating-htmx-like-a-spa-framework"&gt;Where teams struggled: treating htmx like a SPA framework&lt;/h2&gt;
&lt;p&gt;For every success story, there’s a failure mode. The teams that struggled shared one anti-pattern: they tried to use htmx as if it were a replacement for SPA-level state management.&lt;/p&gt;
&lt;p&gt;The symptom was usually the same:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lots of client-side logic creeping into the markup&lt;/li&gt;
&lt;li&gt;“component state” being tracked in the browser anyway&lt;/li&gt;
&lt;li&gt;endpoints returning fragments that depended on complex hidden state&lt;/li&gt;
&lt;li&gt;interactions that required global client coordination&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, they built a pseudo-SPA—but with none of the ergonomic guarantees of a real SPA framework. That combination is painful.&lt;/p&gt;
&lt;p&gt;The practical fix is architectural honesty: &lt;strong&gt;embrace server-driven UI&lt;/strong&gt;. Let the server be the source of truth for what the UI should look like after an action. If you need shared state, push it into the rendered HTML or session-backed server state—then return updated fragments that reflect it.&lt;/p&gt;
&lt;p&gt;A useful rule of thumb:&lt;br&gt;
If an interaction requires complex client-side state orchestration, it’s probably not an htmx-first problem. htmx shines when the “next UI” can be derived from the request and server state.&lt;/p&gt;
&lt;h2 id="concrete-patterns-that-worked-across-django-rails-go-and-net"&gt;Concrete patterns that worked across Django, Rails, Go, and .NET&lt;/h2&gt;
&lt;p&gt;Even with different stacks, the best teams converged on similar techniques.&lt;/p&gt;
&lt;h3 id="1-treat-ui-fragments-as-first-class-endpoints"&gt;1) Treat UI fragments as first-class endpoints&lt;/h3&gt;
&lt;p&gt;Create dedicated routes that return HTML snippets for specific components—tables, forms, status panels. Keep the contract simple: request parameters in, HTML fragment out.&lt;/p&gt;
&lt;p&gt;Example flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User clicks “Toggle status”&lt;/li&gt;
&lt;li&gt;htmx POSTs to &lt;code&gt;/admin/items/{id}/toggle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Server returns the updated table row or badge fragment&lt;/li&gt;
&lt;li&gt;htmx swaps it into place&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-use-progressive-enhancement-intentionally"&gt;2) Use progressive enhancement intentionally&lt;/h3&gt;
&lt;p&gt;Start with a functional HTML page. Then layer htmx on top for interactions that benefit from partial updates. This keeps accessibility and non-JavaScript behavior intact without extra work.&lt;/p&gt;
&lt;h3 id="3-make-errors-renderable"&gt;3) Make errors renderable&lt;/h3&gt;
&lt;p&gt;Instead of returning raw JSON errors that the client must interpret, return the same fragment (or a close cousin) with validation messages and proper markup. Your UI stays consistent because it’s still server-rendered.&lt;/p&gt;
&lt;h3 id="4-keep-requests-scoped"&gt;4) Keep requests scoped&lt;/h3&gt;
&lt;p&gt;Avoid turning the entire page into a single fragment update. Fine-grained swaps make the UI feel instant and reduce bandwidth. More importantly, it keeps debugging straightforward: you can inspect exactly what changed and why.&lt;/p&gt;
&lt;h3 id="5-prefer-server-side-templates-to-client-templates"&gt;5) Prefer server-side templates to “client templates”&lt;/h3&gt;
&lt;p&gt;If the browser is going to render HTML, you’re back to a SPA problem—only with worse tooling. htmx encourages you to render on the server, where your existing template logic already lives.&lt;/p&gt;
&lt;p&gt;Across Django shops, Rails teams, and backends in Go and .NET, those patterns reduced friction more than any specific “hacks.” The winning approach was boring: clean server endpoints, clean templates, targeted swaps.&lt;/p&gt;
&lt;h2 id="conclusion-the-chasm-isnt-about-technologyits-about-mindset"&gt;Conclusion: the chasm isn’t about technology—it’s about mindset&lt;/h2&gt;
&lt;p&gt;htmx crossed the chasm because it respects how web apps work in the real world: server-rendered HTML, incremental enhancements, and straightforward request/response flows. Teams adopted it quickly—often within days—because it didn’t require a wholesale frontend re-platforming effort.&lt;/p&gt;
&lt;p&gt;The consistent lesson from production teams in 2024 is equally clear: &lt;strong&gt;don’t try to make htmx behave like a SPA framework.&lt;/strong&gt; Make it behave like what it is—a server-driven UI enhancement layer. If you do, you’ll get fast UI updates, smaller frontend complexity, and a migration path your engineers can actually sustain.&lt;/p&gt;</content></item><item><title>Svelte 5 Runes: The Framework That Keeps Getting Simpler</title><link>https://decastro.work/blog/svelte-5-runes-framework-simpler/</link><pubDate>Tue, 02 Apr 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/svelte-5-runes-framework-simpler/</guid><description>&lt;p&gt;UI frameworks don’t usually get simpler over time—they get more clever. They add new knobs, new mental models, and new ways to accidentally shoot your feet. Svelte 5 is different. With Runes, it leans harder into a philosophy that has always been its edge: stop fighting JavaScript’s ergonomics and instead compile the work you don’t want to do.&lt;/p&gt;
&lt;p&gt;If you’ve ever thought “React is powerful, but why does it feel like I’m wiring the app myself?”—Svelte’s approach will feel like relief. Not because it’s less capable, but because it’s more direct.&lt;/p&gt;</description><content>&lt;p&gt;UI frameworks don’t usually get simpler over time—they get more clever. They add new knobs, new mental models, and new ways to accidentally shoot your feet. Svelte 5 is different. With Runes, it leans harder into a philosophy that has always been its edge: stop fighting JavaScript’s ergonomics and instead compile the work you don’t want to do.&lt;/p&gt;
&lt;p&gt;If you’ve ever thought “React is powerful, but why does it feel like I’m wiring the app myself?”—Svelte’s approach will feel like relief. Not because it’s less capable, but because it’s more direct.&lt;/p&gt;
&lt;h2 id="the-react-vs-svelte-philosophy-gap-and-why-it-matters"&gt;The React vs. Svelte philosophy gap (and why it matters)&lt;/h2&gt;
&lt;p&gt;React’s story is built around one question: &lt;em&gt;how do we make JavaScript do UI?&lt;/em&gt; That leads to a particular kind of framework: one that gives you primitives (components, hooks, effects) and expects you to assemble the rest with discipline and conventions.&lt;/p&gt;
&lt;p&gt;Svelte starts from a different question: &lt;em&gt;how do we make UI easy?&lt;/em&gt; It treats UI as a compile-time artifact. Instead of asking you to manage reactivity and rendering correctness with runtime abstractions, it asks how to generate the optimal code up front.&lt;/p&gt;
&lt;p&gt;Runes are the sharpest expression of that gap. They don’t add more complexity to the developer experience; they reduce the number of times you have to remember what the framework needs from you.&lt;/p&gt;
&lt;h2 id="what-runic-reactivity-feels-like-in-practice"&gt;What “runic reactivity” feels like in practice&lt;/h2&gt;
&lt;p&gt;Runes are Svelte’s reactivity system for Svelte 5. The headline idea is straightforward: you declare intent in a small, explicit syntax, and Svelte tracks what needs updating.&lt;/p&gt;
&lt;p&gt;In other words, you don’t “teach” reactivity by using a particular lifecycle pattern or by memorizing rules like “this must be immutable” or “this dependency must be listed.” You describe reactive state and derived values, and the compiler handles the wiring.&lt;/p&gt;
&lt;p&gt;Here’s the kind of mental shift you’re looking for.&lt;/p&gt;
&lt;h3 id="a-simple-reactive-state"&gt;A simple reactive state&lt;/h3&gt;
&lt;p&gt;In Svelte 5, reactive state can be expressed directly, without the old pattern of sprinkling &lt;code&gt;$:&lt;/code&gt; everywhere just to coax updates into happening.&lt;/p&gt;
&lt;p&gt;Instead of thinking “when do I need &lt;code&gt;$:&lt;/code&gt;?”, you think “this variable is state.” Then you update it like normal state.&lt;/p&gt;
&lt;p&gt;The practical impact is less code and fewer accidental bugs—especially in components that evolve over time.&lt;/p&gt;
&lt;h3 id="derived-values-without-effect-gymnastics"&gt;Derived values without effect gymnastics&lt;/h3&gt;
&lt;p&gt;Once you have state, you almost always need derived values: formatting, filtering, computed totals, and so on. React tends to push people toward &lt;code&gt;useMemo&lt;/code&gt; or derived state inside render, then adds a new layer of complexity: “is this memo actually needed, and did I get the dependency array right?”&lt;/p&gt;
&lt;p&gt;Runes push derived state into the language of the framework itself. The result is that computed values stay aligned with their inputs by construction, not by developer diligence.&lt;/p&gt;
&lt;p&gt;A common example: you have a &lt;code&gt;price&lt;/code&gt; and a &lt;code&gt;quantity&lt;/code&gt;, and you want &lt;code&gt;total&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In React, you might write logic inside render and hope it’s fast enough, or you might introduce &lt;code&gt;useMemo&lt;/code&gt; and carefully manage dependencies.&lt;/li&gt;
&lt;li&gt;In Svelte with runic reactivity, the derived value reads like a first-class concept. You declare it once; Svelte keeps it correct.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s the difference between “patterns” and “capabilities.”&lt;/p&gt;
&lt;h2 id="use-is-replaced-by-just-works-ergonomics-over-rituals"&gt;“use” is replaced by “just works”: ergonomics over rituals&lt;/h2&gt;
&lt;p&gt;Svelte 5 isn’t just about reactivity; it’s also about reducing the rituals developers pick up when they want reliable behavior.&lt;/p&gt;
&lt;p&gt;React has its own modern rituals—server components, &lt;code&gt;use()&lt;/code&gt; for suspending data flows, and increasingly arcane rendering patterns where correctness depends on when code runs and where state lives. Even when these tools are well-designed, they introduce new ways to be confused.&lt;/p&gt;
&lt;p&gt;Svelte 5’s Runes aim to make the default behavior predictable. You don’t need to re-architect your component just to get updates to propagate cleanly. You express state and derivations directly, and the compiler does the bookkeeping.&lt;/p&gt;
&lt;p&gt;If you’re building a form, the experience is especially noticeable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React forms often become a tangle of state updates, controlled inputs, effects, and edge-case handling (stale closures, synchronization, rerender cascades).&lt;/li&gt;
&lt;li&gt;Svelte can be more direct: state updates trigger the right downstream updates without you having to orchestrate a web of handlers and effects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Developers don’t just want fewer lines—they want fewer &lt;em&gt;failure modes&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="why-the-compiler-magic-angle-is-not-marketing-fluff"&gt;Why the “compiler magic” angle is not marketing fluff&lt;/h2&gt;
&lt;p&gt;It’s tempting to treat “compiler” as a buzzword. But the practical payoff is tangible: less framework runtime, fewer abstractions, and less code shipped to the browser.&lt;/p&gt;
&lt;p&gt;Svelte’s model is that your app is compiled into efficient JavaScript that includes the specific update logic for your components. That means users download less generic runtime code than they would with frameworks that perform more work at runtime to discover dependencies.&lt;/p&gt;
&lt;p&gt;In the same way that a SQL query planner can optimize execution before the database even runs the query, Svelte compiles away generic uncertainty. It knows your component structure ahead of time, so it can produce targeted update logic.&lt;/p&gt;
&lt;p&gt;This isn’t only about performance micro-benchmarks; it’s about simplicity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your mental model becomes smaller because the framework isn’t asking you to provide as many runtime hints.&lt;/li&gt;
&lt;li&gt;Your bundle tends to be leaner because you aren’t carrying as much general machinery.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most “Svelte” part of this is not that it’s fast. It’s that it feels like the framework is on your side.&lt;/p&gt;
&lt;h2 id="a-realistic-migration-mindset-adopt-runes-where-they-hurt-most"&gt;A realistic migration mindset: adopt Runes where they hurt most&lt;/h2&gt;
&lt;p&gt;If you’re already comfortable with Svelte, Runes aren’t something you need to fear. They’re the natural evolution of how Svelte thinks about reactive intent.&lt;/p&gt;
&lt;p&gt;If you’re coming from another framework, treat Runes as an adoption path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with state: model your UI with explicit reactive values.&lt;/li&gt;
&lt;li&gt;Add derived data: prefer computed transformations over “manual syncing” logic.&lt;/li&gt;
&lt;li&gt;Only then worry about advanced patterns.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This order matters. The fastest way to feel friction is to start with patterns you already understand—then layer Runes in second. Instead, let Runes reshape your defaults.&lt;/p&gt;
&lt;p&gt;A practical example: suppose you’re building a dashboard with filters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, model &lt;code&gt;filters&lt;/code&gt; as state.&lt;/li&gt;
&lt;li&gt;Next, derive &lt;code&gt;filteredRows&lt;/code&gt; from &lt;code&gt;filters&lt;/code&gt; and your raw dataset.&lt;/li&gt;
&lt;li&gt;Finally, derive any UI strings (like “Showing 12 of 48 results”) from those computed values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll end up with a component where the dataflow is visible in the code, not hidden in the lifecycle timing of effects.&lt;/p&gt;
&lt;h2 id="developer-satisfaction-isnt-just-tasteits-feedback-loops"&gt;Developer satisfaction isn’t just taste—it’s feedback loops&lt;/h2&gt;
&lt;p&gt;There’s a reason developers tend to stick with Svelte when they switch: the feedback loop is tight. Changes in state lead to predictable UI updates. The code reads like intent. Debugging is less about tracing framework mechanics and more about checking your own logic.&lt;/p&gt;
&lt;p&gt;That’s what high satisfaction usually means in practice. Not that nothing goes wrong, but that when it does, you’re not fighting the framework.&lt;/p&gt;
&lt;p&gt;And it’s not a small difference. When your framework reduces boilerplate and discourages incorrect patterns, developers spend more time shipping the product and less time maintaining “framework correctness scaffolding.”&lt;/p&gt;
&lt;p&gt;That is exactly the kind of payoff you get when a reactivity system is designed to be intuitive instead of merely powerful.&lt;/p&gt;
&lt;h2 id="conclusion-svelte-5-keeps-moving-in-the-right-direction"&gt;Conclusion: Svelte 5 keeps moving in the right direction&lt;/h2&gt;
&lt;p&gt;Runes in Svelte 5 aren’t just a new feature—they’re a continuation of the same promise: make UI authoring easier by compiling the heavy lifting away. The result is a framework that feels less like you’re negotiating with runtime behavior and more like you’re describing the UI you want.&lt;/p&gt;
&lt;p&gt;If React often makes you ask &lt;em&gt;how do I make this work?&lt;/em&gt;, Svelte with Runes pushes you toward &lt;em&gt;here’s what the UI should be&lt;/em&gt;. And for a lot of teams, that shift is the difference between “framework that powers the app” and “framework that gets out of your way.”&lt;/p&gt;</content></item><item><title>Effect-TS Is the Most Important TypeScript Library Nobody's Using</title><link>https://decastro.work/blog/effect-ts-most-important-typescript-library/</link><pubDate>Thu, 21 Mar 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/effect-ts-most-important-typescript-library/</guid><description>&lt;p&gt;TypeScript made “working code” easy—but it didn’t make &lt;em&gt;reliable code&lt;/em&gt; easy. Try/catch spaghetti creeps in, resources leak quietly, and error handling turns into a web of ad-hoc conventions no one remembers agreeing to. Effect-TS tackles those problems head-on with a composable model for errors, dependencies, and concurrency—without asking you to speak fluent category theory. It’s the rare library that feels like it was built by people who’ve spent nights debugging production incidents.&lt;/p&gt;</description><content>&lt;p&gt;TypeScript made “working code” easy—but it didn’t make &lt;em&gt;reliable code&lt;/em&gt; easy. Try/catch spaghetti creeps in, resources leak quietly, and error handling turns into a web of ad-hoc conventions no one remembers agreeing to. Effect-TS tackles those problems head-on with a composable model for errors, dependencies, and concurrency—without asking you to speak fluent category theory. It’s the rare library that feels like it was built by people who’ve spent nights debugging production incidents.&lt;/p&gt;
&lt;h2 id="what-effect-ts-actually-changes-and-what-it-doesnt"&gt;What Effect-TS actually changes (and what it doesn’t)&lt;/h2&gt;
&lt;p&gt;Effect-TS introduces a single core idea: instead of throwing and catching, you build &lt;em&gt;descriptions of work&lt;/em&gt; that the runtime can execute safely. Think of it as a typed, structured “program” that can express:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Typed errors&lt;/strong&gt; that are explicit and composable across boundaries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency injection&lt;/strong&gt; without a container framework&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency&lt;/strong&gt; with structured lifetimes, so tasks don’t outlive what they should&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational concerns&lt;/strong&gt; like retries, timeouts, caching, and observability—treated as first-class capabilities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Crucially, it doesn’t require you to adopt a new team-wide philosophy overnight. You can use it surgically: wrap the riskiest parts (network calls, file operations, background jobs) and leave the rest of your app alone. The best way to understand it is to compare it to the common alternative: &lt;code&gt;Promise&lt;/code&gt; + &lt;code&gt;try/catch&lt;/code&gt; + “whatever error shape happens to exist today.”&lt;/p&gt;
&lt;p&gt;Effect doesn’t eliminate asynchrony. It makes it &lt;em&gt;disciplined&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="typed-errors-that-dont-evaporate-across-boundaries"&gt;Typed errors that don’t evaporate across boundaries&lt;/h2&gt;
&lt;p&gt;In “classic” TypeScript, errors tend to become vibes. You’ll see something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetchUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Promise&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;User&lt;/span&gt;&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;api&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/users/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;catch&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Was this a timeout? 404? auth failure?
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now multiply that across services, and you get the real problem: &lt;strong&gt;the type system stops helping the moment the error crosses a function boundary&lt;/strong&gt;. Effect-TS keeps error types attached to the work that can fail.&lt;/p&gt;
&lt;p&gt;Practically, that means your function signatures communicate what can go wrong. You can also compose failures: map an error from one layer into a more meaningful domain error at the boundary.&lt;/p&gt;
&lt;p&gt;A realistic example: suppose a payment service talks to an external provider. You want your application layer to know whether failures are “provider unavailable” vs “card declined,” and you want to preserve that classification even when you orchestrate multiple effects.&lt;/p&gt;
&lt;p&gt;In Effect-TS terms, you’d create a workflow that can fail with a specific set of error types, and then transform them deliberately when crossing boundaries. The result is less defensive coding (“catch everything and rethrow”) and more intentional error contracts.&lt;/p&gt;
&lt;p&gt;Opinionated takeaway: if you’ve ever written &lt;code&gt;catch (e) { throw new Error(&amp;quot;Something went wrong&amp;quot;) }&lt;/code&gt; and regretted it later, typed errors are the missing ingredient.&lt;/p&gt;
&lt;h2 id="dependency-injection-without-the-container-tax"&gt;Dependency injection without the container tax&lt;/h2&gt;
&lt;p&gt;If your experience with dependency injection is mostly a Spring-style container, you might think this becomes heavyweight. Effect-TS takes a different approach: dependencies are provided explicitly to the “program,” not hidden behind global mutable singletons.&lt;/p&gt;
&lt;p&gt;That matters because it improves two things teams usually fight over:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: swap real services with fakes quickly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clarity&lt;/strong&gt;: see what a unit of work needs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instead of constructing everything manually inside the function (or relying on global variables), you define the effect to require certain capabilities (like &lt;code&gt;HttpClient&lt;/code&gt;, &lt;code&gt;Clock&lt;/code&gt;, &lt;code&gt;Logger&lt;/code&gt;, or database access), then provide those capabilities at runtime.&lt;/p&gt;
&lt;p&gt;Concretely, the win is this: when you test a piece of logic, you can supply deterministic implementations for time, randomness, storage, or external calls. No more brittle “monkey patching” modules or spinning up entire integration environments just to test a retry policy.&lt;/p&gt;
&lt;p&gt;Also, because dependencies are scoped to the effect execution, you avoid the classic failure mode of DI containers: accidental cross-test contamination or accidental sharing of state.&lt;/p&gt;
&lt;p&gt;The best part? You don’t need to refactor your entire codebase. Start with one module that already depends on half a dozen collaborators (common in API handlers and job processors), and convert it into an effect that states its needs explicitly.&lt;/p&gt;
&lt;h2 id="structured-concurrency-that-prevents-resource-leaks"&gt;Structured concurrency that prevents resource leaks&lt;/h2&gt;
&lt;p&gt;JavaScript concurrency is powerful—and remarkably good at leaking resources. You kick off a background task, the request finishes, and the task keeps running. Or you open a connection and forget to close it when an error occurs mid-flight.&lt;/p&gt;
&lt;p&gt;Effect-TS promotes &lt;strong&gt;structured concurrency&lt;/strong&gt;, meaning the lifetime of child work is tied to the parent. When the parent finishes (successfully or with failure), the runtime can ensure children are cancelled or completed appropriately. This is exactly the discipline you want around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request-scoped background jobs&lt;/li&gt;
&lt;li&gt;streaming pipelines&lt;/li&gt;
&lt;li&gt;retries that shouldn’t outlive their caller&lt;/li&gt;
&lt;li&gt;timeouts and cancellation boundaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever debugged “why is the database pool exhausted only after errors spike,” you already know why this matters.&lt;/p&gt;
&lt;p&gt;A practical pattern: wrap any “start something and later stop it” logic in the structured scope of the effect. Don’t rely on “eventually” or “we’ll clean up in catch”—make cleanup part of the program structure.&lt;/p&gt;
&lt;p&gt;The result is not just fewer leaks. It’s confidence. You stop wondering whether a failure path accidentally left a task running.&lt;/p&gt;
&lt;h2 id="retry-caching-and-observabilitybuilt-into-the-workflow"&gt;Retry, caching, and observability—built into the workflow&lt;/h2&gt;
&lt;p&gt;Most teams treat retries, caching, and logging as bolt-ons. That leads to inconsistent behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one service retries timeouts, another retries everything&lt;/li&gt;
&lt;li&gt;one logs correlation IDs, another logs the entire error object (and may leak secrets)&lt;/li&gt;
&lt;li&gt;caching sometimes happens at the edge, sometimes inside the service, sometimes nowhere&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Effect-TS treats operational behavior as part of the effect composition. That’s the difference between “we hope the retry code is correct” and “retry policy is a deliberate component of the workflow.”&lt;/p&gt;
&lt;p&gt;Here’s how this typically plays out in real apps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Retry policies&lt;/strong&gt;: Retry transient failures (like network timeouts) with backoff, but stop on non-transient errors (like invalid input).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching&lt;/strong&gt;: Cache pure-ish reads (like “get config for tenant”) while still respecting invalidation boundaries you control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt;: Emit consistent logs/metrics/traces around the effect execution, including error classification.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can apply these policies at the edges of your domain workflows, not scattered across every low-level API wrapper. For example: put retry/circuit-break behavior around the external-provider call, not in every business function that happens to call it.&lt;/p&gt;
&lt;p&gt;If you want a simple rule: when you can describe the operational intent, you should encode it once—then reuse it everywhere by composing effects.&lt;/p&gt;
&lt;h2 id="the-api-is-big-but-each-part-earns-its-keep"&gt;The API is big, but each part earns its keep&lt;/h2&gt;
&lt;p&gt;Effect-TS can feel daunting at first. There are multiple modules, combinators, and concepts. But the “big API” issue is mostly an onboarding problem—because the primitives map to real production concerns.&lt;/p&gt;
&lt;p&gt;Here’s a practical mental model you can use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Effects are the unit of work&lt;/strong&gt; (typed failures included)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layers/capabilities provide dependencies&lt;/strong&gt; in a scoped way&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combinators transform and compose&lt;/strong&gt; workflows (mapping errors, sequencing steps, branching)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency primitives give you controlled parallelism&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime execution interprets the plan&lt;/strong&gt; and applies resource safety&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of trying to learn everything, pick one workflow type and implement it end-to-end:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write an effect that calls an external service and models its error types.&lt;/li&gt;
&lt;li&gt;Add a retry policy only for the error subset that is transient.&lt;/li&gt;
&lt;li&gt;Add timeout and cancellation behavior.&lt;/li&gt;
&lt;li&gt;Add logging/metrics around the effect execution.&lt;/li&gt;
&lt;li&gt;Provide dependencies via test doubles in unit tests.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That workflow touches the core value propositions immediately, and you’ll learn the rest naturally as you go.&lt;/p&gt;
&lt;h2 id="conclusion-effect-ts-is-a-practical-upgrade-to-how-you-handle-failure"&gt;Conclusion: Effect-TS is a practical upgrade to how you handle failure&lt;/h2&gt;
&lt;p&gt;Effect-TS isn’t “FP cosplay.” It’s a disciplined approach to the exact places where production systems hurt: error handling that degrades across boundaries, hidden dependencies, and concurrency that leaks. You don’t need a PhD to get value—you need typed contracts, scoped dependencies, and structured concurrency that behaves correctly under failure.&lt;/p&gt;
&lt;p&gt;If you’ve written too many catch blocks, cleaned up too many hanging tasks, or lost too many hours to “mystery errors,” Effect-TS is worth putting on your shortlist. Convert one critical path first. The confidence gain is immediate, and the payoff compounds fast.&lt;/p&gt;</content></item><item><title>Bun 1.0 Is Here and Node.js Should Be Nervous</title><link>https://decastro.work/blog/bun-1-0-here-nodejs-should-be-nervous/</link><pubDate>Sat, 09 Mar 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/bun-1-0-here-nodejs-should-be-nervous/</guid><description>&lt;p&gt;For a long time, Bun lived in the “nice benchmark” corner of the JavaScript universe. Fast startup, impressive throughput, and a promise that someday it would feel like a real platform. Bun 1.0 collapses that gap. It’s no longer just a faster way to run JavaScript—it’s a complete runtime with the tooling you actually need to ship production software. Node.js isn’t dead, but its “default by inertia” advantage just got more fragile.&lt;/p&gt;</description><content>&lt;p&gt;For a long time, Bun lived in the “nice benchmark” corner of the JavaScript universe. Fast startup, impressive throughput, and a promise that someday it would feel like a real platform. Bun 1.0 collapses that gap. It’s no longer just a faster way to run JavaScript—it’s a complete runtime with the tooling you actually need to ship production software. Node.js isn’t dead, but its “default by inertia” advantage just got more fragile.&lt;/p&gt;
&lt;h2 id="what-changed-with-bun-10-and-why-it-matters"&gt;What Changed With Bun 1.0 (And Why It Matters)&lt;/h2&gt;
&lt;p&gt;Bun 1.0 isn’t a cosmetic release. The big shift is that Bun now behaves like a complete application platform, not a “run the script” novelty. The runtime comes with its own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;package manager&lt;/strong&gt; (so you’re not forced back into npm workflows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;test runner&lt;/strong&gt; (so your CI and local developer loop can stay in one place)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bundler&lt;/strong&gt; (so you can build, ship, and optimize without bolt-on tooling)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, that means teams can stop treating Bun as a performance experiment and start treating it as a serious choice for new services, CLIs, and even web backends that want to stay lightweight.&lt;/p&gt;
&lt;p&gt;This is also where “benchmarks” stop being the whole story. A fast runtime is helpful, but developers buy &lt;em&gt;stability&lt;/em&gt;: predictable builds, familiar workflows, and tooling that doesn’t fight you. Bun 1.0 is trying to win on that axis.&lt;/p&gt;
&lt;h2 id="the-performance-story-isnt-just-a-gimmick-anymore"&gt;The Performance Story Isn’t Just a Gimmick Anymore&lt;/h2&gt;
&lt;p&gt;Let’s be honest: Node.js has been optimized for years, and it still runs a huge portion of the internet. That’s why performance claims matter only when they translate into real daily wins.&lt;/p&gt;
&lt;p&gt;Bun’s headline advantages are still hard to ignore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;startup times&lt;/strong&gt; that feel dramatically faster—especially noticeable in dev loops and short-lived serverless-style workloads&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;throughput wins&lt;/strong&gt; on many common server workloads where JavaScript execution and module loading dominate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical effect is less “look, numbers” and more “how does this change my workflow?” If your project spins up containers, restarts processes frequently during deployments, or runs many short tasks (think CI jobs, background workers, scheduled scripts), startup speed directly reduces waiting.&lt;/p&gt;
&lt;p&gt;Example: imagine a team with a microservice architecture where each service is restarted often during rollouts. If your runtime starts in a fraction of the time, you shorten the feedback loop for deployments and debugging. That compounds into fewer hours lost to “where did it hang this time?” and faster iteration.&lt;/p&gt;
&lt;p&gt;Is Bun always faster? No. Performance is workload-specific. But the key point is that Bun 1.0 is designed to be used end-to-end—so you don’t just get a faster “node app start,” you get a faster path from dependencies → tests → bundles → execution.&lt;/p&gt;
&lt;h2 id="nodejs-compatibility-strong-enough-becomes-a-real-strategy"&gt;Node.js Compatibility: “Strong Enough” Becomes a Real Strategy&lt;/h2&gt;
&lt;p&gt;The other big lever is compatibility. Bun’s npm compatibility story is strong enough that many Node.js projects can switch with minimal changes.&lt;/p&gt;
&lt;p&gt;For teams considering migration, this is the most important kind of “works on my machine” that actually matters: fewer rewrites, fewer dependency surprises, fewer weeks lost to edge-case debugging.&lt;/p&gt;
&lt;p&gt;A practical migration approach looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with the app’s dependency graph.&lt;/strong&gt; In many Node projects, the dependency set is the hardest part. Try swapping the runtime and see whether Bun can install dependencies and resolve imports without chaos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run the test suite immediately.&lt;/strong&gt; Bun’s test runner helps keep this close to the toolchain. If tests pass, you’ve already cleared the biggest hurdle: behavioral compatibility.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build with the bundler.&lt;/strong&gt; If your project depends on bundling for production artifacts, test that path early. A runtime switch that breaks packaging is not a victory—it’s just a delay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix the handful of friction points.&lt;/strong&gt; Expect some ecosystem edges: subtle differences in how specific Node APIs behave, or assumptions made by certain libraries. The difference now is that Bun gives you enough tooling to iterate quickly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Opinionated take: if your project is greenfield, you shouldn’t wait for a hypothetical perfect compatibility story. If you already have a mature Node codebase, don’t romanticize migration either—trial Bun in a branch, run tests, and measure end-to-end build/run time. The “minimal changes” promise is only real when validated against &lt;em&gt;your&lt;/em&gt; dependencies and &lt;em&gt;your&lt;/em&gt; deployment shape.&lt;/p&gt;
&lt;h2 id="tooling-is-the-battleground-now-package-test-bundle"&gt;Tooling Is the Battleground Now: Package, Test, Bundle&lt;/h2&gt;
&lt;p&gt;Node.js has enjoyed a decade of “best-of-breed” tooling, and that ecosystem is real. But Bun’s bet is that you shouldn’t have to stitch together four or five tools just to ship a modern application.&lt;/p&gt;
&lt;p&gt;Bun 1.0 gives you a cohesive toolchain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Package management&lt;/strong&gt; that’s designed to work naturally with the runtime.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;test runner&lt;/strong&gt; that fits into the developer workflow.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;bundler&lt;/strong&gt; that supports production packaging without forcing you to build around a separate stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s how this shows up for day-to-day development.&lt;/p&gt;
&lt;h3 id="example-a-lightweight-web-service-project"&gt;Example: a lightweight web service project&lt;/h3&gt;
&lt;p&gt;A typical team wants:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;install&lt;/code&gt; to be fast and predictable&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test&lt;/code&gt; to run quickly locally and reliably in CI&lt;/li&gt;
&lt;li&gt;&lt;code&gt;build&lt;/code&gt; to generate output that deploys cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Bun 1.0, you can keep those steps inside one “Bun-native” workflow. That reduces context switching: fewer config formats, fewer script adapters, fewer “why does this work with Webpack but not with our custom setup?” moments.&lt;/p&gt;
&lt;p&gt;And because the runtime is tied to the tooling, you’re less likely to hit performance mismatches where your bundler is fast but your runtime cold-start is slow, or where your install is quick but your test harness lags behind.&lt;/p&gt;
&lt;h2 id="when-bun-is-the-right-choice-and-when-it-isnt"&gt;When Bun Is the Right Choice (and When It Isn’t)&lt;/h2&gt;
&lt;p&gt;Bun’s momentum is not just about speed—it’s about the cost of adoption. The more your project values fast feedback and simple toolchains, the more Bun makes sense.&lt;/p&gt;
&lt;h3 id="bun-is-a-strong-fit-if"&gt;Bun is a strong fit if…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You’re building &lt;strong&gt;greenfield&lt;/strong&gt; services or tooling and you want a modern, cohesive dev workflow.&lt;/li&gt;
&lt;li&gt;You run workloads that &lt;strong&gt;benefit from startup speed&lt;/strong&gt; (many short-lived processes, dev servers, CI tasks, serverless-style execution).&lt;/li&gt;
&lt;li&gt;You want to standardize on a single runtime and toolchain to reduce friction.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nodejs-is-still-the-safer-default-if"&gt;Node.js is still the safer default if…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You have a &lt;strong&gt;highly specialized dependency stack&lt;/strong&gt; that you can’t comfortably validate in time.&lt;/li&gt;
&lt;li&gt;Your team has deep investment in &lt;strong&gt;Node-only tooling&lt;/strong&gt; and you’re not ready to revisit build/test pipelines.&lt;/li&gt;
&lt;li&gt;You rely on edge-case behavior from Node APIs where library support may lag.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the sharper truth: Node isn’t going anywhere soon. Even if Bun becomes the better choice for new projects, Node will remain the compatibility baseline for enterprise systems, long-lived apps, and libraries that assume Node semantics.&lt;/p&gt;
&lt;p&gt;But for greenfield projects, the “default to Node forever” posture should be questioned. Monthly, the case for choosing Bun gets easier: the tooling is already there, the ecosystem compatibility is strong enough to start, and the performance advantages translate into real workflow improvements.&lt;/p&gt;
&lt;h2 id="a-migration-playbook-that-doesnt-waste-time"&gt;A Migration Playbook That Doesn’t Waste Time&lt;/h2&gt;
&lt;p&gt;If you’re on Node and wondering whether to try Bun, treat it like a controlled experiment—not a rewrite.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clone the repo and switch runtime in a feature branch.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run &lt;code&gt;install&lt;/code&gt; and confirm dependencies resolve.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run unit tests and integration tests.&lt;/strong&gt; Fix the smallest set of issues first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run your build pipeline&lt;/strong&gt; and verify that production artifacts behave correctly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare cold-start and CI timing.&lt;/strong&gt; Don’t just benchmark “node vs bun” in isolation—measure your actual pipeline steps: install → test → build → run.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your project has a CI pipeline, make it the truth serum. It’s the most repeatable environment you’ll have, and it reveals whether Bun’s toolchain is truly frictionless under pressure.&lt;/p&gt;
&lt;p&gt;And keep one pragmatic rule: don’t migrate everything at once. Start with one service, one repo, or one subset of jobs. If Bun is going to improve your day-to-day, you’ll see it quickly—without risking the whole organization.&lt;/p&gt;
&lt;h2 id="conclusion-node-should-stay-confidentbut-it-shouldnt-stay-complacent"&gt;Conclusion: Node Should Stay Confident—But It Shouldn’t Stay Complacent&lt;/h2&gt;
&lt;p&gt;Bun 1.0 changes the conversation. The runtime isn’t “just benchmarks” anymore; it’s a complete toolkit with package management, testing, and bundling built in. The performance story remains compelling, and—crucially—it’s now wrapped in workflows that feel production-ready.&lt;/p&gt;
&lt;p&gt;Node.js will keep its place in the ecosystem for a long time. But for greenfield projects, the default choice is no longer automatic. Bun has narrowed the gap between “fast on my laptop” and “works as a platform,” and that’s exactly what makes teams pay attention.&lt;/p&gt;
&lt;p&gt;If you’re building new systems in the next quarter, you should at least evaluate Bun—because the longer you wait, the more likely you are to feel left behind by teams that already made the switch.&lt;/p&gt;</content></item><item><title>Tailwind v4 Alpha: CSS Variables, Lightning CSS, and a Ground-Up Rewrite</title><link>https://decastro.work/blog/tailwind-v4-alpha-css-variables-rewrite/</link><pubDate>Sun, 03 Mar 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/tailwind-v4-alpha-css-variables-rewrite/</guid><description>&lt;p&gt;Utility-first CSS didn’t just “catch on”—it rewired how teams ship interfaces. Tailwind’s winning formula was always pragmatic: generate exactly what you need, avoid bespoke CSS sprawl, and let design systems evolve without constant refactors. Tailwind v4 doesn’t chase more features. It goes after the engine room—swapping out the build pipeline, changing the theming model, and embracing native CSS capabilities so the framework can scale with performance instead of fighting it.&lt;/p&gt;</description><content>&lt;p&gt;Utility-first CSS didn’t just “catch on”—it rewired how teams ship interfaces. Tailwind’s winning formula was always pragmatic: generate exactly what you need, avoid bespoke CSS sprawl, and let design systems evolve without constant refactors. Tailwind v4 doesn’t chase more features. It goes after the engine room—swapping out the build pipeline, changing the theming model, and embracing native CSS capabilities so the framework can scale with performance instead of fighting it.&lt;/p&gt;
&lt;p&gt;What follows is the big picture: Tailwind v4 Alpha replaces the PostCSS-based workflow with Lightning CSS (a Rust-powered parser), moves theming to CSS custom properties, introduces cascade layers, and shifts configuration from JavaScript into CSS via &lt;code&gt;@theme&lt;/code&gt;. This isn’t a cosmetic update. It’s a fundamentals rewrite—arguably the most significant since Tailwind 1.0.&lt;/p&gt;
&lt;h2 id="the-real-shift-from-postcss-utilities-to-a-new-css-compiler-pipeline"&gt;The real shift: from PostCSS utilities to a new CSS compiler pipeline&lt;/h2&gt;
&lt;p&gt;Tailwind has historically relied on PostCSS as its transformation layer: parse your source, interpret classes, and generate CSS. That approach worked brilliantly for years—until it became the ceiling for performance, determinism, and output control.&lt;/p&gt;
&lt;p&gt;Tailwind v4 replaces that engine. The headline change is Lightning CSS: a fast CSS parser and transformer built in Rust. Practically, this matters in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Faster builds with the same mental model.&lt;/strong&gt; When your tooling spends less time parsing and rewriting, iteration loops tighten. Teams feel this immediately: fewer “wait for the build” moments, and more time designing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More predictable CSS transformations.&lt;/strong&gt; Utility frameworks generate lots of CSS under the hood. A more capable CSS pipeline helps ensure the framework’s output is easier to reason about when something goes wrong.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’ve ever debugged “why did this utility not apply?” or “why is this rule winning?” you know the pain isn’t the concept—it’s the compiled output. A modern compiler pipeline is partly about speed, but it’s also about producing CSS that behaves consistently with the platform.&lt;/p&gt;
&lt;h3 id="concrete-example-fewer-moving-parts-when-you-inspect-output"&gt;Concrete example: fewer moving parts when you inspect output&lt;/h3&gt;
&lt;p&gt;In a PostCSS-based setup, your compiled CSS can reflect multiple passes and plugin behaviors. With a more direct CSS pipeline, you’re more likely to see cleaner, more intentional output.&lt;/p&gt;
&lt;p&gt;Try it yourself: build the same small demo with Tailwind v3 and v4, then inspect the generated CSS. You’re looking for legibility, not just size. Even if the output “looks different,” the goal is that it’s easier to debug because the pipeline is less hand-wavy.&lt;/p&gt;
&lt;h2 id="the-theming-revolution-css-custom-properties-as-the-new-contract"&gt;The theming revolution: CSS custom properties as the new contract&lt;/h2&gt;
&lt;p&gt;Tailwind v3 pushed theme customization through config—centralizing design tokens in JavaScript objects and then generating styles. Tailwind v4 takes a bolder stance: &lt;strong&gt;use CSS custom properties as the primary theming mechanism&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a meaningful shift because custom properties are native, dynamic, and cascade-friendly. They let you change theme values at runtime, support theming per subtree, and integrate more naturally with component libraries and design tokens.&lt;/p&gt;
&lt;h3 id="what-changes-for-developers"&gt;What changes for developers?&lt;/h3&gt;
&lt;p&gt;Instead of thinking “my colors are defined in JS and compiled into CSS once,” you start thinking “my tokens are variables resolved by CSS at runtime.”&lt;/p&gt;
&lt;p&gt;Concretely, this enables patterns like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Theme switching without rebuilding CSS.&lt;/strong&gt; Flip variables at the root (or within a container) and the UI updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component-scoped theming.&lt;/strong&gt; Apply a different theme inside a modal or a card by overriding variables there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better interop with design systems.&lt;/strong&gt; If your design system already exports CSS variables, Tailwind can align with it instead of duplicating it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-practical-pattern-lightdark-using-variables"&gt;A practical pattern: light/dark using variables&lt;/h3&gt;
&lt;p&gt;Even without diving into exact syntax yet, the workflow becomes intuitive:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define base variables for colors (e.g., &lt;code&gt;--color-bg&lt;/code&gt;, &lt;code&gt;--color-fg&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Map Tailwind utilities to those variables.&lt;/li&gt;
&lt;li&gt;Override variables in &lt;code&gt;[data-theme=&amp;quot;dark&amp;quot;]&lt;/code&gt; (or the relevant selector).&lt;/li&gt;
&lt;li&gt;Use standard utilities (&lt;code&gt;bg-*&lt;/code&gt;, &lt;code&gt;text-*&lt;/code&gt;) as if nothing changed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This shifts theming from “generate two bundles” to “generate one stylesheet that can adapt.”&lt;/p&gt;
&lt;h2 id="native-cascade-layers-stop-playing-whack-a-mole-with-specificity"&gt;Native cascade layers: stop playing whack-a-mole with specificity&lt;/h2&gt;
&lt;p&gt;If Tailwind has taught teams anything, it’s that specificity wars are avoidable when you control the cascade. Tailwind v4 adds &lt;strong&gt;native cascade layers&lt;/strong&gt;. This is not an aesthetic upgrade; it’s a strategy upgrade.&lt;/p&gt;
&lt;p&gt;Cascade layers let you define priority between groups of rules (e.g., framework utilities vs. component overrides vs. app-specific styles). Instead of relying on fragile ordering or &lt;code&gt;!important&lt;/code&gt;, you can tell the browser how to resolve competing declarations.&lt;/p&gt;
&lt;h3 id="why-it-matters-for-utility-first-css"&gt;Why it matters for utility-first CSS&lt;/h3&gt;
&lt;p&gt;Utility frameworks generate many selectors. In a complex app, you inevitably layer on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;component styles,&lt;/li&gt;
&lt;li&gt;overrides,&lt;/li&gt;
&lt;li&gt;third-party CSS,&lt;/li&gt;
&lt;li&gt;and bespoke rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without a clear cascade strategy, you end up with “works until it doesn’t,” then sprinkle overrides to regain control.&lt;/p&gt;
&lt;p&gt;With cascade layers, you can keep utilities powerful while still guaranteeing that your app and components override them deterministically.&lt;/p&gt;
&lt;h3 id="practical-advice-treat-layers-like-contracts"&gt;Practical advice: treat layers like contracts&lt;/h3&gt;
&lt;p&gt;Use layers to formalize your stylesheet hierarchy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Framework layer&lt;/strong&gt;: generated utilities and base styles.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Component layer&lt;/strong&gt;: your design system components.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App layer&lt;/strong&gt;: routing pages, layout overrides, emergency fixes (hopefully rare).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The win is fewer surprises: when you adjust a component, you won’t accidentally override utilities because some rule order flipped after a dependency update.&lt;/p&gt;
&lt;h2 id="configuration-goes-css-first-theme-beats-everything-in-javascript"&gt;Configuration goes CSS-first: &lt;code&gt;@theme&lt;/code&gt; beats “everything in JavaScript”&lt;/h2&gt;
&lt;p&gt;The most jarring change—at least conceptually—is the move from JavaScript configuration to CSS-based directives using &lt;code&gt;@theme&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Tailwind historically lived in &lt;code&gt;tailwind.config.js&lt;/code&gt;. That file is powerful, but it’s also a separate language with its own runtime assumptions. When the framework is shifting toward CSS variables and native cascade features, it makes sense to move configuration into the same domain where the output ultimately lives: CSS.&lt;/p&gt;
&lt;h3 id="why-css-based-theming-is-a-win"&gt;Why CSS-based theming is a win&lt;/h3&gt;
&lt;p&gt;When your theme definition is written in CSS:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It aligns with the browser’s actual rendering model (variables + cascade).&lt;/li&gt;
&lt;li&gt;It reduces the impedance mismatch between “design tokens” and “style rules.”&lt;/li&gt;
&lt;li&gt;It encourages teams to version and review theme changes alongside CSS semantics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-concrete-example-token-definition-becomes-just-css"&gt;A concrete example: token definition becomes “just CSS”&lt;/h3&gt;
&lt;p&gt;Instead of tweaking a JS object and re-running a build pipeline to understand what changes, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;inspect the theme definitions directly,&lt;/li&gt;
&lt;li&gt;override variables in CSS,&lt;/li&gt;
&lt;li&gt;and rely on cascade layers for predictable resolution.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This also improves portability. If your org already manages design tokens in CSS (or can export them as CSS variables), Tailwind v4 can adopt them with less glue.&lt;/p&gt;
&lt;h2 id="lightning-fast-builds-and-smaller-css-what-to-expect-in-the-real-world"&gt;Lightning-fast builds and smaller CSS: what to expect in the real world&lt;/h2&gt;
&lt;p&gt;The most exciting part of the rewrite isn’t theoretical. It’s the expected outcomes: build times dropping dramatically, and CSS output shrinking.&lt;/p&gt;
&lt;p&gt;But numbers aren’t the story. The story is workflow quality.&lt;/p&gt;
&lt;h3 id="what-smaller-css-changes-day-to-day"&gt;What “smaller CSS” changes day-to-day&lt;/h3&gt;
&lt;p&gt;Less CSS means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster page loads (especially for CSS-heavy apps).&lt;/li&gt;
&lt;li&gt;Less time parsing styles on the client.&lt;/li&gt;
&lt;li&gt;Fewer edge cases where unused or conflicting rules create weird rendering.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if your app doesn’t ship a “huge” stylesheet, Tailwind projects tend to accumulate utility usage over time. A tighter output pipeline helps keep that growth in check.&lt;/p&gt;
&lt;h3 id="a-quick-checklist-to-validate-improvements"&gt;A quick checklist to validate improvements&lt;/h3&gt;
&lt;p&gt;If you’re evaluating Tailwind v4 Alpha, don’t trust vibes—measure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Track build time&lt;/strong&gt; for a representative project (same machine, same dev actions).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inspect compiled CSS size&lt;/strong&gt; (gzip/brotli too, not just raw bytes).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Render critical pages&lt;/strong&gt; and ensure no specificity regressions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test theming changes&lt;/strong&gt; (e.g., toggle dark mode) without rebuilds if your workflow supports it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You’re not just verifying “it’s faster.” You’re verifying it behaves like a production-ready toolchain should.&lt;/p&gt;
&lt;h2 id="the-migration-posture-dont-wait-for-perfectionadapt-the-mental-model"&gt;The migration posture: don’t wait for perfection—adapt the mental model&lt;/h2&gt;
&lt;p&gt;Tailwind v4 Alpha is a signal: the framework is investing in performance infrastructure and aligning more closely with native CSS capabilities. That means the “how” of Tailwind changes even when the “what” (utility-first classes) stays familiar.&lt;/p&gt;
&lt;p&gt;If you’re planning a migration, here’s the approach that reduces pain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with small surfaces.&lt;/strong&gt; Pick one feature area (e.g., a page with light/dark theming) and migrate it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lean into CSS variables.&lt;/strong&gt; Don’t fight the new theming model—use variables to align tokens across your stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adopt cascade layers early.&lt;/strong&gt; Even if you keep overrides minimal, layers prevent future specificity chaos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat &lt;code&gt;@theme&lt;/code&gt; as your source of truth.&lt;/strong&gt; Minimize dual definitions (JS + CSS) during transition.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And be honest: if your team is heavily invested in custom PostCSS or complex build-time plugins, you’ll need to validate those integrations carefully. A new engine is an opportunity, but it’s also a forcing function to simplify.&lt;/p&gt;
&lt;h2 id="conclusion-tailwind-is-still-utility-firstnow-its-platform-first"&gt;Conclusion: Tailwind is still utility-first—now it’s platform-first&lt;/h2&gt;
&lt;p&gt;Tailwind v4 Alpha reads like a mature framework choosing to optimize where it matters: the CSS pipeline, the cascade, and the theming model. Lightning CSS replaces the older PostCSS-driven engine with something built for speed and consistency. CSS custom properties move theming from “compiled config” to “runtime-native tokens.” Cascade layers bring deterministic override behavior to the chaos of real apps. And &lt;code&gt;@theme&lt;/code&gt; shifts configuration into CSS, where it naturally belongs.&lt;/p&gt;
&lt;p&gt;This isn’t Tailwind chasing novelty. It’s Tailwind making the case that utility-first doesn’t have to mean “build-system complexity forever.” If the rewrite holds up in production, Tailwind won’t just generate utilities—it’ll generate better CSS infrastructure for the long haul.&lt;/p&gt;</content></item><item><title>Ollama Made Running Local LLMs Embarrassingly Easy</title><link>https://decastro.work/blog/ollama-made-running-local-llms-easy/</link><pubDate>Mon, 26 Feb 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ollama-made-running-local-llms-easy/</guid><description>&lt;p&gt;For years, the promise of “run AI on your own machine” sounded less like a breakthrough and more like a dare. You wanted a local LLM—sure—but what you got was a weekend lost to Python dependencies, CUDA driver versions, quantization scripts, and mysteriously broken model files. Then Ollama showed up and turned the whole thing into a single, boring command. And that’s the point: local AI stopped being a research project and started being a tool.&lt;/p&gt;</description><content>&lt;p&gt;For years, the promise of “run AI on your own machine” sounded less like a breakthrough and more like a dare. You wanted a local LLM—sure—but what you got was a weekend lost to Python dependencies, CUDA driver versions, quantization scripts, and mysteriously broken model files. Then Ollama showed up and turned the whole thing into a single, boring command. And that’s the point: local AI stopped being a research project and started being a tool.&lt;/p&gt;
&lt;h2 id="the-old-way-local-llms-as-a-systems-engineering-hobby"&gt;The old way: local LLMs as a systems engineering hobby&lt;/h2&gt;
&lt;p&gt;If you ever tried to run a local model back when “local LLM” meant “self-managed inference stack,” you know the pattern. You’d start with something like “I’ll just install the runtime,” then hit a dependency mismatch. Next came GPU configuration—CUDA, drivers, compatible builds—followed by quantization steps to make the model fit in VRAM. Finally, you’d wrestle with how to format prompts correctly and ensure the model actually responds as expected.&lt;/p&gt;
&lt;p&gt;Even when it worked, the setup was fragile. Update one dependency and suddenly the environment breaks. Swap GPUs and you’re rebuilding from scratch. For most teams, that means local inference stays in the lab, not the product roadmap.&lt;/p&gt;
&lt;p&gt;This friction isn’t just annoying—it actively distorts decision-making. If “local AI” requires ML engineering labor, privacy and latency become luxuries. The result: people either send everything to hosted APIs or build nothing.&lt;/p&gt;
&lt;h2 id="the-new-way-one-binary-one-command-and-a-model"&gt;The new way: one binary, one command, and a model&lt;/h2&gt;
&lt;p&gt;Ollama’s core move is disarmingly simple: you install a single tool, then you download and run models through a straightforward CLI. The mental model is “like running a local dev server,” not “like deploying a distributed ML system.”&lt;/p&gt;
&lt;p&gt;Instead of stitching together a runtime, a model format pipeline, and a chat wrapper, you do something along these lines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install Ollama (your OS guides you through it)&lt;/li&gt;
&lt;li&gt;Run a model: &lt;code&gt;ollama run llama2&lt;/code&gt; (or &lt;code&gt;mistral&lt;/code&gt;, &lt;code&gt;mixtral&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Talk to it immediately in a chat-like interface&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That “one command” experience is the real product. It collapses the entire workflow—from acquisition to inference—into a tight feedback loop. You can test an idea in minutes, not days.&lt;/p&gt;
&lt;p&gt;And because Ollama exposes an OpenAI-compatible API, local models stop being a weird side channel and become a drop-in option for applications that already speak “Chat Completions.”&lt;/p&gt;
&lt;h2 id="running-models-like-you-mean-it-llama-mistral-mixtral"&gt;Running models like you mean it: Llama, Mistral, Mixtral&lt;/h2&gt;
&lt;p&gt;The point isn’t that every model is perfect for every task. It’s that you can choose based on use case without re-platforming your stack every time.&lt;/p&gt;
&lt;h3 id="llama-the-baseline-you-can-actually-iterate-on"&gt;Llama: the baseline you can actually iterate on&lt;/h3&gt;
&lt;p&gt;Llama-family models are a solid default when you want something pragmatic: summarization, drafting, Q&amp;amp;A, lightweight assistance. If you’re building features and need consistent behavior across iterations, Llama is often the model you start with because it’s easy to reason about and easy to refine.&lt;/p&gt;
&lt;p&gt;Practical example: imagine you’re building an internal “meeting notes” assistant. You can start with a Llama run locally, tune your prompts, and validate formatting and citation behavior (even if you don’t do retrieval yet). Once the UX is right, you can swap models and keep the same application interface thanks to the API compatibility.&lt;/p&gt;
&lt;h3 id="mistral-7b-conversational-speed-without-the-guilt"&gt;Mistral 7B: conversational speed without the guilt&lt;/h3&gt;
&lt;p&gt;Mistral 7B is popular for a reason: it’s compact enough that it can feel responsive on consumer hardware—especially on modern Mac systems. If your product needs a tight conversational loop (think: support agents, interactive tutoring, quick drafting), latency matters as much as raw quality.&lt;/p&gt;
&lt;p&gt;Practical advice: don’t just “see if it works.” Evaluate the full interaction. Measure how long it takes to start responding, how stable the output is across turns, and whether the model tends to ramble or follow instructions. Local inference makes these iterations fast; use that to quickly identify prompt patterns that work.&lt;/p&gt;
&lt;h3 id="mixtral-when-the-problem-isnt-just-fluency"&gt;Mixtral: when the problem isn’t just fluency&lt;/h3&gt;
&lt;p&gt;For more demanding reasoning and complex tasks, Mixtral-family models can be a better fit—especially when you want the model to handle multi-part instructions without dropping critical constraints. Think: planning workflows, transforming messy input into structured outputs, or performing “agent-like” steps where you want fewer silly mistakes.&lt;/p&gt;
&lt;p&gt;Practical example: suppose you’re building an incident response assistant. The model needs to take logs, extract key details, propose hypotheses, and output a structured checklist. You can prototype that pipeline locally, then compare model behavior: does it preserve constraints? Does it produce useful sections consistently? Mixtral’s strength for multi-step tasks often shows up immediately in these structured transforms.&lt;/p&gt;
&lt;h2 id="the-api-advantage-privacy-isnt-just-an-architecture-its-a-product-decision"&gt;The API advantage: privacy isn’t just an architecture, it’s a product decision&lt;/h2&gt;
&lt;p&gt;Local models are often discussed in terms of privacy, but the more interesting shift is product design. When your AI runs locally, you can build features that were previously blocked by data-sharing concerns—or at least you can reduce the cost of being careful.&lt;/p&gt;
&lt;p&gt;Because Ollama is OpenAI-compatible, you can treat the local model like another “backend” in your application. That means you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run local inference in sensitive environments (legal, healthcare-adjacent workflows, internal operations)&lt;/li&gt;
&lt;li&gt;Default to local processing for user-provided text by design&lt;/li&gt;
&lt;li&gt;Keep a hosted fallback for non-sensitive or heavy workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete architecture pattern many teams will adopt: &lt;strong&gt;local-first, cloud-later&lt;/strong&gt;. Start locally for interactive features (drafting, summarization, transformations). If the user explicitly opts in—or if tasks exceed what your machine can handle—route to a hosted model. The key is that your application code doesn’t have to be rewritten every time.&lt;/p&gt;
&lt;p&gt;This also changes procurement and policy conversations. Instead of arguing about whether “we can justify sending data to a third party,” you can sometimes say: “we can keep it on-device by default.” That’s not a philosophical win; it’s a practical one.&lt;/p&gt;
&lt;h2 id="practical-setup-make-local-llms-boring-in-a-good-way"&gt;Practical setup: make local LLMs boring (in a good way)&lt;/h2&gt;
&lt;p&gt;The biggest risk with any “easy” tool is that people stop thinking about the operational details. Ollama makes running models simple, but you still want your experience to be stable and repeatable.&lt;/p&gt;
&lt;p&gt;Here’s how to keep things sane:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick a small set of models and stick to them.&lt;/strong&gt;&lt;br&gt;
If your app supports five models with wildly different behavior, your prompts become a mess. Choose one “default” (often Llama or Mistral) and one “advanced” option (often Mixtral), then evaluate.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat prompts as code.&lt;/strong&gt;&lt;br&gt;
Store prompt templates in your repo. Version them. If you tweak instructions to reduce verbosity or improve formatting, do it intentionally—not ad hoc in a terminal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Plan for resource constraints.&lt;/strong&gt;&lt;br&gt;
Even if a model “runs,” your laptop has limits. If you notice lag, consider using a smaller model, adjusting generation settings (like max tokens), or running fewer parallel requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Build evaluation into the workflow.&lt;/strong&gt;&lt;br&gt;
Don’t rely on “it seems good.” Create a small set of test inputs—your real user scenarios—and compare model outputs before and after prompt changes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integrate via the API, not via a manual terminal session.&lt;/strong&gt;&lt;br&gt;
You’ll learn faster early on in the CLI, but production should go through the API so your app stays consistent.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal is simple: make the whole thing feel like an everyday dependency, not an experiment.&lt;/p&gt;
&lt;h2 id="where-this-goes-next-local-ai-becomes-the-baseline-not-the-exception"&gt;Where this goes next: local AI becomes the baseline, not the exception&lt;/h2&gt;
&lt;p&gt;Once you can run strong models locally with a one-command flow, the center of gravity shifts. Developers stop asking “can we run LLMs on our machine?” and start asking “which features should run locally by default?”&lt;/p&gt;
&lt;p&gt;Expect local-first patterns to show up everywhere: internal copilots, document transformation tools, interactive assistants for domain-specific workflows, privacy-sensitive chat experiences, and prototypes that never need to send raw user data off-device.&lt;/p&gt;
&lt;p&gt;The best part is that Ollama’s approach lowers the intimidation barrier for new teams. You don’t need to be a GPU-tuning wizard to start building. You need a product idea, a prompt strategy, and the willingness to test.&lt;/p&gt;
&lt;h2 id="conclusion-local-llms-are-no-longer-a-project"&gt;Conclusion: local LLMs are no longer a project&lt;/h2&gt;
&lt;p&gt;Ollama didn’t invent better models—it made using them dramatically easier. By collapsing setup, model downloads, and inference into a single tool—and keeping an OpenAI-compatible API—you can go from “I have an idea” to “it’s running on my laptop” faster than most teams can write the project charter.&lt;/p&gt;
&lt;p&gt;That’s why local AI feels different now. It’s not just more private; it’s more usable. And once it’s usable, it becomes a default choice.&lt;/p&gt;</content></item><item><title>The TypeScript Ecosystem Is Now Best-in-Class for Full-Stack Development</title><link>https://decastro.work/blog/typescript-ecosystem-best-in-class-full-stack/</link><pubDate>Wed, 14 Feb 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/typescript-ecosystem-best-in-class-full-stack/</guid><description>&lt;p&gt;Full-stack development used to feel like a series of translation layers: database types become JSON, JSON becomes DTOs, DTOs become UI models, and somewhere along the way, one tiny mismatch turns into a production bug. In 2024, TypeScript largely breaks that spell. The ecosystem around it now delivers something more ambitious than “types help”: it enables compile-time and runtime correctness that travels across your entire stack—without you having to weld everything together by hand.&lt;/p&gt;</description><content>&lt;p&gt;Full-stack development used to feel like a series of translation layers: database types become JSON, JSON becomes DTOs, DTOs become UI models, and somewhere along the way, one tiny mismatch turns into a production bug. In 2024, TypeScript largely breaks that spell. The ecosystem around it now delivers something more ambitious than “types help”: it enables compile-time and runtime correctness that travels across your entire stack—without you having to weld everything together by hand.&lt;/p&gt;
&lt;p&gt;This isn’t hype. It’s a quality threshold the tooling has finally crossed. When you combine modern TypeScript-friendly libraries like Drizzle, Zod, tRPC, and Tailwind, you get end-to-end feedback loops that are fast, practical, and—most importantly—boring in the best way. Fewer surprises. Clearer contracts. Better refactors. Let’s unpack why this stack has become my default for building full applications.&lt;/p&gt;
&lt;h2 id="typescripts-real-superpower-composable-guarantees"&gt;TypeScript’s real superpower: composable guarantees&lt;/h2&gt;
&lt;p&gt;TypeScript isn’t just a language feature; it’s an ecosystem magnet. The reason is simple: once you have a strong type system, people build libraries that “speak types” in a way the compiler can understand.&lt;/p&gt;
&lt;p&gt;But the real leap in full-stack quality comes from &lt;em&gt;composition&lt;/em&gt;. Individual libraries can be good. The difference now is that the best-in-class ones reinforce each other:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database layer&lt;/strong&gt; can expose types that flow upward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API layer&lt;/strong&gt; can enforce those same shapes at the boundaries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime layer&lt;/strong&gt; can validate external inputs safely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI layer&lt;/strong&gt; can guide you with editor intelligence instead of docs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a workflow where the compiler and your editor become reliable partners, not just documentation checkers.&lt;/p&gt;
&lt;p&gt;To put it in concrete terms: you should be able to change a field in your database schema and have your editor highlight every place that needs updating—&lt;em&gt;before&lt;/em&gt; you run the app. That’s the baseline expectation for “best-in-class” in 2024, and TypeScript tooling now gets you there more consistently than alternative ecosystems.&lt;/p&gt;
&lt;h2 id="drizzle--sql-type-inference-that-doesnt-feel-fragile"&gt;Drizzle + SQL: type inference that doesn’t feel fragile&lt;/h2&gt;
&lt;p&gt;If you’ve ever used an ORM that forces you to manually define shapes—then later discovers the database schema drifted anyway—you know the pain. Drizzle’s approach is compelling because it treats SQL as something you can reason about with types, not a black box.&lt;/p&gt;
&lt;p&gt;In practice, Drizzle lets your queries produce types you can trust. That means when you select columns, your returned objects have the inferred structure you expect, and you can build on top of it without constantly re-declaring interfaces.&lt;/p&gt;
&lt;p&gt;A common pattern looks like this conceptually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define your tables (or import schema).&lt;/li&gt;
&lt;li&gt;Write queries using those table definitions.&lt;/li&gt;
&lt;li&gt;Receive results typed by the query.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, you can reuse those result types to populate API outputs or UI models with minimal friction. The important part isn’t that you “have types.” It’s that the types remain connected to the underlying query logic.&lt;/p&gt;
&lt;p&gt;A practical advantage: when you refactor a query—rename a column, change a join, adjust nullability—TypeScript can force you to fix downstream code immediately. This is especially valuable when multiple developers are working on different layers. The compiler becomes the contract negotiator.&lt;/p&gt;
&lt;h2 id="zod-runtime-validation-that-derives-types-instead-of-duplicating-them"&gt;Zod: runtime validation that derives types instead of duplicating them&lt;/h2&gt;
&lt;p&gt;Compile-time types are great—until you accept untrusted input. Anything coming from the network, from forms, from cookies, or from external services must be validated at runtime. Zod is the ecosystem’s go-to because it bridges the gap cleanly: you write validation logic once, and you get type inference out of it.&lt;/p&gt;
&lt;p&gt;The key idea is that Zod schemas are not “parallel truth.” They’re a single source of validation, which TypeScript can use to infer static types.&lt;/p&gt;
&lt;p&gt;A typical workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define a Zod schema for input (e.g., a “CreateUser” payload).&lt;/li&gt;
&lt;li&gt;Use it to validate at runtime.&lt;/li&gt;
&lt;li&gt;Reuse the inferred TypeScript type wherever you need compile-time safety.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This prevents the worst failure mode in full-stack projects: developers adding a type definition to satisfy the compiler while the actual runtime validation allows a broader or different shape. Zod’s schema-first model keeps those concerns aligned.&lt;/p&gt;
&lt;p&gt;Even more important: Zod plays nicely with real-world constraints. You can validate strings, numbers, enums, nested objects, arrays, and custom refinements. You can enforce invariants like “password must match confirmation” or “email must be normalized,” then carry the derived types forward without extra ceremony.&lt;/p&gt;
&lt;h2 id="trpc-type-safe-apis-without-the-schema-tax"&gt;tRPC: type-safe APIs without the schema tax&lt;/h2&gt;
&lt;p&gt;APIs are where correctness goes to die—because that’s where contracts get duplicated. The usual approach is to describe an API with a schema (OpenAPI, JSON Schema, protobuf, etc.) and then generate types for clients. It works, but it creates friction and overhead.&lt;/p&gt;
&lt;p&gt;tRPC takes a different stance: you define procedures in TypeScript and let the type system propagate. Instead of maintaining a separate schema file, you get end-to-end typing between server and client.&lt;/p&gt;
&lt;p&gt;The practical win is velocity and refactor safety:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Change a procedure input type on the server.&lt;/li&gt;
&lt;li&gt;Your client code updates with editor help.&lt;/li&gt;
&lt;li&gt;You avoid “works on my machine” mismatches caused by stale generated artifacts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is especially potent when paired with Zod. If a tRPC procedure validates input with a Zod schema, you get both runtime safety and static types. That combination is the closest thing I’ve found to “the boundary is actually safe.”&lt;/p&gt;
&lt;p&gt;And it has a cultural effect too: developers don’t treat the API as a fragile string-based interface. They treat it as code that the compiler can reason about.&lt;/p&gt;
&lt;h2 id="tailwind-editor-level-feedback-that-makes-ui-correctness-feel-effortless"&gt;Tailwind: editor-level feedback that makes UI correctness feel effortless&lt;/h2&gt;
&lt;p&gt;For all the talk about backends, full-stack quality lives in the UI too. Tailwind’s “className intelligence” (via editor tooling) matters because it shortens the loop between intent and correctness.&lt;/p&gt;
&lt;p&gt;Instead of guessing which utility names are valid or relying on runtime CSS surprises, the editor can help you keep classes consistent. The payoff is subtle but real: fewer typos, fewer dead styles, less time lost to formatting and lookup.&lt;/p&gt;
&lt;p&gt;Tailwind also aligns with TypeScript’s general philosophy: prefer explicitness and tooling feedback over hidden conventions. When your UI class composition is safer and faster to edit, you can focus on behavior and state, not stylesheet debugging.&lt;/p&gt;
&lt;p&gt;In a TypeScript-first codebase, this matters because the UI isn’t an island—it’s the consumer of your typed API models. When your components build from well-typed props and well-typed data, the UI becomes harder to break.&lt;/p&gt;
&lt;h2 id="putting-it-together-an-end-to-end-type-journey-example"&gt;Putting it together: an end-to-end “type journey” example&lt;/h2&gt;
&lt;p&gt;Here’s what the best TypeScript full-stack experience feels like when it’s working:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Database query shape&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You define tables and write queries with Drizzle.&lt;/li&gt;
&lt;li&gt;The results are inferred and typed based on what you actually select.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API boundary&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You expose procedures with tRPC.&lt;/li&gt;
&lt;li&gt;Inputs are validated with Zod at runtime.&lt;/li&gt;
&lt;li&gt;Outputs remain typed end-to-end so clients know exactly what they’ll receive.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;UI consumption&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your frontend uses typed data directly.&lt;/li&gt;
&lt;li&gt;Components receive typed props inferred from the procedure outputs.&lt;/li&gt;
&lt;li&gt;Editor intelligence helps you map data to UI without guesswork.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refactor with confidence&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Suppose you rename a field from &lt;code&gt;displayName&lt;/code&gt; to &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The database query changes (Drizzle).&lt;/li&gt;
&lt;li&gt;The API procedure output changes (tRPC).&lt;/li&gt;
&lt;li&gt;The UI component usage breaks at compile time until you update it.&lt;/li&gt;
&lt;li&gt;You catch the entire mismatch before shipping.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is why I call it best-in-class. Not because TypeScript is magical, but because the ecosystem is now mature enough to make the “type journey” reliable across every boundary that typically leaks correctness.&lt;/p&gt;
&lt;h2 id="conclusion-the-ecosystem-has-turned-typescript-into-a-full-stack-advantage"&gt;Conclusion: the ecosystem has turned TypeScript into a full-stack advantage&lt;/h2&gt;
&lt;p&gt;The TypeScript ecosystem isn’t merely popular—it’s coherent. Drizzle provides typed data access. Zod enforces runtime correctness without duplicating types. tRPC keeps API contracts aligned with the code that implements them. Tailwind finishes the loop with editor-grade UI feedback.&lt;/p&gt;
&lt;p&gt;When you combine those pieces, you stop fighting your tools and start trusting them. And that’s the real quality threshold: not more types, but fewer broken contracts, faster iteration, and refactors that don’t feel scary. If you’re building full-stack apps in 2024, TypeScript isn’t just a choice—it’s the default architecture for developers who want correctness to be effortless.&lt;/p&gt;</content></item><item><title>The Hidden Cost of 'Free' AI APIs</title><link>https://decastro.work/blog/hidden-cost-free-ai-apis/</link><pubDate>Thu, 08 Feb 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/hidden-cost-free-ai-apis/</guid><description>&lt;p&gt;“Free” AI APIs are seductive: you get a quick endpoint, a slick demo, and the confidence to ship before you fully understand the bill. But the real cost of an AI integration rarely shows up in the README. It shows up in the fine print—rate limits that throttle production, data retention rules that change how your compliance story reads, model deprecations that break working prompts, and pricing tiers that jump the moment you succeed.&lt;/p&gt;</description><content>&lt;p&gt;“Free” AI APIs are seductive: you get a quick endpoint, a slick demo, and the confidence to ship before you fully understand the bill. But the real cost of an AI integration rarely shows up in the README. It shows up in the fine print—rate limits that throttle production, data retention rules that change how your compliance story reads, model deprecations that break working prompts, and pricing tiers that jump the moment you succeed.&lt;/p&gt;
&lt;p&gt;If you’re building with hosted LLMs, you need to treat your provider like a dependency—not a convenience. Here’s what the “free” tier actually hides, and how to design around it.&lt;/p&gt;
&lt;h2 id="rate-limits-your-apps-performance-ceiling-is-someone-elses-policy"&gt;Rate limits: your app’s performance ceiling is someone else’s policy&lt;/h2&gt;
&lt;p&gt;Most teams assume rate limits are an implementation detail. They’re not. Rate limits are a hard operational constraint that can cap your throughput, shape user experience, and force architectural compromises you can’t easily undo.&lt;/p&gt;
&lt;h3 id="what-it-looks-like-in-the-real-world"&gt;What it looks like in the real world&lt;/h3&gt;
&lt;p&gt;Imagine you build a customer support chatbot. In the first week, traffic is low, so you’re happily generating responses at will. Then marketing launches a campaign. Suddenly you hit your per-minute token or request limit, and the experience degrades: delays, retries, and occasional failures. Even if your code is correct, your product becomes hostage to the provider’s throttling rules.&lt;/p&gt;
&lt;p&gt;The most common mistake is assuming you can “just add retries.” Retries can turn a temporary slowdown into a cascading failure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your app queues requests internally.&lt;/li&gt;
&lt;li&gt;Users click “Try again.”&lt;/li&gt;
&lt;li&gt;Your retry loop multiplies load.&lt;/li&gt;
&lt;li&gt;You hit rate limits harder and burn through the free tier faster.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Treat rate limits like a design input, not an afterthought:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model your worst-case traffic.&lt;/strong&gt; Decide what happens when you receive 10× normal requests for 10 minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implement backpressure.&lt;/strong&gt; If you can’t serve immediately, degrade gracefully: shorter responses, fewer tool calls, or “we’re busy—please wait.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a queue with a deadline.&lt;/strong&gt; Time-box requests so you’re not endlessly waiting for the provider.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache aggressively where it’s safe.&lt;/strong&gt; If users ask the same question repeatedly (or the same instruction template is used), caching reduces both cost and rate pressure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point isn’t to “avoid” limits. It’s to prevent them from dictating your product’s behavior.&lt;/p&gt;
&lt;h2 id="data-retention-the-compliance-bill-you-didnt-budget-for"&gt;Data retention: the compliance bill you didn’t budget for&lt;/h2&gt;
&lt;p&gt;Free tiers love to advertise how fast you can “ship AI.” Few teams read the retention and logging policy with the seriousness they’d apply to analytics pipelines. But with AI APIs, retention is not abstract—it’s part of your risk model.&lt;/p&gt;
&lt;h3 id="the-hidden-question-where-does-my-users-text-go"&gt;The hidden question: “Where does my user’s text go?”&lt;/h3&gt;
&lt;p&gt;Depending on the provider and settings, your prompts and outputs may be retained for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;abuse monitoring&lt;/li&gt;
&lt;li&gt;service improvement&lt;/li&gt;
&lt;li&gt;training or fine-tuning (directly or indirectly)&lt;/li&gt;
&lt;li&gt;debugging and audit logs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you never request training, a provider may still keep data for defined periods. That matters for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;privacy obligations&lt;/strong&gt; (e.g., whether users reasonably expect their inputs to be stored)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;confidentiality&lt;/strong&gt; (customer data, internal docs, trade secrets)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;regulatory constraints&lt;/strong&gt; (industry-specific requirements, cross-border data handling)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice-1"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Before you write a single line of production code, answer these questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Is retention configurable?&lt;/strong&gt; Look for options like “no training” and “short retention,” and confirm defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do they use your prompts/outputs for model improvement?&lt;/strong&gt; The wording is everything—“may” and “aggregated” are not guarantees.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What’s the retention period?&lt;/strong&gt; Ask what happens to logs after the period ends.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can you delete data on request?&lt;/strong&gt; In practice, deletion can be slow or limited.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What about system prompts and tool results?&lt;/strong&gt; If you send proprietary context as part of an instruction, that context may be treated as data too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then adjust your architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimize what you send.&lt;/strong&gt; Don’t include entire documents when a retrieval snippet will do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redact sensitive fields.&lt;/strong&gt; Build a preprocessing step that masks PII or confidential identifiers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use deterministic routing where needed.&lt;/strong&gt; If you must meet strict rules, route sensitive flows through a provider configuration that supports your requirements, or through a self-hosted alternative.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t want your AI vendor to become the surprise owner of your users’ most sensitive text.&lt;/p&gt;
&lt;h2 id="model-deprecation-when-working-prompts-stop-working-overnight"&gt;Model deprecation: when “working” prompts stop working overnight&lt;/h2&gt;
&lt;p&gt;LLM providers don’t just iterate—they replace. And replacement can break systems that rely on stable behavior.&lt;/p&gt;
&lt;p&gt;A model deprecation means the provider will eventually retire a model version. That retirement can alter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;response formatting&lt;/li&gt;
&lt;li&gt;tool calling behavior&lt;/li&gt;
&lt;li&gt;reasoning style (often indirectly)&lt;/li&gt;
&lt;li&gt;refusal patterns&lt;/li&gt;
&lt;li&gt;latency and output length distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dangerous part is that the system may still “work” at a superficial level—until it doesn’t. For production, subtle changes can be catastrophic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JSON output becomes inconsistent.&lt;/li&gt;
&lt;li&gt;A classifier flips edge cases.&lt;/li&gt;
&lt;li&gt;Your extraction pipeline starts failing.&lt;/li&gt;
&lt;li&gt;“Same prompt” yields different behavior for the same user intent.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice-2"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Design for change as a core requirement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version explicitly.&lt;/strong&gt; If the API lets you pin a model, do it. Don’t drift silently across releases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add output contracts.&lt;/strong&gt; Enforce JSON schema validation and retry with a constrained instruction when formatting fails.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintain regression tests.&lt;/strong&gt; Build a small “golden set” of representative prompts and expected structured outputs. Run it on every model change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use canary routing.&lt;/strong&gt; Send a small percentage of production traffic to the new model and monitor outcomes before full rollout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log prompts responsibly.&lt;/strong&gt; You’ll need test evidence, but remember the retention discussion—log minimally and securely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best production teams treat model updates like software releases, not like background maintenance.&lt;/p&gt;
&lt;h2 id="pricing-tiers-and-surprise-egress-your-bill-scales-faster-than-your-team"&gt;Pricing tiers and surprise egress: your bill scales faster than your team&lt;/h2&gt;
&lt;p&gt;Pricing is where “free” turns into a lesson. Many providers structure costs so that early prototypes are cheap, then costs expand quickly with real usage. And usage isn’t just prompt tokens—sometimes it’s also output length, tool calls, retries, caching misses, and network egress.&lt;/p&gt;
&lt;h3 id="the-common-scaling-traps"&gt;The common scaling traps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long outputs.&lt;/strong&gt; A chat interface invites users to request verbose answers. Output tokens grow fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry loops.&lt;/strong&gt; When formatting fails or tools time out, retries add hidden cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No caching.&lt;/strong&gt; If every request is unique, you pay for everything every time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool chaining.&lt;/strong&gt; Each tool call may add additional model usage and orchestration overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Egress costs.&lt;/strong&gt; If your architecture sends data to and from multiple services (or regions), network costs can become meaningful.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yes—providers can change pricing. Even if they give notice, your production forecasting is only as good as your assumption that the assumptions stay stable.&lt;/p&gt;
&lt;h3 id="practical-advice-3"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Stop thinking about “the cost per request” and start thinking about “the cost per user outcome.”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Set token budgets per feature.&lt;/strong&gt; For example: “summary must be under 200 tokens.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrain the interface.&lt;/strong&gt; Encourage short, structured responses for workflows that don’t need verbosity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure end-to-end cost.&lt;/strong&gt; Include retries and tool calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build a cost guardrail.&lt;/strong&gt; If spending exceeds a daily budget, switch strategies (shorter responses, caching, fewer tool calls).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forecast with scenarios.&lt;/strong&gt; Model not just average usage, but spikes and worst-case retry behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you’re serious about reducing risk, consider portability: the cost of swapping providers is real, but the cost of being unable to swap is worse.&lt;/p&gt;
&lt;h2 id="vendor-lock-in-portability-isnt-a-luxury-its-an-exit-strategy"&gt;Vendor lock-in: portability isn’t a luxury, it’s an exit strategy&lt;/h2&gt;
&lt;p&gt;“Build fast with AI” is a valid prototype strategy. The trap is treating prototypes like architecture. When you tie critical product flows to one vendor’s API quirks—prompt formats, tool calling semantics, embeddings choices, evaluation tooling—you make switching expensive.&lt;/p&gt;
&lt;p&gt;Lock-in isn’t only about rewriting code. It’s also about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;operational knowledge embedded in team workflows&lt;/li&gt;
&lt;li&gt;evaluation datasets tuned to a specific model’s behavior&lt;/li&gt;
&lt;li&gt;prompt templates that rely on specific response patterns&lt;/li&gt;
&lt;li&gt;infrastructure around your provider’s streaming, logging, and rate limit mechanics&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice-4"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;You don’t need to chase full multi-provider complexity. You need an abstraction boundary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create a model gateway.&lt;/strong&gt; One internal interface for “generate,” “classify,” “extract,” etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Normalize inputs and outputs.&lt;/strong&gt; Convert responses into your own internal schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate orchestration from vendor calls.&lt;/strong&gt; Your app should decide &lt;em&gt;what&lt;/em&gt; to do; the gateway decides &lt;em&gt;how&lt;/em&gt; to call the vendor.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep prompt templates portable.&lt;/strong&gt; Avoid vendor-specific instruction patterns that won’t translate cleanly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document assumptions.&lt;/strong&gt; If your extraction depends on a particular formatting convention, write it down. That’s how you avoid rewriting blindly later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of portability as insurance: not because you expect to need it, but because you’ll regret it when you do.&lt;/p&gt;
&lt;h2 id="a-production-ready-checklist-for-free-api-optimism"&gt;A production-ready checklist for “free” API optimism&lt;/h2&gt;
&lt;p&gt;If you’re moving from prototype to production, treat these as non-negotiables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read rate limit and retry guidance&lt;/strong&gt; and implement backpressure with timeouts and caching.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify data retention and training settings&lt;/strong&gt; and minimize what you send; redact sensitive content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin model versions&lt;/strong&gt; and create regression tests with schema validation and canary rollout.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track end-to-end cost&lt;/strong&gt; (including retries and tool calls) with daily budget guardrails.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build a gateway abstraction&lt;/strong&gt; so swapping providers doesn’t require a rewrite of your product logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for deprecation events&lt;/strong&gt; as routine releases, not emergencies.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The uncomfortable truth: “free” is a marketing phase. Production is where incentives and constraints collide.&lt;/p&gt;
&lt;h2 id="conclusion-ship-fast-but-architect-for-the-day-the-fine-print-becomes-real"&gt;Conclusion: ship fast, but architect for the day the fine print becomes real&lt;/h2&gt;
&lt;p&gt;Free AI APIs are great for learning and for proving value. But if you let the prototype phase become your production architecture, the hidden costs will arrive all at once—rate limits that throttle growth, retention policies that complicate compliance, model deprecations that break behavior, and pricing changes that strain budgets.&lt;/p&gt;
&lt;p&gt;You don’t need to fear vendors. You need to design like reality is going to happen—because it will. Build your AI layer with contracts, guardrails, tests, and a clear exit strategy, and you’ll keep your momentum when the fine print finally matters.&lt;/p&gt;</content></item><item><title>RAG Is the Pattern That Makes LLMs Actually Useful for Enterprises</title><link>https://decastro.work/blog/rag-pattern-makes-llms-useful-enterprises/</link><pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rag-pattern-makes-llms-useful-enterprises/</guid><description>&lt;p&gt;Enterprise AI has a branding problem: it looks magical in a demo and disappoints in production. The dirty secret isn’t that LLMs are “bad”—it’s that vanilla LLMs are ungrounded. They’re fantastic at sounding right. They’re not automatically good at knowing &lt;em&gt;your&lt;/em&gt; policies, &lt;em&gt;your&lt;/em&gt; contracts, or &lt;em&gt;your&lt;/em&gt; product catalog. Retrieval-Augmented Generation (RAG) is the pattern that turns “cool text generation” into “reliable enterprise assistance.”&lt;/p&gt;
&lt;h2 id="the-enterprise-failure-mode-confident-answers-without-grounding"&gt;The enterprise failure mode: confident answers without grounding&lt;/h2&gt;
&lt;p&gt;Most companies don’t fail with LLMs because the model can’t write. They fail because the model doesn’t know what it’s supposed to be answering.&lt;/p&gt;</description><content>&lt;p&gt;Enterprise AI has a branding problem: it looks magical in a demo and disappoints in production. The dirty secret isn’t that LLMs are “bad”—it’s that vanilla LLMs are ungrounded. They’re fantastic at sounding right. They’re not automatically good at knowing &lt;em&gt;your&lt;/em&gt; policies, &lt;em&gt;your&lt;/em&gt; contracts, or &lt;em&gt;your&lt;/em&gt; product catalog. Retrieval-Augmented Generation (RAG) is the pattern that turns “cool text generation” into “reliable enterprise assistance.”&lt;/p&gt;
&lt;h2 id="the-enterprise-failure-mode-confident-answers-without-grounding"&gt;The enterprise failure mode: confident answers without grounding&lt;/h2&gt;
&lt;p&gt;Most companies don’t fail with LLMs because the model can’t write. They fail because the model doesn’t know what it’s supposed to be answering.&lt;/p&gt;
&lt;p&gt;If you ask a base LLM, “What’s our policy for vendor onboarding exceptions?” it will answer as if it were guessing based on generic patterns from the internet and its training. It may produce something coherent—perhaps even plausible—but it has no direct line to your actual policy documents. When you operationalize that output, the problems get expensive fast:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compliance risk:&lt;/strong&gt; An answer that quotes the wrong version of a policy is still an incorrect answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legal risk:&lt;/strong&gt; Summaries of contract clauses that “sound right” can be misleading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational friction:&lt;/strong&gt; Support teams lose trust and revert to search and spreadsheets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User harm:&lt;/strong&gt; Employees make decisions based on generated text that wasn’t grounded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core issue is not reasoning capacity. It’s &lt;strong&gt;knowledge access&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="the-rag-principle-retrieve-first-then-generate"&gt;The RAG principle: retrieve first, then generate&lt;/h2&gt;
&lt;p&gt;RAG bridges the gap by changing the workflow. Instead of letting the LLM freestyle from its internal priors, you ground it in enterprise-owned sources at query time.&lt;/p&gt;
&lt;p&gt;The pattern is simple to describe and easy to implement incorrectly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ingest your content&lt;/strong&gt; (docs, tickets, PDFs, wiki pages, database rows).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Split it into chunks&lt;/strong&gt; (small enough to retrieve, large enough to be meaningful).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embed the chunks&lt;/strong&gt; into vectors and store them in a &lt;strong&gt;vector database&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At question time&lt;/strong&gt;, embed the user query, retrieve the most relevant chunks, and feed those chunks to the LLM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate an answer&lt;/strong&gt; that is constrained by the retrieved context—ideally with citations.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice what’s happening: the LLM becomes an answer synthesizer, not a knowledge source. Retrieval does the “what we know,” and generation does the “how we explain it.”&lt;/p&gt;
&lt;p&gt;Practical takeaway: if you’re evaluating an LLM app, don’t ask “Can it respond?” Ask “Can it respond using the right evidence, every time?”&lt;/p&gt;
&lt;h2 id="rag-in-practice-an-employee-facing-qa-system-that-wont-lie"&gt;RAG in practice: an employee-facing Q&amp;amp;A system that won’t lie&lt;/h2&gt;
&lt;p&gt;Let’s make this concrete. Imagine a company HR team wants an internal assistant that answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“What’s the vacation policy for hourly contractors?”&lt;/li&gt;
&lt;li&gt;“Do we reimburse relocation expenses, and what receipts are required?”&lt;/li&gt;
&lt;li&gt;“How do I request an exception to the onboarding timeline?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A vanilla LLM will be vague at best. Worse, it will “complete” your question with generic HR norms. That might even pass a casual test—until someone asks a question that depends on a specific clause or an updated form.&lt;/p&gt;
&lt;p&gt;A RAG system, by contrast, retrieves the relevant policy sections and then generates a response anchored to them. The result can look like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Answer:&lt;/strong&gt; Hourly contractors are eligible for X days after Y months of service. Exceptions require manager approval and must include documentation of Z.&lt;br&gt;
&lt;strong&gt;Sources:&lt;/strong&gt; “Contractor Leave Policy v3 (Section 4.2)” and “Exception Request Guidelines (Updated 2025-01).”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even if the LLM still makes mistakes in phrasing, the system has a fighting chance to be correct because the model is working from the actual text.&lt;/p&gt;
&lt;p&gt;Operational detail that matters: you want retrieval to return chunks that preserve meaning. If you chunk a policy by arbitrary length, you might separate the eligibility statement from the definitions. That produces “technically plausible but wrong” answers. Good chunking—plus overlap and metadata—turns RAG from brittle to dependable.&lt;/p&gt;
&lt;h2 id="designing-the-pipeline-chunking-embeddings-and-retrieval-quality"&gt;Designing the pipeline: chunking, embeddings, and retrieval quality&lt;/h2&gt;
&lt;p&gt;RAG isn’t glamorous. It’s also not magic. Most of the real work is in the boring parts, because those parts determine whether the right context arrives in the prompt.&lt;/p&gt;
&lt;p&gt;Here are the practices that typically separate demos from production:&lt;/p&gt;
&lt;h3 id="chunking-that-preserves-intent"&gt;Chunking that preserves intent&lt;/h3&gt;
&lt;p&gt;Use chunk sizes that fit your content type and query patterns. For policies, chunk around headings or logical sections, not just token counts. Include overlap so definitions and referents aren’t stranded in different pieces.&lt;/p&gt;
&lt;h3 id="metadata-is-not-optional"&gt;Metadata is not optional&lt;/h3&gt;
&lt;p&gt;Store metadata with every chunk: document name, section path, version, effective date, department owner. This enables filtering (“only return current HR policies”) and better citations.&lt;/p&gt;
&lt;p&gt;For example, when someone asks about “the current vacation policy,” retrieval should favor documents marked as current. Without metadata filtering, you may pull last year’s version, and the LLM will happily summarize outdated rules.&lt;/p&gt;
&lt;h3 id="retrieval-settings-should-be-tuned-not-trusted-blindly"&gt;Retrieval settings should be tuned, not trusted blindly&lt;/h3&gt;
&lt;p&gt;Vector search is only as good as its configuration. Tune the number of retrieved chunks and consider hybrid retrieval (vector + keyword) when content has identifiers, product names, or legal terms that don’t embed cleanly.&lt;/p&gt;
&lt;p&gt;Also: monitor retrieval failure cases. If users repeatedly ask about the same subject and get irrelevant context, you likely need better chunk boundaries or additional indexing sources.&lt;/p&gt;
&lt;h3 id="prompting-that-encourages-grounded-answers"&gt;Prompting that encourages grounded answers&lt;/h3&gt;
&lt;p&gt;A common mistake is to dump retrieved text into the prompt and assume the LLM will behave. Write instructions that require the model to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;answer using the provided context,&lt;/li&gt;
&lt;li&gt;mention uncertainty when context is insufficient,&lt;/li&gt;
&lt;li&gt;and prefer quoting or citing relevant excerpts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your prompt never tells the model how to behave when evidence is missing, you’ll eventually see hallucinated citations or confident inventions.&lt;/p&gt;
&lt;h2 id="vector-databases-are-plumbingevaluation-is-the-product"&gt;Vector databases are plumbing—evaluation is the product&lt;/h2&gt;
&lt;p&gt;Enterprises love to buy infrastructure. They should instead buy evaluation.&lt;/p&gt;
&lt;p&gt;You can build a perfect RAG pipeline on paper and still fail in the real world because retrieval quality and answer correctness drift over time: docs change, formats vary, and teams add content in inconsistent ways.&lt;/p&gt;
&lt;p&gt;Treat evaluation as a first-class feature:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build a test set of real questions&lt;/strong&gt; from the people who will use the system—HR, finance, support, engineering. Include edge cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure retrieval accuracy&lt;/strong&gt; (did we fetch the right sections?) and &lt;strong&gt;answer correctness&lt;/strong&gt; (did the response match the fetched text?).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Track regressions&lt;/strong&gt; when you update chunking rules, embedding models, or document ingestion logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log what context was retrieved&lt;/strong&gt; for every answer so you can debug failures quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One practical approach: classify failures into categories—wrong doc, wrong section, insufficient context, and reasoning error after correct retrieval. Each category points to a different fix. Otherwise, you’ll chase phantom improvements and wonder why user trust doesn’t return.&lt;/p&gt;
&lt;p&gt;And yes, you should require citations (or at least evidence references) for high-stakes answers. RAG is the pattern that makes LLMs &lt;em&gt;useful&lt;/em&gt;, but it doesn’t remove the need for accountability.&lt;/p&gt;
&lt;h2 id="beyond-qa-rag-as-a-general-enterprise-grounding-layer"&gt;Beyond Q&amp;amp;A: RAG as a general enterprise “grounding” layer&lt;/h2&gt;
&lt;p&gt;RAG isn’t limited to chatbots. It’s a grounding layer you can apply wherever LLMs need to operate on your data without inventing it.&lt;/p&gt;
&lt;p&gt;Common expansions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Support assistants&lt;/strong&gt; that retrieve relevant troubleshooting steps and known issues from your ticket history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contract and policy analyzers&lt;/strong&gt; that pull clause text and then generate clause-by-clause explanations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internal knowledge copilots&lt;/strong&gt; that draft summaries of documents while citing where each claim came from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent workflows&lt;/strong&gt; that retrieve state and constraints before taking action (e.g., “what approvals are required for this request type?”).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key design idea stays the same: retrieval provides authoritative context; generation provides readability and structure.&lt;/p&gt;
&lt;p&gt;When you treat RAG as foundational infrastructure—not a one-off feature—you reduce the risk of turning “AI” into a glorified auto-completer.&lt;/p&gt;
&lt;h2 id="conclusion-stop-demoing-start-grounding"&gt;Conclusion: stop demoing, start grounding&lt;/h2&gt;
&lt;p&gt;Vanilla LLMs are impressive, but for enterprise use they’re fundamentally ungrounded. RAG is what makes them practically reliable by forcing answers to be generated from your actual documents and databases. It’s not glamorous, but it’s the difference between a chatbot that hallucinates company policies and one that can cite them.&lt;/p&gt;
&lt;p&gt;If you want LLMs to earn trust inside your organization, build RAG—and then invest just as hard in evaluation, metadata, and retrieval quality as you do in the model itself. That’s how you move from prototypes to production.&lt;/p&gt;</content></item><item><title>Neon and Supabase Are Making PostgreSQL Cool for Indie Hackers</title><link>https://decastro.work/blog/neon-supabase-postgresql-cool-indie-hackers/</link><pubDate>Sun, 21 Jan 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/neon-supabase-postgresql-cool-indie-hackers/</guid><description>&lt;p&gt;For years, “use PostgreSQL” was easy advice and hard reality. The database was brilliant—until you had to babysit migrations, scaling, replicas, backups, and connection limits while also building product. What’s changed is simple: Neon and Supabase have turned PostgreSQL from “the right choice” into “the obvious one” for indie hackers who want speed, branching, and guardrails without hiring a DBA.&lt;/p&gt;
&lt;h2 id="the-postgresql-problem-indie-hackers-actually-felt"&gt;The PostgreSQL problem indie hackers actually felt&lt;/h2&gt;
&lt;p&gt;PostgreSQL isn’t the issue. Operations are. Even the most capable solo developer eventually runs into the same friction points:&lt;/p&gt;</description><content>&lt;p&gt;For years, “use PostgreSQL” was easy advice and hard reality. The database was brilliant—until you had to babysit migrations, scaling, replicas, backups, and connection limits while also building product. What’s changed is simple: Neon and Supabase have turned PostgreSQL from “the right choice” into “the obvious one” for indie hackers who want speed, branching, and guardrails without hiring a DBA.&lt;/p&gt;
&lt;h2 id="the-postgresql-problem-indie-hackers-actually-felt"&gt;The PostgreSQL problem indie hackers actually felt&lt;/h2&gt;
&lt;p&gt;PostgreSQL isn’t the issue. Operations are. Even the most capable solo developer eventually runs into the same friction points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scaling pressure&lt;/strong&gt;: PostgreSQL can scale, but you still need to think about indexing, connection pooling, and when to move off a single box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrations with consequences&lt;/strong&gt;: Schema changes are required, but they’re also risky. One bad migration can break production, and “rollback” is never as clean as you hope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment sprawl&lt;/strong&gt;: You want a staging database, a preview database for each PR, and maybe a couple of test fixtures—then the cost and overhead hit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“Not just a database”&lt;/strong&gt;: Modern apps require auth, storage, file uploads, background jobs, and real-time updates. Building all of that correctly is what keeps projects small—until you realize you’ve reinvented half a platform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Firebase and PlanetScale filled part of that gap: they were easy to operate and packaged nicely. But they often moved developers away from SQL-first thinking, and the long-term cost was avoidable lock-in.&lt;/p&gt;
&lt;p&gt;The new wave flips the story: keep PostgreSQL, remove the operational drag, and add the platform features you’d otherwise bolt on yourself.&lt;/p&gt;
&lt;h2 id="neon-serverless-postgres-plus-schema-branching-git-for-your-data-model"&gt;Neon: serverless Postgres plus schema branching (git for your data model)&lt;/h2&gt;
&lt;p&gt;Neon’s big idea is to make Postgres feel elastic without forcing you to become a cloud infrastructure expert. Instead of treating your database like a fixed-capacity server, Neon offers &lt;strong&gt;serverless scaling&lt;/strong&gt; that’s designed to handle varying workloads.&lt;/p&gt;
&lt;p&gt;But the feature indie teams care about most is &lt;strong&gt;branching&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think about how you ship code today: you branch your app, test changes, and merge. For databases, the equivalent has historically been “migrate in place” (or maintain multiple manual environments). Neon’s branching brings a Git-like workflow to your schema and data access patterns, so you can test changes in isolation.&lt;/p&gt;
&lt;h3 id="a-practical-example-preview-environments-for-every-change"&gt;A practical example: preview environments for every change&lt;/h3&gt;
&lt;p&gt;Imagine you’re building a SaaS dashboard and you want a URL where teammates can review the UI &lt;strong&gt;and&lt;/strong&gt; the database schema changes behind it.&lt;/p&gt;
&lt;p&gt;With branching, you can create a database branch tied to a specific code change, run migrations, seed test data, and point your preview deployment at that branch. If the change passes, you merge forward; if it fails, you discard the branch. No emergency migration surgery on shared environments.&lt;/p&gt;
&lt;h3 id="why-this-matters"&gt;Why this matters&lt;/h3&gt;
&lt;p&gt;Branching isn’t just convenience—it’s risk management.&lt;/p&gt;
&lt;p&gt;When you can safely test migrations in a disposable environment, you’ll ship faster. You’ll also write migrations differently. Instead of “make it work on production,” you start writing migrations like you’re testing code: deterministic, reviewable, and rollback-friendly.&lt;/p&gt;
&lt;p&gt;Serverless scaling is the enabler; branching is the workflow upgrade.&lt;/p&gt;
&lt;h2 id="supabase-postgres-as-the-core-plus-auth-storage-edge-functions-and-real-time"&gt;Supabase: Postgres as the core, plus auth, storage, edge functions, and real-time&lt;/h2&gt;
&lt;p&gt;If Neon is the “make Postgres easy to operate” story, Supabase is the “make your app stack coherent” story.&lt;/p&gt;
&lt;p&gt;Supabase sits on Postgres, then adds the missing pieces that used to send indie hackers chasing a grab bag of services. The result is a platform that feels like Firebase in spirit—but keeps PostgreSQL at the center.&lt;/p&gt;
&lt;p&gt;Key components include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt;: sign-in flows you can configure quickly, plus a clean way to integrate user identity into your database-backed logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: a practical pipeline for files and assets with policies you can reason about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge functions&lt;/strong&gt;: server-side logic close to users, useful for webhook handling, background orchestration, or lightweight APIs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time&lt;/strong&gt;: the ability to react to database changes without inventing your own pub/sub layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-practical-example-events-from-the-database-without-extra-plumbing"&gt;A practical example: “events from the database” without extra plumbing&lt;/h3&gt;
&lt;p&gt;Suppose your app needs to update a dashboard instantly when a user’s job status changes. With Supabase’s real-time capabilities, you can subscribe to updates backed by Postgres changes.&lt;/p&gt;
&lt;p&gt;Instead of building a custom event stream (or managing another database for events), you keep your source of truth in Postgres and let the app react. It’s a tighter feedback loop: database changes produce product changes.&lt;/p&gt;
&lt;h3 id="edge-functions-as-the-glue"&gt;Edge functions as the glue&lt;/h3&gt;
&lt;p&gt;Postgres is your model; edge functions handle the “app logic around the edges.” For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a Stripe webhook handler that validates the event, writes entitlements into Postgres, and triggers any downstream updates;&lt;/li&gt;
&lt;li&gt;a function that processes an uploaded file, updates metadata in Postgres, and returns a clean response to the client.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You still get SQL where it counts—and you don’t have to build an entire backend platform yourself.&lt;/p&gt;
&lt;h2 id="the-killer-combo-neon-for-database-ergonomics-supabase-for-app-infrastructure"&gt;The killer combo: Neon for database ergonomics, Supabase for app infrastructure&lt;/h2&gt;
&lt;p&gt;Here’s the opinionated take: &lt;strong&gt;use the tools where they’re strongest&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neon brings excellent &lt;strong&gt;Postgres scaling and branching&lt;/strong&gt;—the operational freedom and safe schema experimentation that keeps shipping moving.&lt;/li&gt;
&lt;li&gt;Supabase brings the &lt;strong&gt;product layer&lt;/strong&gt;: auth, storage, edge functions, and real-time so you’re not stitching together five services to launch a credible MVP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Depending on your setup, you might choose one as your primary platform. But the larger point is that the PostgreSQL ecosystem has matured: you can keep your architecture SQL-first and still get the “batteries included” experience.&lt;/p&gt;
&lt;h3 id="a-realistic-indie-workflow"&gt;A realistic indie workflow&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Build features with Postgres-backed tables.&lt;/li&gt;
&lt;li&gt;Use branching to test migrations and seed data safely for preview deployments.&lt;/li&gt;
&lt;li&gt;Use Supabase for authentication and file handling (so your app logic doesn’t balloon).&lt;/li&gt;
&lt;li&gt;Use edge functions for webhook glue and lightweight APIs.&lt;/li&gt;
&lt;li&gt;Use real-time subscriptions for “it updates instantly” UX.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is how you compress the distance between idea and working product.&lt;/p&gt;
&lt;h2 id="generous-free-tiers-change-the-economics-of-shipping"&gt;Generous free tiers change the economics of shipping&lt;/h2&gt;
&lt;p&gt;Indie hackers don’t just need tools—they need room to experiment without learning the painful cost curve too early. Neon and Supabase both offer &lt;strong&gt;generous free tiers&lt;/strong&gt;, which matters for two reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can build and iterate without turning every test into a budgeting exercise.&lt;/li&gt;
&lt;li&gt;You can keep the architecture you want. When costs are predictable, you don’t feel forced into shortcuts like denormalizing everything prematurely or avoiding real-time features entirely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The end result is cultural as much as financial: PostgreSQL stops being “what you’ll use later when you have money for ops,” and becomes what you start with on day one.&lt;/p&gt;
&lt;h2 id="what-to-watch-out-for-so-you-dont-repeat-old-mistakes"&gt;What to watch out for (so you don’t repeat old mistakes)&lt;/h2&gt;
&lt;p&gt;Even with modern platforms, you can still shoot yourself in the foot. The goal isn’t magic—it’s fewer chores and better workflows. A few practical guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treat migrations like code&lt;/strong&gt;: review them, test them in a branch, and keep them deterministic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Index intentionally&lt;/strong&gt;: serverless and managed platforms don’t erase query design. If your dashboard needs fast filtering, add the right indexes early.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be mindful of connection patterns&lt;/strong&gt;: real-time and edge functions can increase concurrency. Use pooling strategies appropriate to your stack.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep security policies tight&lt;/strong&gt;: Supabase’s strengths include a strong approach to access control—use it deliberately rather than “open by default.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you do these things, the new Postgres workflow becomes less about reacting to production fires and more about shipping.&lt;/p&gt;
&lt;h2 id="conclusion-firebases-default-era-is-endingsql-first-is-back"&gt;Conclusion: Firebase’s default era is ending—SQL-first is back&lt;/h2&gt;
&lt;p&gt;Neon and Supabase don’t just make PostgreSQL easier; they restore a long-lost indie developer feeling: you can build a serious backend without turning every day into infrastructure work.&lt;/p&gt;
&lt;p&gt;Serverless scaling and schema branching mean safer, faster development. Auth, storage, edge functions, and real-time mean you don’t sacrifice product velocity to avoid complexity. And generous free tiers keep experimentation affordable.&lt;/p&gt;
&lt;p&gt;PostgreSQL was always the right core. Now it’s the right developer experience too—and that’s the shift that finally makes “use Postgres” the default choice again.&lt;/p&gt;</content></item><item><title>Every Developer Needs an AI Workflow in 2024 (Here's Mine)</title><link>https://decastro.work/blog/every-developer-needs-ai-workflow-2024/</link><pubDate>Tue, 09 Jan 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/every-developer-needs-ai-workflow-2024/</guid><description>&lt;p&gt;AI isn’t the future—it’s the present. But in 2024, “using AI” is less about fancy prompts and more about building a workflow you can trust. After a year of experimentation (and a few embarrassing bug reports), I’ve landed on a simple rule: treat AI like a junior developer. It can draft, it can suggest, it can learn your style. But it cannot be left alone with production.&lt;/p&gt;
&lt;p&gt;Here’s the workflow I actually use: Copilot for autocompletion, ChatGPT for architecture discussions and code review, and local models when sensitive data is involved. The goal isn’t to make AI do everything. The goal is to make you faster—without becoming the quality assurance department for hallucinations.&lt;/p&gt;</description><content>&lt;p&gt;AI isn’t the future—it’s the present. But in 2024, “using AI” is less about fancy prompts and more about building a workflow you can trust. After a year of experimentation (and a few embarrassing bug reports), I’ve landed on a simple rule: treat AI like a junior developer. It can draft, it can suggest, it can learn your style. But it cannot be left alone with production.&lt;/p&gt;
&lt;p&gt;Here’s the workflow I actually use: Copilot for autocompletion, ChatGPT for architecture discussions and code review, and local models when sensitive data is involved. The goal isn’t to make AI do everything. The goal is to make you faster—without becoming the quality assurance department for hallucinations.&lt;/p&gt;
&lt;h2 id="the-real-problem-isnt-ai-qualityits-workflow"&gt;The Real Problem Isn’t “AI Quality”—It’s Workflow&lt;/h2&gt;
&lt;p&gt;Most developers fall into one of two traps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Over-reliance:&lt;/strong&gt; They paste tasks into a chat, accept the first answer, and hope tests will catch the rest. Spoiler: tests often catch the symptom, not the root cause—and sometimes they don’t catch it at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stubborn refusal:&lt;/strong&gt; They insist they’ll “use AI later,” which usually means never. Meanwhile, everyone else is iterating on designs and prototypes twice as fast.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The middle path is what I recommend: &lt;strong&gt;aggressive adoption with ruthless verification&lt;/strong&gt;. That means AI outputs are treated as drafts and candidates, not decisions. You don’t “ask AI to code.” You run an iterative process where AI helps you get to a better final version under your control.&lt;/p&gt;
&lt;p&gt;Think of it like pair programming—with the caveat that your pair is brilliant at first drafts and terrible at knowing when it’s wrong.&lt;/p&gt;
&lt;h2 id="my-stack-copilot-chatgpt-and-local-modelsused-on-purpose"&gt;My Stack: Copilot, ChatGPT, and Local Models—Used on Purpose&lt;/h2&gt;
&lt;p&gt;My workflow is intentionally boring in the best way. Each tool has a job, and I don’t blur the boundaries.&lt;/p&gt;
&lt;h3 id="copilot-for-autocompletion-speed-with-guardrails"&gt;Copilot for autocompletion (speed with guardrails)&lt;/h3&gt;
&lt;p&gt;Copilot is my default for day-to-day coding: filling in obvious boilerplate, suggesting method implementations, and accelerating refactors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I use it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I let Copilot write the &lt;em&gt;first draft&lt;/em&gt; of a function.&lt;/li&gt;
&lt;li&gt;I then immediately review for correctness, edge cases, and style alignment.&lt;/li&gt;
&lt;li&gt;I keep prompts minimal. Autocomplete doesn’t need a novel; it needs context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; I’m adding a new endpoint handler in a TypeScript service. Copilot can generate request parsing, validation scaffolding, and response shape mapping. I then verify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;status codes and error formats&lt;/li&gt;
&lt;li&gt;input validation rules&lt;/li&gt;
&lt;li&gt;logging and tracing hooks&lt;/li&gt;
&lt;li&gt;and whether the function respects existing conventions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Copilot’s suggestion conflicts with established patterns, I overwrite it. That’s not “fighting AI.” That’s maintaining consistency.&lt;/p&gt;
&lt;h3 id="chatgpt-for-architecture-discussions-and-code-review-thinking-and-critique"&gt;ChatGPT for architecture discussions and code review (thinking and critique)&lt;/h3&gt;
&lt;p&gt;ChatGPT is where I slow down. I use it to pressure-test design decisions, identify missing pieces, and review diffs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How I use it:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For architecture: I ask for tradeoffs, failure modes, and “what would you do differently?”&lt;/li&gt;
&lt;li&gt;For code review: I paste targeted snippets or PR-ready diffs and ask for specific categories of feedback.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Before committing to a caching strategy, I’ll ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Given these constraints (latency, data freshness, invalidation pattern), what architecture would you recommend and why?”&lt;/li&gt;
&lt;li&gt;“What are the top three ways this can fail in production?”&lt;/li&gt;
&lt;li&gt;“What would you measure first to verify it’s working?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ChatGPT is best when you narrow the question. A vague request yields generic answers. A precise request yields actionable critique.&lt;/p&gt;
&lt;h3 id="local-models-for-sensitive-data-control-the-blast-radius"&gt;Local models for sensitive data (control the blast radius)&lt;/h3&gt;
&lt;p&gt;When the work involves proprietary data, customer context, or anything you wouldn’t want floating through third-party services, I switch to local models.&lt;/p&gt;
&lt;p&gt;This isn’t about paranoia. It’s about risk management and operational hygiene. Local inference keeps sensitive content inside your boundary, where you can version, log, and control it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maintain a separate “local prompt template” for security-sensitive tasks.&lt;/li&gt;
&lt;li&gt;Don’t reuse prompts that accidentally reference sensitive payloads.&lt;/li&gt;
&lt;li&gt;Use local models for summarization, classification, and constrained extraction when possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key isn’t making local models perfect—it’s ensuring you’re not using a chat tool as a data pipeline.&lt;/p&gt;
&lt;h2 id="the-junior-developer-mindset-drafts-not-decisions"&gt;The Junior-Developer Mindset: Drafts, Not Decisions&lt;/h2&gt;
&lt;p&gt;Here’s the mindset that changed everything for me: &lt;strong&gt;AI is useful like a junior developer, not like a system.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A junior can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;implement the happy path quickly&lt;/li&gt;
&lt;li&gt;draft a method with reasonable structure&lt;/li&gt;
&lt;li&gt;propose an API shape you can refine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A junior also can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;misunderstand requirements&lt;/li&gt;
&lt;li&gt;ignore non-happy paths&lt;/li&gt;
&lt;li&gt;miss subtle concurrency or security issues&lt;/li&gt;
&lt;li&gt;output code that compiles but is wrong&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I use a simple workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Get a draft fast&lt;/strong&gt; (Copilot or ChatGPT).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make it specific&lt;/strong&gt; (add constraints, test expectations, formatting rules).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify aggressively&lt;/strong&gt; (tests, linters, type checks, manual reasoning).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lock in changes only after proof.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you follow that loop, AI becomes a lever instead of a liability.&lt;/p&gt;
&lt;h2 id="a-practical-workflow-you-can-copy-tomorrow"&gt;A Practical Workflow You Can Copy Tomorrow&lt;/h2&gt;
&lt;p&gt;Let’s make this concrete. This is how I run AI-assisted development end-to-end.&lt;/p&gt;
&lt;h3 id="step-1-start-with-the-spec-you-actually-have"&gt;Step 1: Start with the spec you actually have&lt;/h3&gt;
&lt;p&gt;Before calling any model, write a short “working spec” for the task:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What should happen?&lt;/li&gt;
&lt;li&gt;What must not happen?&lt;/li&gt;
&lt;li&gt;Inputs/outputs (including edge cases)&lt;/li&gt;
&lt;li&gt;Existing constraints (libraries, patterns, performance)&lt;/li&gt;
&lt;li&gt;Acceptance checks (tests, metrics, formatting)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the difference between AI helping and AI guessing.&lt;/p&gt;
&lt;h3 id="step-2-use-copilot-for-the-scaffold"&gt;Step 2: Use Copilot for the scaffold&lt;/h3&gt;
&lt;p&gt;Ask for the structure, not the truth. Let Copilot generate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;function skeletons&lt;/li&gt;
&lt;li&gt;data transformations&lt;/li&gt;
&lt;li&gt;interface wiring&lt;/li&gt;
&lt;li&gt;repetitive boilerplate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then immediately do a “human pass”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;check for off-by-one logic&lt;/li&gt;
&lt;li&gt;validate error handling&lt;/li&gt;
&lt;li&gt;confirm proper async/await usage&lt;/li&gt;
&lt;li&gt;ensure you’re not silently swallowing exceptions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-3-use-chatgpt-for-review-categories"&gt;Step 3: Use ChatGPT for review categories&lt;/h3&gt;
&lt;p&gt;When I want critique, I don’t ask “is this good?” I ask targeted questions. Example prompt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Review this code for: correctness, security issues, performance pitfalls, and maintainability. Call out any lines that are likely wrong.”&lt;/li&gt;
&lt;li&gt;“Suggest test cases I’m missing, including concurrency and failure modes.”&lt;/li&gt;
&lt;li&gt;“If you were refactoring this for clarity, what would you change first?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This turns ChatGPT into a reviewer with an agenda—much closer to how you actually want help.&lt;/p&gt;
&lt;h3 id="step-4-demand-verification-not-confidence"&gt;Step 4: Demand verification, not confidence&lt;/h3&gt;
&lt;p&gt;Every AI-assisted change triggers verification:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unit tests for logic&lt;/li&gt;
&lt;li&gt;integration tests for behavior&lt;/li&gt;
&lt;li&gt;type checks and linting for consistency&lt;/li&gt;
&lt;li&gt;code review from me, with attention to edge cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If AI suggests something that isn’t reflected in the tests, it’s a hypothesis—not a conclusion.&lt;/p&gt;
&lt;h3 id="step-5-escalate-to-local-models-for-sensitive-work"&gt;Step 5: Escalate to local models for sensitive work&lt;/h3&gt;
&lt;p&gt;If content crosses a line (customer identifiers, production payloads, confidential algorithms), don’t improvise. Switch tools and use locally hosted models for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;transformation of sensitive data into safe summaries&lt;/li&gt;
&lt;li&gt;extraction of schema info&lt;/li&gt;
&lt;li&gt;generation of non-sensitive scaffolding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep the sensitive text out of your normal chat stack. Make “tool choice” part of your workflow, not an afterthought.&lt;/p&gt;
&lt;h2 id="what-ive-learned-the-hard-way-so-you-dont-have-to"&gt;What I’ve Learned the Hard Way (So You Don’t Have To)&lt;/h2&gt;
&lt;p&gt;A few lessons I wish someone had hammered into me early:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don’t trust “it compiles”&lt;/strong&gt;. AI can produce syntactically correct code that violates your business logic. Tests and reasoning matter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t give AI your whole brain&lt;/strong&gt;. The more context you paste, the more likely it is to mirror misunderstandings. Provide a crisp spec and a narrow target.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer iterative refinement over one-shot answers.&lt;/strong&gt; Generate a draft, critique it, then regenerate with constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch for consistency drift.&lt;/strong&gt; Copilot can introduce style deviations and subtle API mismatches. Treat your existing codebase as the source of truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat architecture advice as a set of options, not a blueprint.&lt;/strong&gt; ChatGPT is excellent at presenting tradeoffs. It’s not omniscient about your constraints. Your constraints are the real input.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most importantly: AI adoption should feel like hiring a junior who’s fast but needs supervision. If you’re “letting AI drive,” you’re not supervising—you’re outsourcing.&lt;/p&gt;
&lt;h2 id="conclusion-build-a-workflow-you-can-trust"&gt;Conclusion: Build a Workflow You Can Trust&lt;/h2&gt;
&lt;p&gt;AI in 2024 isn’t a magic upgrade. It’s a new collaborator, and like any collaborator, it needs boundaries. My approach—Copilot for autocompletion, ChatGPT for architecture and review, and local models for sensitive data—works because it’s structured. The drafts are fast. The verification is ruthless. The final responsibility stays with you.&lt;/p&gt;
&lt;p&gt;If you want one takeaway: &lt;strong&gt;adopt AI aggressively, but verify with discipline.&lt;/strong&gt; That’s how you get speed without chaos—and how AI becomes a tool you rely on instead of a problem you inherit.&lt;/p&gt;</content></item><item><title>Devin Was a Wake-Up Call, Not a Replacement</title><link>https://decastro.work/blog/devin-wake-up-call-not-replacement/</link><pubDate>Wed, 03 Jan 2024 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/devin-wake-up-call-not-replacement/</guid><description>&lt;p&gt;The viral demo wasn’t a prophecy. It was a stress test—of our expectations, our workflows, and our ability to tell “cool” from “real.” When Cognition’s Devin showed an AI agent that can plan, code, debug, and deploy with minimal human steering, the internet instantly split into two tribes: “developers are dead” and “it’s vaporware.” Both reactions missed the point.&lt;/p&gt;
&lt;p&gt;The more interesting truth is simpler: agentic coding is real for narrow problems, and “autonomous software engineering” for messy, ambiguous work is still a long way off. Devin wasn’t a replacement. It was a wake-up call—about which parts of software work will get automated first, and what you’ll need to protect (and sharpen) to stay valuable.&lt;/p&gt;</description><content>&lt;p&gt;The viral demo wasn’t a prophecy. It was a stress test—of our expectations, our workflows, and our ability to tell “cool” from “real.” When Cognition’s Devin showed an AI agent that can plan, code, debug, and deploy with minimal human steering, the internet instantly split into two tribes: “developers are dead” and “it’s vaporware.” Both reactions missed the point.&lt;/p&gt;
&lt;p&gt;The more interesting truth is simpler: agentic coding is real for narrow problems, and “autonomous software engineering” for messy, ambiguous work is still a long way off. Devin wasn’t a replacement. It was a wake-up call—about which parts of software work will get automated first, and what you’ll need to protect (and sharpen) to stay valuable.&lt;/p&gt;
&lt;h2 id="what-the-devin-demo-provedand-what-it-didnt"&gt;What the Devin Demo Proved—and What It Didn’t&lt;/h2&gt;
&lt;p&gt;A great demo is a mirror: it reflects what the team can do today and what the audience wants to believe tomorrow. Devin’s viral moments highlighted capabilities that many developers can recognize immediately—iterating on code, running tests, searching for failing conditions, and producing working changes.&lt;/p&gt;
&lt;p&gt;But demos also hide complexity. They typically compress the messy parts of engineering into something that can be evaluated quickly: a defined repository, a clear task statement, a constrained environment, and success criteria you can check with a green checkmark. In other words, the demo proved feasibility under conditions that are close to “well-specified engineering.”&lt;/p&gt;
&lt;p&gt;What it didn’t prove is the hardest part of software: navigating ambiguity. Real requirements aren’t just a string prompt; they’re a moving target shaped by stakeholders, trade-offs, legacy constraints, regulatory considerations, incident history, performance budgets, and “we didn’t think about that edge case until production.” A demo can simulate those pressures, but it can’t fully recreate them at the same time the way a long-lived system does.&lt;/p&gt;
&lt;p&gt;So yes: agentic coding is possible. No: that doesn’t mean it’s ready to replace the craft of building software in the world as it actually exists.&lt;/p&gt;
&lt;h2 id="agentic-coding-is-realif-you-keep-the-box-small"&gt;Agentic Coding Is Real—If You Keep the Box Small&lt;/h2&gt;
&lt;p&gt;Here’s the pattern that matters: the best use cases for autonomous agents are narrow, bounded, and testable. Think of tasks where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The inputs and outputs are reasonably clear&lt;/li&gt;
&lt;li&gt;There’s a reliable harness to verify correctness (tests, linters, build steps)&lt;/li&gt;
&lt;li&gt;The environment is constrained enough that the agent won’t need endless clarifying conversations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example: “Fix the failing unit tests in this repo.” If you can provide a reproduction (or a failing CI run) and the goal is to get from red to green, an agent can do real work—often faster than a human can context-switch into that codebase.&lt;/p&gt;
&lt;p&gt;Another example: “Migrate a set of endpoints from one auth mechanism to another.” If the transformation is consistent, the agent can apply a structured refactor, update call sites, and run the test suite until it passes.&lt;/p&gt;
&lt;p&gt;Even better: these tasks don’t just benefit from automation—they benefit from feedback. Agents are at their strongest when every attempt produces signals: compilation output, stack traces, test failures, formatting errors, type checker complaints. That’s the loop software engineers already use instinctively; the difference is that agents can run that loop at machine speed without needing a coffee break.&lt;/p&gt;
&lt;p&gt;Your takeaway: Devin-style agents are most useful when you can turn “engineering” into “iteration with verification.” When you can’t, the agent stalls—or worse, produces plausible but wrong outcomes.&lt;/p&gt;
&lt;h2 id="why-developers-are-dead-is-a-bad-forecast"&gt;Why “Developers Are Dead” Is a Bad Forecast&lt;/h2&gt;
&lt;p&gt;The “developers are dead” argument tends to rely on a single mistaken assumption: that software development is mostly about writing code. It isn’t. Code is the artifact; engineering is the process that produces the right artifact under uncertainty.&lt;/p&gt;
&lt;p&gt;Most real developer value sits in places demos don’t capture well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Translating fuzzy business intent into technical requirements&lt;/li&gt;
&lt;li&gt;Designing systems that can survive change (not just tests that can go green)&lt;/li&gt;
&lt;li&gt;Making trade-offs (latency vs. cost, simplicity vs. scalability, correctness vs. developer velocity)&lt;/li&gt;
&lt;li&gt;Coordinating across teams and constraints&lt;/li&gt;
&lt;li&gt;Knowing which assumptions are safe—and which will explode in production&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If an agent can churn through a bugfix task, great. But if you hand it an initiative like “reduce churn by improving onboarding,” it doesn’t get to “just code.” It needs experimentation plans, analytics instrumentation, privacy considerations, rollout strategy, and a model of user behavior. That’s not a narrow repo problem; it’s a product-and-platform problem.&lt;/p&gt;
&lt;p&gt;Even within a single codebase, ambiguous work dominates: “This feels slow” becomes a performance investigation; “We need better reliability” becomes an incident review, an SLO definition, and a series of mitigations across components. There’s often no clean evaluation harness that a tool can automatically label as “correct.”&lt;/p&gt;
&lt;p&gt;The strongest claim you can make about agentic agents is not that they replace developers—it’s that they will increasingly handle the repetitive, structured parts of development. The rest remains stubbornly human.&lt;/p&gt;
&lt;h2 id="why-its-vaporware-misses-the-real-shift"&gt;Why “It’s Vaporware” Misses the Real Shift&lt;/h2&gt;
&lt;p&gt;At the other extreme, calling Devin vaporware ignores something equally important: the industry doesn’t need full autonomy to change how work gets done. Even partial autonomy can reshape workflows.&lt;/p&gt;
&lt;p&gt;Once developers start using agents for the boring-but-critical steps—scaffolding, refactoring, generating boilerplate, rewriting repetitive code, hunting down test failures—teams will reorganize around that reality. Code review becomes more about evaluating intent and design constraints. Planning becomes more iterative as agents propose multiple implementations. Debugging shifts from “find the bug” toward “validate the agent’s hypothesis.”&lt;/p&gt;
&lt;p&gt;In practice, you don’t need an agent to reliably ship production on its own to get value. You need it to reduce time spent on “mechanical work” while improving developer throughput and shortening feedback loops.&lt;/p&gt;
&lt;p&gt;The “vaporware” framing also underestimates the cultural impact of a demo like this. Even if the agent is limited, it establishes a new mental model: software engineering can be approached as a sequence of actions toward verifiable outcomes. That model will spread, and tools will improve. You can’t dismiss the future because it isn’t here in full.&lt;/p&gt;
&lt;p&gt;The reality is probabilistic, not binary. Some tasks will be automated sooner than others. That’s how technology adoption usually works—messily, unevenly, but relentlessly.&lt;/p&gt;
&lt;h2 id="where-agents-actually-fit-in-todays-engineering-workflow"&gt;Where Agents Actually Fit in Today’s Engineering Workflow&lt;/h2&gt;
&lt;p&gt;So what should you do with this wake-up call? Treat agents as junior collaborators with obsessive follow-through—useful for speed, dangerous for unchecked assumptions.&lt;/p&gt;
&lt;p&gt;Here’s a practical framing you can adopt immediately:&lt;/p&gt;
&lt;h3 id="1-give-agents-problems-with-receipts"&gt;1) Give agents problems with receipts&lt;/h3&gt;
&lt;p&gt;Prefer tasks that come with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a failing test log or CI run&lt;/li&gt;
&lt;li&gt;a reproduction script&lt;/li&gt;
&lt;li&gt;a target interface or schema&lt;/li&gt;
&lt;li&gt;a clear definition of “done”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your prompt is “Improve this module,” you’ll get vague outputs. If it’s “Fix the failing integration tests in &lt;code&gt;payments-service&lt;/code&gt; by updating the request retry logic to meet these constraints,” you’ll get something actionable.&lt;/p&gt;
&lt;h3 id="2-make-evaluation-non-negotiable"&gt;2) Make evaluation non-negotiable&lt;/h3&gt;
&lt;p&gt;Don’t treat generated code as a conclusion—treat it as a hypothesis. Require:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;test runs&lt;/li&gt;
&lt;li&gt;lint/type checks&lt;/li&gt;
&lt;li&gt;code review with a checklist&lt;/li&gt;
&lt;li&gt;targeted regression testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words: keep the machine in the loop, but keep humans responsible for outcomes.&lt;/p&gt;
&lt;h3 id="3-use-agents-for-breadth-humans-for-judgment"&gt;3) Use agents for breadth, humans for judgment&lt;/h3&gt;
&lt;p&gt;Agents are great at exploring options quickly: “Try three ways to refactor this function.” Humans should decide which path aligns with architecture, maintainability, and product goals.&lt;/p&gt;
&lt;h3 id="4-guard-against-plausible-but-wrong"&gt;4) Guard against “plausible but wrong”&lt;/h3&gt;
&lt;p&gt;When ambiguity is high, agent outputs can look coherent while missing the real requirement. A reliable pattern is to force explicit alignment steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask the agent to restate requirements&lt;/li&gt;
&lt;li&gt;Ask it to list assumptions&lt;/li&gt;
&lt;li&gt;Have it cite evidence from the repo or error logs&lt;/li&gt;
&lt;li&gt;Require a minimal plan before execution&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-build-your-own-agent-playbook"&gt;5) Build your own “agent playbook”&lt;/h3&gt;
&lt;p&gt;Your best investment isn’t chasing the newest model—it’s codifying how you work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;standard repo structure conventions&lt;/li&gt;
&lt;li&gt;test harness expectations&lt;/li&gt;
&lt;li&gt;command sequences for local reproduction&lt;/li&gt;
&lt;li&gt;templates for prompts and review questions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The teams that win won’t be the ones who “use Devin.” They’ll be the ones who turn agent workflows into repeatable operating systems.&lt;/p&gt;
&lt;h2 id="become-indispensable-by-moving-up-the-value-chain"&gt;Become Indispensable by Moving Up the Value Chain&lt;/h2&gt;
&lt;p&gt;If you want the sharpest interpretation of Devin’s lesson, it’s this: AI won’t eliminate software developers—it will compress the role of “coder” into something closer to “orchestrator.”&lt;/p&gt;
&lt;p&gt;The boring parts will be handled more often:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;writing boilerplate&lt;/li&gt;
&lt;li&gt;refactoring mechanical code&lt;/li&gt;
&lt;li&gt;chasing failing tests&lt;/li&gt;
&lt;li&gt;generating migrations and repetitive updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your leverage will come from the interesting parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;system design under constraints&lt;/li&gt;
&lt;li&gt;requirement shaping and stakeholder alignment&lt;/li&gt;
&lt;li&gt;security, correctness, and operational responsibility&lt;/li&gt;
&lt;li&gt;debugging the hard failures that aren’t reducible to a clean test harness&lt;/li&gt;
&lt;li&gt;making decisions when the data is incomplete&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re a developer, your competitive advantage is not that you can type faster. It’s that you can understand the problem deeply enough to know what “correct” even means—and you can own the consequences when it’s wrong.&lt;/p&gt;
&lt;p&gt;That’s why the wake-up call isn’t fear. It’s momentum. Learn how to delegate structured work to agents, and spend your time where agents struggle: ambiguity, trade-offs, and judgment.&lt;/p&gt;
&lt;h2 id="conclusion-the-future-isnt-autonomy-everywhereits-automation-where-it-can-earn-trust"&gt;Conclusion: The Future Isn’t “Autonomy Everywhere”—It’s “Automation Where It Can Earn Trust”&lt;/h2&gt;
&lt;p&gt;Devin didn’t prove that software engineering is dead. It proved that agentic coding can work when the task is bounded, verifiable, and guided by feedback loops. It also showed the uncomfortable gap between demos and reality: real engineering is messy, ambiguous, and accountable.&lt;/p&gt;
&lt;p&gt;So don’t bet your career on competing with the machine at typing. Bet it on becoming the person who knows how to define the right problem, validate the right outcome, and keep systems reliable when the world refuses to be neatly specified. That’s how you turn a viral demo into an advantage—without losing your craft.&lt;/p&gt;</content></item><item><title>Predictions for 2024: What I'm Betting On</title><link>https://decastro.work/blog/predictions-2024-betting-on/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/predictions-2024-betting-on/</guid><description>&lt;p&gt;Enterprise AI is entering its “quietly inevitable” phase: fewer demos, more plumbing. In 2024, I expect the conversation to shift from “can we build it?” to “can we run it reliably, cheaply, and safely?” My bets are specific—and yes, I’m willing to be wrong in public. So bookmark this and come roast me in December.&lt;/p&gt;
&lt;h2 id="1-rag-becomes-the-default-enterprise-architecture"&gt;1) RAG becomes the default enterprise architecture&lt;/h2&gt;
&lt;p&gt;For years, the standard pattern has been: prompt the model, hope for the best, and then write a blog post about “responsible AI” after the first angry ticket arrives. In 2024, retrieval-augmented generation (RAG) won’t just become popular—it will become the default architecture for most enterprise AI apps.&lt;/p&gt;</description><content>&lt;p&gt;Enterprise AI is entering its “quietly inevitable” phase: fewer demos, more plumbing. In 2024, I expect the conversation to shift from “can we build it?” to “can we run it reliably, cheaply, and safely?” My bets are specific—and yes, I’m willing to be wrong in public. So bookmark this and come roast me in December.&lt;/p&gt;
&lt;h2 id="1-rag-becomes-the-default-enterprise-architecture"&gt;1) RAG becomes the default enterprise architecture&lt;/h2&gt;
&lt;p&gt;For years, the standard pattern has been: prompt the model, hope for the best, and then write a blog post about “responsible AI” after the first angry ticket arrives. In 2024, retrieval-augmented generation (RAG) won’t just become popular—it will become the default architecture for most enterprise AI apps.&lt;/p&gt;
&lt;p&gt;Why? Because RAG solves the problem enterprises actually have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Knowledge is messy and constantly changing.&lt;/strong&gt; A model that’s trained once can’t keep up with policy updates, product documentation revisions, or support playbooks that change weekly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ground truth matters.&lt;/strong&gt; When a bot confidently hallucinates a shipping policy, it’s not a cute failure mode—it’s a process failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditing becomes possible.&lt;/strong&gt; Retrieval gives you citations, traceability, and a path to debugging.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-standard-will-look-like-in-practice"&gt;What “standard” will look like in practice&lt;/h3&gt;
&lt;p&gt;If you’re building enterprise AI in 2024, I’d bet you end up with a pipeline that looks something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ingest&lt;/strong&gt; documents (PDFs, wikis, tickets, manuals).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunk&lt;/strong&gt; them with intent (not arbitrary token counts).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embed&lt;/strong&gt; chunks into a vector index.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrieve&lt;/strong&gt; relevant passages at query time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generate&lt;/strong&gt; using the retrieved context plus a strict prompt template.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate&lt;/strong&gt; with a feedback loop: which answers were wrong, and why?&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="the-practical-advice-ill-stand-by"&gt;The practical advice I’ll stand by&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don’t start with “top-k and pray.”&lt;/strong&gt; Your chunking strategy will matter more than your model selection early on. If your chunks cut across table rows or policy sections, retrieval quality will collapse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat RAG as a product, not a feature.&lt;/strong&gt; Set up monitoring for retrieval hits, response usefulness, and failure categories (missing info vs. misread policy vs. wrong reasoning).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for “no good context.”&lt;/strong&gt; The best RAG systems know when to say: “I couldn’t find a relevant policy section.” Build that behavior deliberately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RAG won’t replace all generative workflows, but it will become the baseline for enterprise “knowledge tasks” like support automation, internal assistants, compliance Q&amp;amp;A, and draft-with-sources experiences.&lt;/p&gt;
&lt;h2 id="2-open-source-llms-close-the-gapand-companies-stop-paying-the-premium"&gt;2) Open-source LLMs close the gap—and companies stop paying the premium&lt;/h2&gt;
&lt;p&gt;My second bet is blunt: open-source LLMs from Meta and Mistral (and others) will get close enough to top-tier proprietary models that “best model wins” stops being the default procurement strategy.&lt;/p&gt;
&lt;p&gt;I’m not claiming identity with any specific frontier system. I am saying that, in real product conditions—latency constraints, tool use, RAG grounding, and evaluation-driven prompting—open models will reach a point where many teams can deliver comparable user outcomes at a fraction of the cost.&lt;/p&gt;
&lt;h3 id="90-quality--10-cost-as-a-working-assumption"&gt;“90% quality / 10% cost” as a working assumption&lt;/h3&gt;
&lt;p&gt;In a spreadsheet fantasy world, “quality” is a single number. In production, quality is a mix of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;factual grounding (RAG helps a lot here),&lt;/li&gt;
&lt;li&gt;instruction-following reliability,&lt;/li&gt;
&lt;li&gt;safety behavior,&lt;/li&gt;
&lt;li&gt;latency under load,&lt;/li&gt;
&lt;li&gt;and the cost of failure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So my prediction is really about &lt;strong&gt;ROI&lt;/strong&gt;: open models will become the rational choice for most use cases once teams invest in evaluation, prompt scaffolding, and retrieval.&lt;/p&gt;
&lt;h3 id="how-teams-will-make-this-switch-without-rewriting-everything"&gt;How teams will make this switch without rewriting everything&lt;/h3&gt;
&lt;p&gt;Expect a pattern shift:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’ll standardize on &lt;strong&gt;model-agnostic prompting&lt;/strong&gt; and evaluation harnesses.&lt;/li&gt;
&lt;li&gt;You’ll build &lt;strong&gt;adapters&lt;/strong&gt; for tool calling and formatting rather than bespoke prompts for one vendor.&lt;/li&gt;
&lt;li&gt;You’ll use RAG to reduce dependency on raw parametric knowledge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re already using RAG, switching models is less scary than it sounds. Retrieval provides stable context; the model becomes a rendering engine plus reasoning layer. That’s exactly the job open models are increasingly good at.&lt;/p&gt;
&lt;h2 id="3-htmx-goes-from-cool-demo-to-production-default"&gt;3) htmx goes from “cool demo” to production default&lt;/h2&gt;
&lt;p&gt;htmx has been the darling of devs who want fewer abstractions and more actual HTTP. In 2024, I expect it to cross the chasm from curiosity to adoption in real products—especially where the UI is mostly forms, lists, and incremental updates.&lt;/p&gt;
&lt;p&gt;The reason is simple: many web apps don’t need client-side complexity. They need &lt;strong&gt;fast feedback loops&lt;/strong&gt;, &lt;strong&gt;clear server-side logic&lt;/strong&gt;, and &lt;strong&gt;predictable state&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="what-production-adoption-looks-like"&gt;What production adoption looks like&lt;/h3&gt;
&lt;p&gt;You’ll see htmx in places like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;internal dashboards and admin tools,&lt;/li&gt;
&lt;li&gt;CRUD-heavy workflows (ticketing, approvals, inventory),&lt;/li&gt;
&lt;li&gt;content management UIs,&lt;/li&gt;
&lt;li&gt;“modal + partial update” patterns without building a SPA.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, instead of turning your app into a JavaScript framework project, you can ship a server-rendered page where actions like “approve request” trigger a targeted request and swap only the relevant fragment.&lt;/p&gt;
&lt;h3 id="the-strategic-advantage"&gt;The strategic advantage&lt;/h3&gt;
&lt;p&gt;The hidden win of htmx is operational:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fewer front-end build steps.&lt;/li&gt;
&lt;li&gt;Less state management in the browser.&lt;/li&gt;
&lt;li&gt;More leverage from your existing backend stack.&lt;/li&gt;
&lt;li&gt;Easier auditing and debugging because the server remains the source of truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever debugged a production SPA issue that turned out to be a state desync, you already understand why this matters.&lt;/p&gt;
&lt;h2 id="4-bun-10-landsand-starts-taking-bites-out-of-nodes-dominance"&gt;4) Bun 1.0 lands—and starts taking bites out of Node’s dominance&lt;/h2&gt;
&lt;p&gt;My next bet: Bun will hit 1.0 and start stealing market share from Node.js. Not because Node is bad—it’s because the ecosystem is now big enough that performance matters again.&lt;/p&gt;
&lt;p&gt;Bun’s pitch is compelling: faster startup, a tighter toolchain, and an integrated developer experience. Even if you don’t care about raw speed, teams care about iteration time, CI time, and “time-to-first-response” for developers.&lt;/p&gt;
&lt;h3 id="where-bun-wins-immediately"&gt;Where Bun wins immediately&lt;/h3&gt;
&lt;p&gt;I expect adoption first in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;greenfield services that don’t depend on esoteric Node internals,&lt;/li&gt;
&lt;li&gt;teams that run lots of short-lived jobs and scripts,&lt;/li&gt;
&lt;li&gt;environments where dev experience is a priority,&lt;/li&gt;
&lt;li&gt;workloads where startup and memory behavior are visible costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-to-do-if-youre-skeptical"&gt;What to do if you’re skeptical&lt;/h3&gt;
&lt;p&gt;Be pragmatic. Don’t rewrite your whole org because a new runtime looks shiny. Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prototype a single service,&lt;/li&gt;
&lt;li&gt;run your tests under Bun,&lt;/li&gt;
&lt;li&gt;compare build + deploy + runtime metrics in your real environment,&lt;/li&gt;
&lt;li&gt;and only then standardize.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best “framework wars” aren’t decided by hype—they’re decided by whether the new option makes your team faster without making production harder.&lt;/p&gt;
&lt;h2 id="5-framework-wars-get-interestingbecause-the-real-battleground-is-feedback-loops"&gt;5) Framework wars get interesting—because the real battleground is feedback loops&lt;/h2&gt;
&lt;p&gt;Framework wars usually get framed like religion: “React vs. Vue,” “Next vs. Remix,” “server vs. client.” 2024’s twist is that the battleground is shifting from logos to &lt;strong&gt;iteration velocity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Three forces make this real:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AI-assisted development&lt;/strong&gt; reduces the cost of wiring up prototypes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production requirements&lt;/strong&gt; (security, latency, observability) still don’t go away.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modern architectures&lt;/strong&gt; increasingly separate the “UI shell” from the “capabilities backend” (RAG, tools, workflows).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the winning frameworks will be the ones that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;make it easy to ship incremental changes,&lt;/li&gt;
&lt;li&gt;integrate cleanly with backend services,&lt;/li&gt;
&lt;li&gt;and don’t punish you with complexity when you need to debug.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also where htmx’s rise fits neatly: when you’re building AI-assisted workflows, you want UI patterns that respond quickly and predictably to backend decisions.&lt;/p&gt;
&lt;h2 id="6-at-least-one-major-ai-disaster-resets-expectations"&gt;6) At least one major AI “disaster” resets expectations&lt;/h2&gt;
&lt;p&gt;Here’s the one bet I both expect and dread: in 2024, at least one major company will have a public AI disaster—bad enough that it temporarily resets how people talk about AI.&lt;/p&gt;
&lt;p&gt;Will it be a data leak? A runaway automation incident? A high-profile reliability failure? I’m not predicting the exact cause. I’m predicting the &lt;em&gt;pattern&lt;/em&gt;: impressive capabilities collide with real-world messiness, and the result becomes a teachable moment for everyone.&lt;/p&gt;
&lt;h3 id="what-that-means-for-your-roadmap"&gt;What that means for your roadmap&lt;/h3&gt;
&lt;p&gt;If you’re building with AI, the industry-level “reset” translates into three practical actions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prioritize guardrails over optimism.&lt;/strong&gt; Add safe defaults, refusal logic, and permission checks where relevant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invest in evaluation.&lt;/strong&gt; You can’t “prompt” your way out of bad edge cases forever.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for failure.&lt;/strong&gt; A system that degrades gracefully beats one that fails loudly every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Disasters are bad for users—but they’re also how the market learns. Expect 2024 to reward teams that treat AI as an engineering discipline, not a novelty.&lt;/p&gt;
&lt;h2 id="conclusion-the-year-of-boring-reliability"&gt;Conclusion: The year of boring reliability&lt;/h2&gt;
&lt;p&gt;My 2024 thesis is that AI stops being a spectacle and becomes infrastructure. RAG becomes standard because enterprises need grounding. Open-source models close the practical gap because evaluation and retrieval make “good enough” feel excellent. htmx and Bun make developers faster because they reduce ceremony. And when disaster strikes, it will remind everyone that reliability, observability, and guardrails aren’t optional.&lt;/p&gt;
&lt;p&gt;In other words: bookmark this—and be ready to update your priors.&lt;/p&gt;</content></item><item><title>Turbopack, Rspack, and the Rust-ification of JavaScript Tooling</title><link>https://decastro.work/blog/turbopack-rspack-rustification-javascript-tooling/</link><pubDate>Mon, 11 Dec 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/turbopack-rspack-rustification-javascript-tooling/</guid><description>&lt;p&gt;JavaScript doesn’t need to be “fixed.” It needs to be &lt;em&gt;handled&lt;/em&gt;. And for the last couple of years, the people building the JavaScript developer experience have been quietly making a bet: if you can’t make JS faster at runtime, make the tools that process it faster. The newest wave of that bet isn’t just performance—it’s implementation. Multiple core parts of the ecosystem are being rewritten in Rust, and the pattern is now too consistent to ignore.&lt;/p&gt;</description><content>&lt;p&gt;JavaScript doesn’t need to be “fixed.” It needs to be &lt;em&gt;handled&lt;/em&gt;. And for the last couple of years, the people building the JavaScript developer experience have been quietly making a bet: if you can’t make JS faster at runtime, make the tools that process it faster. The newest wave of that bet isn’t just performance—it’s implementation. Multiple core parts of the ecosystem are being rewritten in Rust, and the pattern is now too consistent to ignore.&lt;/p&gt;
&lt;p&gt;This shift didn’t start with Rust. It started with the uncomfortable truth that a lot of JavaScript tooling was written in languages that don’t love CPU-heavy parsing and transformation. Then &lt;strong&gt;esbuild&lt;/strong&gt; proved a thesis that felt like heresy to some: rewrite the hot path in a compiled language and the whole experience changes. Suddenly, the “developer workflow” wasn’t a black box—it was a performance budget you could actually win.&lt;/p&gt;
&lt;p&gt;Now the budget has a new currency: Rust.&lt;/p&gt;
&lt;h2 id="from-javascript-everywhere-to-rust-in-the-critical-path"&gt;From “JavaScript everywhere” to “Rust in the critical path”&lt;/h2&gt;
&lt;p&gt;To understand why Rust is showing up everywhere, you have to separate &lt;em&gt;where&lt;/em&gt; JavaScript tooling spends time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parsing&lt;/strong&gt;: turning source code into an AST (and doing it repeatedly)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transforming&lt;/strong&gt;: rewriting syntax features, compiling TS/JSX, applying plugins&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bundling and graph traversal&lt;/strong&gt;: resolving modules and building dependency graphs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linting/formatting&lt;/strong&gt;: walking syntax trees, generating diagnostics, printing consistent output&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of those are algorithmic and CPU-bound. They also benefit from predictable memory usage and fast execution. You can absolutely do high-performance work in JavaScript or TypeScript, but the runtime model fights you: the GC and dynamic dispatch are rarely your friends when you’re trying to parse tens of thousands of lines repeatedly during watch mode.&lt;/p&gt;
&lt;p&gt;Rust’s pitch is straightforward: compile to native code, control allocations, avoid GC pauses, and squeeze performance out of the exact hot path that matters. The irony, of course, is that JavaScript—the language loved for its accessibility—is increasingly using Rust behind the curtain to make the day-to-day experience tolerable.&lt;/p&gt;
&lt;p&gt;If you want a visceral example of why this matters, consider watch-mode workflows. It’s not “how fast can you build once?” It’s “how quickly can you respond to keystrokes without making your laptop sound like a jet engine?” Parsing and re-transforming code happens continuously, and the tooling has to be responsive under constant churn.&lt;/p&gt;
&lt;h2 id="turbopack-rust-first-for-next-app-performance"&gt;Turbopack: Rust-first for “next app” performance&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Turbopack&lt;/strong&gt; is the clearest signal that the Rustification isn’t limited to niche tools—it’s moving into the front yard. It’s being built with a Rust core strategy for the same reason earlier tooling innovations targeted the hot path: fast builds are a feature, not a luxury.&lt;/p&gt;
&lt;p&gt;Where Turbopack matters is in the kinds of feedback loops modern apps expect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instant or near-instant incremental updates&lt;/li&gt;
&lt;li&gt;Aggressive caching&lt;/li&gt;
&lt;li&gt;Dependency graph intelligence&lt;/li&gt;
&lt;li&gt;Handling modern module ecosystems efficiently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, the best developer experience isn’t just speed in a benchmark. It’s the reduction of “dead time.” If your bundler is slow, your mental model gets cut every time the UI freezes: you stop trusting edits, you stop experimenting, you start waiting.&lt;/p&gt;
&lt;p&gt;A Rust-powered bundler core can aim for a simple promise: minimize the work between &lt;em&gt;change&lt;/em&gt; and &lt;em&gt;result&lt;/em&gt;. That means doing less, reusing more, and scheduling what you must do in a way that keeps the pipeline responsive.&lt;/p&gt;
&lt;p&gt;You can think of Turbopack’s Rust direction as “optimize the entire feedback loop,” not just compile something faster. It’s the difference between a race car and a commuter bike with better brakes.&lt;/p&gt;
&lt;h2 id="rspack-bytedances-rust-bet-on-bundler-reality"&gt;Rspack: ByteDance’s Rust bet on bundler reality&lt;/h2&gt;
&lt;p&gt;If Turbopack tells you “the future is Rust,” &lt;strong&gt;Rspack&lt;/strong&gt; tells you “Rust scales.” ByteDance is not interested in experimental prototypes for developer tools—they’re interested in tooling that handles huge codebases, constant iteration, and the practical messiness of real-world apps.&lt;/p&gt;
&lt;p&gt;Bundlers don’t just process small examples. They process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large dependency graphs&lt;/li&gt;
&lt;li&gt;Frequent updates across many files&lt;/li&gt;
&lt;li&gt;Framework-specific patterns and edge cases&lt;/li&gt;
&lt;li&gt;A constant stream of “works on my machine” issues&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rspack’s emphasis on performance aligns with the same core thesis: JavaScript tooling lives or dies by how efficiently it can parse and transform code &lt;em&gt;and&lt;/em&gt; keep that work incremental.&lt;/p&gt;
&lt;p&gt;Here’s a practical framing: if your build pipeline is slow, developers invent workarounds. Those workarounds become cultural overhead. They spawn tribal knowledge (“don’t touch this file,” “avoid this config,” “split this route”), which then makes performance worse over time. High-performance tooling prevents that spiral by reducing the friction that causes the workarounds in the first place.&lt;/p&gt;
&lt;p&gt;Rspack is a sign that teams with serious throughput aren’t treating Rust as novelty. They’re treating it as infrastructure.&lt;/p&gt;
&lt;h2 id="biome-rust-as-the-replacement-layer-for-eslint--prettier"&gt;Biome: Rust as the replacement layer for ESLint + Prettier&lt;/h2&gt;
&lt;p&gt;The Rust trend isn’t confined to bundlers. &lt;strong&gt;Biome&lt;/strong&gt; is where the “Rustification” becomes more intimate: it’s about editing and formatting—the stuff you see every day.&lt;/p&gt;
&lt;p&gt;Historically, the ecosystem built a two-tool pipeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESLint&lt;/strong&gt; for linting (often overlapping with type-aware checks)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prettier&lt;/strong&gt; for formatting (opinionated, predictable output)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That separation can be valuable, but it also creates overhead: more configuration, more tooling startup, more places for conflicts, and sometimes more latency in the editor loop.&lt;/p&gt;
&lt;p&gt;Biome’s Rust-first approach targets the whole formatting/linting experience as one coherent system. That’s the important editorial distinction: it’s not merely faster linting. It’s reducing “tool orchestration tax.” When you collapse multiple concerns into a single engine, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Parse once, analyze multiple rules&lt;/li&gt;
&lt;li&gt;Share AST work between linting and formatting&lt;/li&gt;
&lt;li&gt;Provide more consistent diagnostics and edits&lt;/li&gt;
&lt;li&gt;Reduce the time between file change and “green” state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical way to evaluate this in your own workflow: measure the time from “save file” to “editor stops nagging.” If that cycle feels sluggish, you don’t necessarily need a new rule set—you need fewer passes, less overhead, and faster execution in the critical loop. Rust is particularly suited to this kind of tight iteration.&lt;/p&gt;
&lt;h2 id="oxc-the-compiler-pipeline-creeps-into-everyday-javascript"&gt;Oxc: The compiler pipeline creeps into everyday JavaScript&lt;/h2&gt;
&lt;p&gt;Then there’s &lt;strong&gt;Oxc&lt;/strong&gt;, which represents a deeper shift: Rust is becoming the language of JavaScript’s internal machinery. Oxc isn’t just “a linter.” It’s a toolkit covering parsing, transformation, and linting-like capabilities—effectively, pieces of a compiler pipeline.&lt;/p&gt;
&lt;p&gt;This is where the Rustification becomes a philosophical change. For years, JavaScript tooling has often been “glued together” with plugins and AST walks done in whatever language the project started with. But as tooling grows more ambitious—supporting new syntax, optimizing transforms, and keeping pace with evolving specs—the cost of duct-tape implementations becomes obvious.&lt;/p&gt;
&lt;p&gt;A compiler-like architecture benefits from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast parser implementations&lt;/li&gt;
&lt;li&gt;High-quality AST representations&lt;/li&gt;
&lt;li&gt;Efficient code generation / transformation&lt;/li&gt;
&lt;li&gt;A coherent internal model for rules and diagnostics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust’s ownership model doesn’t magically guarantee correctness, but it does encourage careful engineering around memory and invariants—exactly what you want when you’re building the engine that sits underneath every edit.&lt;/p&gt;
&lt;h2 id="the-real-advantage-smaller-latency-not-just-faster-builds"&gt;The real advantage: smaller latency, not just faster builds&lt;/h2&gt;
&lt;p&gt;It’s tempting to reduce the Rust shift to “performance.” But performance is the surface explanation; the real win is &lt;em&gt;latency predictability&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When tooling is written in slower execution environments, the user feels it as randomness: sometimes it’s fine, sometimes it spikes. Rust-powered tooling aims to make the execution time more consistent—less jank during parse/transform phases, fewer “waiting for the machine” moments.&lt;/p&gt;
&lt;p&gt;That matters because developer workflows aren’t linear. They’re interrupt-driven. You type, you save, you switch tabs, you rename files, you re-run commands. Tooling needs to behave well under interruption and incremental updates. The best tools make you forget they’re running—until they need to output a helpful diagnostic. That’s the ideal loop: fast enough that the tool fades into the background.&lt;/p&gt;
&lt;p&gt;If you’re adopting these tools, prioritize this question over benchmark numbers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does the tool reduce the number of times you have to wait, restart, or rethink your workflow?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In most teams, that’s more valuable than shaving seconds off a one-time build.&lt;/p&gt;
&lt;h2 id="should-you-care-as-a-developeror-only-as-a-tooling-user"&gt;Should you care as a developer—or only as a tooling user?&lt;/h2&gt;
&lt;p&gt;Yes—and no.&lt;/p&gt;
&lt;p&gt;If you’re just using the ecosystem, you’ll feel the improvements without doing anything. Faster hot reloads, less editor lag, quicker feedback on issues: the benefits land automatically.&lt;/p&gt;
&lt;p&gt;But if you’re building libraries, frameworks, or custom tooling, the Rustification changes what “good engineering” looks like in the ecosystem. It sets expectations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AST processing and transforms must be efficient&lt;/li&gt;
&lt;li&gt;Incrementality should be built-in, not bolted on&lt;/li&gt;
&lt;li&gt;Tooling latency is part of the product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the practical advice is simple: treat tooling performance like UX. If your project’s configuration or custom transforms create slow paths, new fast engines won’t save you. You still need to be mindful about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excessive plugin chains&lt;/li&gt;
&lt;li&gt;Unnecessary rebuild triggers&lt;/li&gt;
&lt;li&gt;Expensive type-aware checks in the wrong loop&lt;/li&gt;
&lt;li&gt;Overly broad lint scopes for large repos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust engines can move faster, but they can’t fix design that forces them to do unnecessary work.&lt;/p&gt;
&lt;h2 id="conclusion-rust-isnt-replacing-javascriptits-defending-it"&gt;Conclusion: Rust isn’t replacing JavaScript—it’s defending it&lt;/h2&gt;
&lt;p&gt;The JavaScript ecosystem didn’t become Rust-first because developers suddenly got bored with JavaScript. It happened because the bottleneck moved into places where CPU-heavy parsing and transformation dominate, and the tooling needs to be responsive under constant change.&lt;/p&gt;
&lt;p&gt;Turbopack, Rspack, Biome, Oxc—each tool is different, but they rhyme. They’re rewriting the critical path in Rust to deliver faster feedback loops, lower latency, and more dependable developer workflows.&lt;/p&gt;
&lt;p&gt;And the irony remains delicious: the language that developers reach for because it’s accessible is increasingly paired with Rust behind the scenes because “accessible” should also mean “fast enough to keep up.”&lt;/p&gt;</content></item><item><title>2023: The Year AI Crashed the Developer Party</title><link>https://decastro.work/blog/2023-year-ai-crashed-developer-party/</link><pubDate>Wed, 06 Dec 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/2023-year-ai-crashed-developer-party/</guid><description>&lt;p&gt;In 2023, software development didn’t just get new tools—it got a new baseline expectation. One moment, AI was an experimental novelty; the next, it was sitting in your editor, answering questions while you worked, drafting code while you blinked. The result wasn’t a robot uprising. It was something more uncomfortable: a permanent shift in what “good” looks like for a single developer.&lt;/p&gt;
&lt;h2 id="ai-became-part-of-the-workflownot-a-separate-project"&gt;AI became part of the workflow—not a separate project&lt;/h2&gt;
&lt;p&gt;For years, “AI for developers” sounded like a side quest. People experimented with bots, tried prompts for documentation, and built prototypes that felt impressive but didn’t change the daily rhythm of shipping products.&lt;/p&gt;</description><content>&lt;p&gt;In 2023, software development didn’t just get new tools—it got a new baseline expectation. One moment, AI was an experimental novelty; the next, it was sitting in your editor, answering questions while you worked, drafting code while you blinked. The result wasn’t a robot uprising. It was something more uncomfortable: a permanent shift in what “good” looks like for a single developer.&lt;/p&gt;
&lt;h2 id="ai-became-part-of-the-workflownot-a-separate-project"&gt;AI became part of the workflow—not a separate project&lt;/h2&gt;
&lt;p&gt;For years, “AI for developers” sounded like a side quest. People experimented with bots, tried prompts for documentation, and built prototypes that felt impressive but didn’t change the daily rhythm of shipping products.&lt;/p&gt;
&lt;p&gt;In 2023, that changed. ChatGPT became a default reference tool for many developers, and Copilot-style assistants moved from “try it” to “use it.” The practical impact is simple: AI started compressing time-to-first-draft. You asked fewer “Where do I start?” questions and more “Will this scale?” questions—because the first answer arrived instantly.&lt;/p&gt;
&lt;p&gt;That speed is addictive, but it’s also a trap if you treat AI output as truth. A common 2023 pattern: a developer accepts a generated function, runs tests, and then discovers edge cases three days later. The mistake isn’t using AI—it’s skipping the “human responsibility” step that you can’t delegate: validating assumptions. AI is excellent at producing plausible solutions. It’s not responsible for your product’s invariants.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical framing for 2024:&lt;/strong&gt; treat AI like a powerful junior teammate. Great at drafts, unreliable on consequences. Require it to explain, justify, and reveal uncertainty. If the assistant can’t articulate trade-offs—latency, cost, correctness, failure modes—it’s not ready for production decisions.&lt;/p&gt;
&lt;h2 id="the-bar-rose-one-developer-could-do-moreand-had-to"&gt;The bar rose: one developer could do more—and had to&lt;/h2&gt;
&lt;p&gt;The biggest story of 2023 wasn’t replacement. It was leverage. When tools reduce friction for common tasks—boilerplate, translation between APIs, test scaffolding, code review suggestions—the limiting factor moves. Less time gets spent wrestling syntax, more gets spent on architecture, correctness, and system behavior.&lt;/p&gt;
&lt;p&gt;This is why 2023 felt disruptive even to developers who barely changed their codebase. The work became less about writing the first version and more about owning the second and third: performance tuning, security hardening, and resilience under real-world chaos.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine a developer building an internal service. Without AI, they might spend an entire afternoon wiring request handlers, validating inputs, and writing basic tests. With AI, they can get that skeleton running in minutes. Great. But now they’re expected to use that freed time to handle the stuff that AI can’t “solve” for you: rate limits, idempotency, observability, schema evolution, and clear failure semantics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated takeaway:&lt;/strong&gt; the future isn’t “AI writes code.” It’s “AI writes drafts,” and developers become accountable for design quality. In 2023, companies learned they could expect faster delivery from individuals. That expectation doesn’t vanish. It just raises the cost of shipping sloppy work.&lt;/p&gt;
&lt;h2 id="copilot-style-coding-changed-how-teams-review-code"&gt;Copilot-style coding changed how teams review code&lt;/h2&gt;
&lt;p&gt;Once AI is in the editor, pull requests change. The review surface area doesn’t disappear—it morphs.&lt;/p&gt;
&lt;p&gt;In 2023, many teams discovered that “looks good” is no longer sufficient. Generated code can be stylistically consistent and logically coherent while still being wrong for your context. It may:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;misunderstand your domain constraints,&lt;/li&gt;
&lt;li&gt;ignore your existing abstractions,&lt;/li&gt;
&lt;li&gt;introduce inefficient patterns that pass tests but struggle in production,&lt;/li&gt;
&lt;li&gt;or quietly omit security and error-handling details you normally wouldn’t forget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best teams responded by tightening review criteria. Instead of spending energy debating whether a loop should be a &lt;code&gt;for&lt;/code&gt; or a &lt;code&gt;while&lt;/code&gt;, reviews shifted toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;correctness under edge cases,&lt;/li&gt;
&lt;li&gt;adherence to system conventions,&lt;/li&gt;
&lt;li&gt;performance characteristics (especially around I/O and concurrency),&lt;/li&gt;
&lt;li&gt;and explicit handling of failures (timeouts, retries, partial results).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; update your team’s pull request checklist to reflect AI realities. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the change include tests for failure modes?&lt;/li&gt;
&lt;li&gt;Does it follow established error-handling patterns?&lt;/li&gt;
&lt;li&gt;Are we using safe parsing/validation instead of trusting inputs?&lt;/li&gt;
&lt;li&gt;Are we logging enough to debug incidents without drowning in noise?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your review process didn’t evolve in 2023, your quality drift probably did—just quietly.&lt;/p&gt;
&lt;h2 id="postgresql-finished-the-job-as-the-default-database-choice"&gt;PostgreSQL “finished the job” as the default database choice&lt;/h2&gt;
&lt;p&gt;Every developer has a story about database indecision: one project that used something “new” that became a liability, another that bolted on caching without understanding consistency, another that promised “schema-less flexibility” and delivered chaos instead.&lt;/p&gt;
&lt;p&gt;In 2023, PostgreSQL’s position as the default backbone hardened. This wasn’t a dramatic announcement—it was a continuation. Teams kept choosing it because it reliably supports the real work: transactions, indexing, query planning, constraints, and the boring discipline that keeps data from turning into a haunted house.&lt;/p&gt;
&lt;p&gt;When you combine PostgreSQL with modern tooling, it becomes an even more practical default. AI-assisted development thrives on stable primitives. It’s easier to ask an assistant to generate correct migrations, queries, and test cases when the “target” system is mature and well-understood.&lt;/p&gt;
&lt;p&gt;A common 2023 workflow looked like this: developers built features faster using assistants, then leaned on PostgreSQL to keep data integrity intact. They still had to learn how to write good queries and model relationships—but they weren’t fighting the database itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice if you’re modernizing in 2024:&lt;/strong&gt; don’t treat PostgreSQL as a novelty. Treat it like the core product. Invest in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;proper indexing (and measuring query plans),&lt;/li&gt;
&lt;li&gt;constraints and validation at the schema level,&lt;/li&gt;
&lt;li&gt;migrations you can roll forward and back safely,&lt;/li&gt;
&lt;li&gt;and observability around slow queries and lock contention.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fastest AI-generated feature is worthless if it causes recurring incidents at 2 a.m.&lt;/p&gt;
&lt;h2 id="rust-kept-expanding-the-systems-programming-frontier"&gt;Rust kept expanding the systems programming frontier&lt;/h2&gt;
&lt;p&gt;While AI dominated headlines, 2023 also reinforced a quieter story: developers keep choosing Rust for systems-level correctness and performance without forfeiting safety.&lt;/p&gt;
&lt;p&gt;Rust’s “conquest” isn’t about replacing every language. It’s about winning the slices where teams care deeply about memory safety, concurrency, and predictable behavior. In practice, Rust made its case by being usable: tooling matured, crates ecosystems filled in the gaps, and teams could move from “this is promising” to “this is our standard” at an accelerating pace.&lt;/p&gt;
&lt;p&gt;A useful way to view 2023’s Rust momentum is as a counterpoint to AI. AI can generate code quickly, but systems programming punishes assumptions. You can’t hand-wave data races or undefined behavior. Rust’s borrow checker isn’t just a constraint—it’s a forcing function that makes correctness cheaper over time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; if you’re adopting Rust in 2024, resist the urge to rewrite everything. Start with components that benefit immediately from safety and performance: parsers, protocol handlers, background workers, and services with heavy concurrency. Then build a culture: consistent error handling, profiling early, and a shared approach to testing.&lt;/p&gt;
&lt;h2 id="the-real-question-for-2024-how-not-to-become-dependent"&gt;The real question for 2024: how not to become dependent&lt;/h2&gt;
&lt;p&gt;AI in 2023 didn’t replace developers. It replaced some steps in the workflow. That’s a gift—but gifts can become crutches.&lt;/p&gt;
&lt;p&gt;Dependency doesn’t look like “not coding.” It looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;blindly trusting output,&lt;/li&gt;
&lt;li&gt;failing to understand the generated code enough to debug it,&lt;/li&gt;
&lt;li&gt;shipping with weak tests because “it worked in the demo,”&lt;/li&gt;
&lt;li&gt;and letting expertise atrophy because the machine handles the first draft.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To avoid that outcome, you need intentional habits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with “understand before you merge.”&lt;/strong&gt; When AI drafts a function, your job is to verify behavior, not just compile success. Ask it to add tests for edge cases. Then run them. When it suggests an optimization, measure it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use AI to accelerate learning, not bypass it.&lt;/strong&gt; A good workflow is: generate → inspect → refactor → explain. If you can’t explain why the code is correct, it’s not done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Codify prompt patterns as team knowledge.&lt;/strong&gt; Prompts are like macros: they’re powerful when standardized. Create internal prompt templates for common tasks—migrations, query writing, test generation, security checks—and document the review expectations. The goal is not to standardize wording. It’s to standardize rigor.&lt;/p&gt;
&lt;h2 id="conclusion-2023-raised-the-bar-2024-is-about-craftsmanship-with-leverage"&gt;Conclusion: 2023 raised the bar; 2024 is about craftsmanship with leverage&lt;/h2&gt;
&lt;p&gt;2023 was the year AI crashed the developer party—then quietly became the music. The change wasn’t that developers vanished. It’s that shipping got faster, and quality expectations rose with it.&lt;/p&gt;
&lt;p&gt;The smartest teams in 2024 won’t ask whether to use AI. They’ll ask how to use it without surrendering judgment. If AI drafts the code, developers must own the consequences. That’s the new contract—and it’s how the best work will keep happening.&lt;/p&gt;</content></item><item><title>Advent of Code Is the Best Professional Development You're Not Doing</title><link>https://decastro.work/blog/advent-of-code-best-professional-development/</link><pubDate>Fri, 24 Nov 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/advent-of-code-best-professional-development/</guid><description>&lt;p&gt;Every December, a weird little ritual takes over developer calendars: people voluntarily sign up for an inbox of intentionally pointless problems. Advent of Code doesn’t ship features. It doesn’t close tickets. It certainly doesn’t care about your sprint goals. And yet, if you want a fast track to becoming a sharper production engineer, it’s one of the best training programs you’ll actually do.&lt;/p&gt;
&lt;p&gt;Not because the puzzles are “fun” in some vague, hand-wavy way—but because they relentlessly pressure the exact skills that make work smoother when the stakes are higher: parsing what you thought would be easy, handling edge cases you didn’t model, optimizing the right thing, and turning “I think this works” into “this works everywhere.”&lt;/p&gt;</description><content>&lt;p&gt;Every December, a weird little ritual takes over developer calendars: people voluntarily sign up for an inbox of intentionally pointless problems. Advent of Code doesn’t ship features. It doesn’t close tickets. It certainly doesn’t care about your sprint goals. And yet, if you want a fast track to becoming a sharper production engineer, it’s one of the best training programs you’ll actually do.&lt;/p&gt;
&lt;p&gt;Not because the puzzles are “fun” in some vague, hand-wavy way—but because they relentlessly pressure the exact skills that make work smoother when the stakes are higher: parsing what you thought would be easy, handling edge cases you didn’t model, optimizing the right thing, and turning “I think this works” into “this works everywhere.”&lt;/p&gt;
&lt;h2 id="why-pointless-puzzles-arent-pointless"&gt;Why pointless puzzles aren’t pointless&lt;/h2&gt;
&lt;p&gt;Advent of Code is intentionally designed to be a workout. Each year delivers 25 daily problems that are conceptually small but technically sticky. The inputs are messy in the way real systems are messy—lines break unexpectedly, separators aren’t what you assumed, counts overflow your intuition, and off-by-one errors show up like clockwork.&lt;/p&gt;
&lt;p&gt;In product engineering, you often stay in your domain. You build pipelines, services, UIs, or infrastructure within a familiar toolbox. That’s valuable, but it can lead to a kind of cognitive comfort: you default to the approach you’ve used before.&lt;/p&gt;
&lt;p&gt;Advent of Code blows that comfort up. One day you’re writing a parser. The next you’re doing graph traversal. Then you’re threading a needle through dynamic programming. This is not “domain hopping” for entertainment; it’s targeted cross-training. You’re forced to learn how to think when the usual assumptions don’t apply.&lt;/p&gt;
&lt;p&gt;And here’s the part that matters: the skills don’t stay trapped in December. They migrate into January as habits—cleaner code boundaries, more careful reasoning, better test instincts, and a willingness to scrutinize performance before it hurts.&lt;/p&gt;
&lt;h2 id="the-real-professional-skill-turning-inputs-into-trusted-behavior"&gt;The real professional skill: turning inputs into trusted behavior&lt;/h2&gt;
&lt;p&gt;The fastest way to become a better production engineer is to improve your relationship with inputs. In the real world, bad data isn’t rare—it’s just not always your fault. Teams learn (or should learn) to treat input handling as a first-class concern.&lt;/p&gt;
&lt;p&gt;Advent of Code starts there. Even when the puzzle theme is whimsical, the practical task is always the same: take a text blob, interpret it correctly, and produce accurate results for two parts—often with Part 2 requiring a more sophisticated approach.&lt;/p&gt;
&lt;p&gt;Consider the recurring pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parse&lt;/strong&gt; an input format you don’t fully control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt; the problem state clearly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solve&lt;/strong&gt; for Part 1 with a straightforward technique.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor or upgrade&lt;/strong&gt; for Part 2 when the naive solution fails—usually due to time complexity, memory use, or a misunderstood constraint.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That cycle is a microcosm of production work. In January, when a new service receives unexpected payload shapes or when a query slows down after growth, you’re better prepared—not because the problems were identical, but because your brain learned to treat “what does the input actually mean?” as a disciplined question.&lt;/p&gt;
&lt;h2 id="the-edge-case-factory-and-why-its-good-for-you"&gt;The edge-case factory (and why it’s good for you)&lt;/h2&gt;
&lt;p&gt;If you want to know what separates reliable engineers from “works on my machine” engineers, it’s not heroics. It’s edge cases.&lt;/p&gt;
&lt;p&gt;Advent of Code is a deliberate edge-case factory. The puzzles often reward players who do the boring work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confirming boundary behavior (empty lines, singleton lists, zero-length segments)&lt;/li&gt;
&lt;li&gt;Validating assumptions (are you indexing rows or coordinates?)&lt;/li&gt;
&lt;li&gt;Handling special cases explicitly instead of hiding them in vague “else” logic&lt;/li&gt;
&lt;li&gt;Making invariants obvious (what must always be true at each step?)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common example: movement and adjacency problems. You’ll repeatedly manage coordinate transforms. In production, you’ll later deal with time zones, pagination, coordinate systems in graphics, or indexing in caches. The details differ, but the failure modes rhyme.&lt;/p&gt;
&lt;p&gt;When you solve these puzzles, you develop a reflex: before you run, you ask where the logic might break. You start building confidence through reasoning, not luck.&lt;/p&gt;
&lt;h2 id="learn-outside-your-comfort-zoneon-purpose"&gt;Learn outside your comfort zone—on purpose&lt;/h2&gt;
&lt;p&gt;The strongest argument for Advent of Code as development is simple: &lt;strong&gt;do it in a language you’re learning&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Learning a new language makes the training more honest. It forces you to engage with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parsing and string handling idioms&lt;/li&gt;
&lt;li&gt;data structure tradeoffs&lt;/li&gt;
&lt;li&gt;performance characteristics&lt;/li&gt;
&lt;li&gt;error handling patterns&lt;/li&gt;
&lt;li&gt;test ergonomics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For instance, in one language you might reach instinctively for regex-heavy parsing and forget about maintainability. In another, the standard library might steer you toward robust parsing utilities. Either way, you feel the consequences quickly because the puzzles don’t care about your preferences—they care about correctness and efficiency.&lt;/p&gt;
&lt;p&gt;A practical rule: pick one “home language” for the year and stick with it for the whole series. You don’t need to become fluent in 25 days; you need to build a repeatable workflow: read input → model → implement → test → iterate.&lt;/p&gt;
&lt;p&gt;If you bounce languages mid-season, you’ll lose the compounding effect. The point is to let your tooling and muscle memory improve over time.&lt;/p&gt;
&lt;h2 id="treat-it-like-a-mini-engineering-project"&gt;Treat it like a mini engineering project&lt;/h2&gt;
&lt;p&gt;Advent of Code is a personal game unless you upgrade the process. If you want the training to stick, run it like a small engineering project:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write a real parser for the input&lt;/strong&gt;, even if a quick split works today.&lt;br&gt;
Tomorrow’s input variations will punish sloppy assumptions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add tests early&lt;/strong&gt;, not just at the end.&lt;br&gt;
Most puzzle statements include examples. Turn those into tests. Then create additional tests for boundary scenarios you can reason about.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Benchmark the Part 2 approach&lt;/strong&gt;, even if it’s “just” a puzzle.&lt;br&gt;
If your algorithm becomes slow, that’s your clue that your production instinct should already be screaming: optimize the right thing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Refactor between parts.&lt;/strong&gt;&lt;br&gt;
A common trap is copying your Part 1 solution into Part 2 and bolting on hacks. Instead, extract reusable functions (grid parsing, neighbor generation, graph edges, state transitions) and keep the core logic clean.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write explanations in comments or a short README.&lt;/strong&gt;&lt;br&gt;
Future-you is a teammate. Your future self will thank you when you revisit an approach months later.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s a concrete example workflow that pays dividends: suppose you’re solving a grid navigation puzzle. Write helper functions first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parse_grid(input)&lt;/code&gt; that returns a structured grid&lt;/li&gt;
&lt;li&gt;&lt;code&gt;neighbors(position, grid)&lt;/code&gt; that centralizes movement rules&lt;/li&gt;
&lt;li&gt;&lt;code&gt;solve_part1()&lt;/code&gt; for the straightforward shortest path (maybe BFS)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;solve_part2()&lt;/code&gt; for the modified constraint (maybe Dijkstra or BFS with additional state)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you do this repeatedly across days, you’re practicing the same habits you’ll need for production: modularity, clarity, and separation of concerns.&lt;/p&gt;
&lt;h2 id="what-youll-notice-in-january"&gt;What you’ll notice in January&lt;/h2&gt;
&lt;p&gt;The improvements aren’t theoretical. They show up as small but compounding differences in your day-to-day work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fewer “surprises”&lt;/strong&gt; when inputs don’t match your expectations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better algorithm choices&lt;/strong&gt; under constraints, because you’ve felt performance pain before.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More deliberate edge-case coverage&lt;/strong&gt;, especially around boundaries and indexing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner code structure&lt;/strong&gt;, because you’re constantly forced to revisit designs when Part 2 arrives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A calmer approach to complexity&lt;/strong&gt;, since you’ve trained on escalating difficulty without panicking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most valuable shift is mental. Advent of Code teaches you to respect constraints—time, memory, correctness—early. You stop assuming “it should work” and start proving it, either with tests or with reasoning, or both.&lt;/p&gt;
&lt;p&gt;And yes, it’s still fun. But fun is the marketing. Skill is the product.&lt;/p&gt;
&lt;h2 id="conclusion-do-it-then-keep-the-habits"&gt;Conclusion: Do it, then keep the habits&lt;/h2&gt;
&lt;p&gt;Advent of Code is the best professional development you’re not doing because it’s not pretending to be a course. It’s a forcing function: it drags you into unfamiliar problem types, punishes sloppy assumptions, and trains the engineering behaviors that matter most when real systems fail.&lt;/p&gt;
&lt;p&gt;If you want a measurable impact, don’t just “solve puzzles.” Run it like an engineering practice. Use a language you’re learning. Build tests from the examples. Refactor deliberately. Then carry the habits into January—where the work actually happens.&lt;/p&gt;</content></item><item><title>Signals Are Taking Over Frontend Frameworks and React Is Late to the Party</title><link>https://decastro.work/blog/signals-taking-over-frontend-react-late/</link><pubDate>Sun, 12 Nov 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/signals-taking-over-frontend-react-late/</guid><description>&lt;p&gt;Frontend development is finally admitting what many teams have felt for years: “re-render everything” is a blunt instrument. In its place, a more precise paradigm is spreading across frameworks—signals. These reactive primitives track what depends on what, then update only the parts of the UI that actually need to change. Angular did it first, Solid made it feel effortless, and Vue/Preact/Qwik shipped variants. React, meanwhile, is still treating memoization as an art form instead of a baseline capability.&lt;/p&gt;</description><content>&lt;p&gt;Frontend development is finally admitting what many teams have felt for years: “re-render everything” is a blunt instrument. In its place, a more precise paradigm is spreading across frameworks—signals. These reactive primitives track what depends on what, then update only the parts of the UI that actually need to change. Angular did it first, Solid made it feel effortless, and Vue/Preact/Qwik shipped variants. React, meanwhile, is still treating memoization as an art form instead of a baseline capability.&lt;/p&gt;
&lt;p&gt;This isn’t just trend-chasing. It’s a different mental model—one that reduces wasted work and makes performance less of a self-inflicted tax.&lt;/p&gt;
&lt;h2 id="what-signals-change-from-re-render-the-world-to-update-the-truth"&gt;What signals change: from “re-render the world” to “update the truth”&lt;/h2&gt;
&lt;p&gt;The core idea behind signals is simple: state is represented by primitives that know who depends on them. When a signal changes, the system automatically recalculates derived values and re-renders only the minimal set of consumers.&lt;/p&gt;
&lt;p&gt;Contrast that with the dominant React pattern most apps still follow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UI re-renders when component state/props change.&lt;/li&gt;
&lt;li&gt;Developers then try to prevent unnecessary re-renders using &lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, and &lt;code&gt;useCallback&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When optimization fails, the fix is often “sprinkle more memoization,” sometimes with fragile dependency arrays and unclear performance wins.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Signals flip that burden. You describe reactive relationships once, and the runtime keeps them consistent. If a UI subtree doesn’t depend on the updated signal, it never re-renders in the first place.&lt;/p&gt;
&lt;p&gt;A practical way to see the difference: imagine a dashboard with filters on the left and a chart list on the right. When a user toggles a filter, only the chart list needs to change. With signals, that dependency is explicit: chart rendering consumes filter signals, while unrelated UI doesn’t. With re-render-everything, the “chart list” is often still re-rendered indirectly—React will reconcile anyway, and whether reconciliation short-circuits depends on memoization strategy and component structure.&lt;/p&gt;
&lt;h2 id="angular-solid-vue-preact-qwik-the-ecosystem-is-converging"&gt;Angular, Solid, Vue, Preact, Qwik: the ecosystem is converging&lt;/h2&gt;
&lt;p&gt;Signals aren’t one monolithic thing—each framework has its own flavor—but the trajectory is clear: the industry is converging on dependency-tracked reactivity as the default.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Angular&lt;/strong&gt; adopted signals as a first-class reactivity model. It’s not positioned as a niche optimization; it’s the direction of the platform.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solid&lt;/strong&gt; is essentially “signals-first.” Reactive updates are granular by design, and UI updates are tied directly to reactive reads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue&lt;/strong&gt; introduced reactivity that feels very close to signals in practice, with dependency tracking and targeted updates baked into the framework’s reactivity system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preact&lt;/strong&gt; has followed the same pragmatic path: keep updates precise and fast without forcing developers into a maze of memoization hooks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qwik&lt;/strong&gt; takes the idea further by optimizing for resumability (the ability to load less JavaScript up front), while still using signals/reac­tivity patterns to keep UI updates aligned with actual data dependencies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The common thread across these ecosystems is that “efficiency” isn’t an afterthought. It’s what the runtime does while you focus on building features.&lt;/p&gt;
&lt;h2 id="reacts-holdout-power-through-memoization-or-a-performance-tax-in-disguise"&gt;React’s holdout: power through memoization, or a performance tax in disguise?&lt;/h2&gt;
&lt;p&gt;React has a sophisticated rendering model and a strong mental framework—there’s a reason it became the default for years. But in the signal era, React’s current stance reads less like a principled design decision and more like a willingness to outsource performance to developers.&lt;/p&gt;
&lt;p&gt;In practice, the React approach often looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A state update occurs.&lt;/li&gt;
&lt;li&gt;Components re-render.&lt;/li&gt;
&lt;li&gt;Developers attempt to curb the blast radius:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useMemo&lt;/code&gt; to avoid recomputing expensive derived values.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useCallback&lt;/code&gt; to stabilize function identities passed to children.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;React.memo&lt;/code&gt; to prevent re-renders of child components.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If something still re-renders, you profile, refactor, and repeat.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The problem isn’t that React memoization can work. It’s that the default path requires constant vigilance. A small code change—like capturing stale values incorrectly or missing a dependency in a memo hook—can silently sabotage performance (or correctness).&lt;/p&gt;
&lt;p&gt;Worse, many teams end up with optimization logic that’s harder to read than the actual UI code. The application becomes a patchwork of “render control” rather than a clean expression of data dependencies.&lt;/p&gt;
&lt;p&gt;To be blunt: React didn’t just lag behind signals. It kept the assumption that developers can and should manually manage reactivity boundaries.&lt;/p&gt;
&lt;h2 id="the-real-benefit-signals-make-performance-predictable-not-heroic"&gt;The real benefit: signals make performance predictable, not heroic&lt;/h2&gt;
&lt;p&gt;The most compelling argument for signals is not raw speed—it’s &lt;em&gt;predictability&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;With dependency-tracked updates, the system knows exactly which computations and UI pieces are affected. That turns performance into a property of the programming model, not a recurring project management task.&lt;/p&gt;
&lt;p&gt;Consider a common React performance trap: a list rendering component with derived sorting/filtering. Developers often wrap the sort with &lt;code&gt;useMemo&lt;/code&gt; and the callbacks with &lt;code&gt;useCallback&lt;/code&gt;. But the real work is determining whether the dependencies truly change when they should. In large apps, dependency arrays become a form of long-term technical debt.&lt;/p&gt;
&lt;p&gt;Signals reduce this fragility. Derived values are automatically recomputed when (and only when) the signals they read change. You’re not asking React to guess when recomputation is necessary—you’re telling the reactive system what depends on what.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like conceptually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A derived value reads &lt;code&gt;filterSignal&lt;/code&gt; and &lt;code&gt;itemsSignal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;filterSignal&lt;/code&gt; changes, only the derived value recalculates.&lt;/li&gt;
&lt;li&gt;Only the UI bound to that derived value updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No need to propagate “stability” via &lt;code&gt;useCallback&lt;/code&gt; just to satisfy referential equality.&lt;/p&gt;
&lt;h2 id="practical-migration-mindset-you-dont-need-a-rewrite-to-learn-from-signals"&gt;Practical migration mindset: you don’t need a rewrite to learn from signals&lt;/h2&gt;
&lt;p&gt;React might not be shipping signals broadly tomorrow (or ever, in the same way), but you can still adopt the core lesson: make dependencies explicit.&lt;/p&gt;
&lt;p&gt;If you want the practical benefits without a framework switch, start by changing how you structure state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Co-locate derived state with its inputs.&lt;/strong&gt; If something is derived from multiple sources, keep it near the code that consumes it and ensure it’s recomputed only when those sources change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid passing unstable objects/functions deep into the tree.&lt;/strong&gt; Even in React, this is often where performance leaks out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer localized state updates over global “everything changed” patterns.&lt;/strong&gt; If one change invalidates a huge subtree, your reactivity boundaries are too coarse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profile before optimizing.&lt;/strong&gt; The goal isn’t to memoize everything—it’s to stop re-render storms at the source.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re already using React and feel forced into the memoization treadmill, signals are the clearest signal (pun intended) that you should reconsider your architecture. You don’t have to adopt a new framework to adopt the mindset: dependencies should drive updates, not the other way around.&lt;/p&gt;
&lt;p&gt;And if you are choosing a new stack today? The decision is hard to justify for teams optimizing for correctness &lt;em&gt;and&lt;/em&gt; performance without micromanaging re-render boundaries. Angular/Solid/Vue/Preact/Qwik aren’t perfect, but their direction is coherent: reactive primitives first, optimization as default behavior rather than a developer chore.&lt;/p&gt;
&lt;h2 id="conclusion-signals-are-the-direction-of-travelreact-should-catch-up"&gt;Conclusion: signals are the direction of travel—React should catch up&lt;/h2&gt;
&lt;p&gt;Signals represent a shift from “render cycles as the unit of work” to “data dependency as the unit of truth.” That change reduces unnecessary work, simplifies reasoning about updates, and removes a large portion of the performance burden from developers.&lt;/p&gt;
&lt;p&gt;React can still build great products, and it will keep its loyal base. But the ecosystem is converging on signals because they solve a real, recurring problem: re-render-everything models force developers to simulate dependency tracking after the fact. Signals don’t simulate—they embody it.&lt;/p&gt;
&lt;p&gt;At this point, the question isn’t whether signals are better. It’s whether React wants to treat them as a feature of modern UI engineering rather than a competing philosophy—because the rest of the industry already has.&lt;/p&gt;</content></item><item><title>GitHub Copilot Chat vs. ChatGPT for Coding: Which Actually Ships Better Code?</title><link>https://decastro.work/blog/copilot-chat-vs-chatgpt-coding-comparison/</link><pubDate>Sun, 05 Nov 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/copilot-chat-vs-chatgpt-coding-comparison/</guid><description>&lt;p&gt;AI coding tools are everywhere, but “better” is the wrong question. The question that matters is simpler: which one gets you to a commit that actually passes review—faster, with less thrash? I tested two popular options head-to-head—GitHub Copilot Chat and ChatGPT—across real tasks you’d recognize from a normal engineering week: debugging, refactoring, test generation, code review, and greenfield feature work. Then I looked not at demos, but at the edit distance between “AI output” and “shippable code.”&lt;/p&gt;</description><content>&lt;p&gt;AI coding tools are everywhere, but “better” is the wrong question. The question that matters is simpler: which one gets you to a commit that actually passes review—faster, with less thrash? I tested two popular options head-to-head—GitHub Copilot Chat and ChatGPT—across real tasks you’d recognize from a normal engineering week: debugging, refactoring, test generation, code review, and greenfield feature work. Then I looked not at demos, but at the edit distance between “AI output” and “shippable code.”&lt;/p&gt;
&lt;h2 id="the-setup-30-tasks-real-constraints-real-code"&gt;The Setup: 30 Tasks, Real Constraints, Real Code&lt;/h2&gt;
&lt;p&gt;This wasn’t a synthetic benchmark where everyone agrees on the same toy problem. I ran both tools through 30 real-world coding tasks, roughly evenly distributed across five categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Debugging&lt;/strong&gt;: given a failing function or stack trace, fix the bug with minimal churn.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactoring&lt;/strong&gt;: improve structure without changing behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test generation&lt;/strong&gt;: add unit tests that match existing patterns and libraries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code review&lt;/strong&gt;: identify issues, suggest improvements, and estimate risk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Greenfield implementation&lt;/strong&gt;: implement a feature from a specification with reasonable coding standards.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A few ground rules made the results meaningful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I used the tools in the environments they’re designed for: &lt;strong&gt;Copilot Chat inside the IDE&lt;/strong&gt; and &lt;strong&gt;ChatGPT in a browser&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;I scored outcomes by whether the code worked on the first integration attempt, how much editing it required, and how often the tool needed follow-up clarification to avoid drifting.&lt;/li&gt;
&lt;li&gt;For “code generation,” I measured a practical metric: &lt;strong&gt;the percentage of output that needed modification&lt;/strong&gt; before it was correct and stylistically consistent. In both tools, it hovered around &lt;strong&gt;60%&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point matters. It’s the antidote to the hype.&lt;/p&gt;
&lt;h2 id="what-ide-integrated-ai-does-better-in-context-modification"&gt;What IDE-Integrated AI Does Better: In-Context Modification&lt;/h2&gt;
&lt;p&gt;Copilot Chat’s biggest advantage isn’t that it “understands code” in some abstract way. It’s that it &lt;strong&gt;lives with your code&lt;/strong&gt;. When your IDE has open files, surrounding definitions, local types, and the current refactor context, the assistant can stop guessing.&lt;/p&gt;
&lt;h3 id="example-debugging-without-re-explaining-the-world"&gt;Example: Debugging Without Re-Explaining the World&lt;/h3&gt;
&lt;p&gt;In debugging tasks, Copilot Chat consistently performed better because it could see the relevant module structure immediately. For instance, when tracking down a null-handling bug in a TypeScript utility, I could point to the failing call site and ask Copilot Chat to “fix the logic and keep the current interface.” It didn’t need me to paste the entire dependency graph—because it could infer what mattered from the workspace.&lt;/p&gt;
&lt;p&gt;The result wasn’t magic. It was operationally cheaper:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fewer clarifying questions,&lt;/li&gt;
&lt;li&gt;smaller patches,&lt;/li&gt;
&lt;li&gt;less time reconciling assumptions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="example-refactors-that-respect-the-local-style"&gt;Example: Refactors That Respect the Local Style&lt;/h3&gt;
&lt;p&gt;When refactoring, Copilot Chat also tended to respect local patterns: naming conventions, helper functions already present, and idioms used in nearby files. In one refactor, I asked it to “extract validation into a reusable function” while preserving error types. It produced a change set that matched the project’s existing approach—so I edited minor details rather than rewriting the whole plan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated takeaway:&lt;/strong&gt; if your problem is “change these lines correctly, given what’s already here,” Copilot Chat is the more reliable tool. It’s built for the &lt;em&gt;moment-to-moment&lt;/em&gt; work of editing.&lt;/p&gt;
&lt;h2 id="where-chatgpt-holds-the-edge-architecture-planning-and-dialogue"&gt;Where ChatGPT Holds the Edge: Architecture, Planning, and Dialogue&lt;/h2&gt;
&lt;p&gt;ChatGPT’s advantage isn’t proximity to your workspace. It’s its ability to handle &lt;strong&gt;long-form reasoning&lt;/strong&gt; and maintain a coherent thread over multiple iterations—especially when the problem is bigger than a single file.&lt;/p&gt;
&lt;h3 id="example-greenfield-feature-planning"&gt;Example: Greenfield Feature Planning&lt;/h3&gt;
&lt;p&gt;For greenfield tasks, I often started with a feature spec and constraints: “Implement X, but follow our modular boundaries and ensure we can later add Y.” ChatGPT did better in two ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It proposed an architecture first&lt;/strong&gt;—components, responsibilities, and how data flows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It handled iterative refinement&lt;/strong&gt; without losing context.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Copilot Chat can do architecture too, but it’s more naturally oriented toward local changes. ChatGPT, by contrast, feels like you’re collaborating with someone who can hold the full project model in their working set while you debate tradeoffs.&lt;/p&gt;
&lt;h3 id="example-code-review-as-a-conversation"&gt;Example: Code Review as a Conversation&lt;/h3&gt;
&lt;p&gt;For code review tasks, I leaned on ChatGPT for a different reason: I could ask it to walk through reasoning, risks, and alternative approaches. When reviewing concurrency logic or API design decisions, I didn’t just want a list of “what’s wrong.” I wanted the “why,” the “what could break,” and the “what would you change if you had a day to improve it.”&lt;/p&gt;
&lt;p&gt;ChatGPT tends to deliver that in a more dialog-friendly way—especially across multiple rounds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated takeaway:&lt;/strong&gt; if your problem is “make sense of the design, choose an approach, and iterate through tradeoffs,” ChatGPT is usually the stronger partner.&lt;/p&gt;
&lt;h2 id="the-surprise-neither-tool-is-reliably-good-at-code-generation"&gt;The Surprise: Neither Tool Is Reliably “Good at Code Generation”&lt;/h2&gt;
&lt;p&gt;Here’s the part that undercuts the most common marketing claims: for the act of generating code that you can drop in and run, &lt;strong&gt;neither tool was consistently better&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Across the tasks, both tools produced code that required modification about &lt;strong&gt;60% of the time&lt;/strong&gt;. Sometimes that modification was small—renaming symbols, adjusting edge-case handling, or aligning with existing abstractions. Other times it was structural—an incomplete implementation, mismatched assumptions about libraries, or logic that worked in isolation but not in the project.&lt;/p&gt;
&lt;h3 id="why-this-happens-and-why-its-not-a-deal-breaker"&gt;Why this happens (and why it’s not a deal-breaker)&lt;/h3&gt;
&lt;p&gt;Code generation fails in predictable ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Project-specific conventions&lt;/strong&gt; aren’t always known to the model, even when context is provided.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hidden dependencies&lt;/strong&gt; (configuration, error types, middleware expectations, existing helper utilities) cause drift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt; are rarely captured correctly on the first try, especially when the spec is underspecified.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here’s the useful reframing: “bad at generating perfect code” doesn’t mean “bad at helping you ship.” In practice, the real win is using the tool to compress the gap between a vague idea and a working baseline—then using your judgment (and tests) to finish the job.&lt;/p&gt;
&lt;h2 id="practical-workflow-use-both-and-use-them-for-what-theyre-best-at"&gt;Practical Workflow: Use Both, and Use Them for What They’re Best At&lt;/h2&gt;
&lt;p&gt;So what’s the best system? Not “pick a winner.” It’s a workflow that matches the tools to the tasks.&lt;/p&gt;
&lt;h3 id="when-to-use-copilot-chat-in-ide"&gt;When to Use Copilot Chat (In-IDE)&lt;/h3&gt;
&lt;p&gt;Use Copilot Chat when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;targeted edits to open files,&lt;/li&gt;
&lt;li&gt;small refactors that must match local patterns,&lt;/li&gt;
&lt;li&gt;quick debugging tied to the current codebase,&lt;/li&gt;
&lt;li&gt;test additions that match existing conventions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tactic:&lt;/strong&gt; Ask for &lt;em&gt;a patch&lt;/em&gt;, not an essay. For example:&lt;br&gt;
“Fix the bug here while preserving the public API. Show the diff and don’t refactor unrelated code.”&lt;/p&gt;
&lt;h3 id="when-to-use-chatgpt-browser-based"&gt;When to Use ChatGPT (Browser-Based)&lt;/h3&gt;
&lt;p&gt;Use ChatGPT when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;architecture planning and tradeoff exploration,&lt;/li&gt;
&lt;li&gt;longer back-and-forth refinement,&lt;/li&gt;
&lt;li&gt;code review reasoning and risk analysis,&lt;/li&gt;
&lt;li&gt;learning support (“explain why this approach is safer”).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tactic:&lt;/strong&gt; Provide the “shape” of the system, then iterate. Example:&lt;br&gt;
“Here’s our module layout and constraints. Propose a design, then critique it and suggest an alternative that improves testability.”&lt;/p&gt;
&lt;h3 id="the-best-hybrid-loop"&gt;The Best Hybrid Loop&lt;/h3&gt;
&lt;p&gt;A workflow that repeatedly worked during my test run:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ChatGPT for plan&lt;/strong&gt;: define architecture, validate assumptions, list edge cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Copilot Chat for execution&lt;/strong&gt;: implement the plan in the IDE, adjust to local code realities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests as the arbiter&lt;/strong&gt;: whatever the AI says, tests determine truth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One final ChatGPT pass for review&lt;/strong&gt;: ask for risk assessment and improvements.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This loop turns AI from “code generator” into “software partner”—planning plus execution.&lt;/p&gt;
&lt;h2 id="dont-trust-outputinstrument-it"&gt;Don’t Trust Output—Instrument It&lt;/h2&gt;
&lt;p&gt;If you want the tools to “ship better code,” you have to pair them with verification. In my runs, the highest-confidence improvements weren’t the ones that looked polished—they were the ones that were &lt;strong&gt;validated quickly&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Concrete advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Require minimal diffs&lt;/strong&gt; for debugging: change the smallest area first, then expand only if tests demand it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use property-based or boundary-focused tests&lt;/strong&gt; when you ask for “edge case handling.” Don’t accept generic “handles errors” language—write tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask for test updates alongside code changes&lt;/strong&gt;, especially for refactors. If the tool can’t explain what tests would change, it’s probably guessing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For code review tasks, ask for failure modes&lt;/strong&gt;: “What would you expect to break in production if this ships?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;AI is best treated like a junior engineer who can draft quickly—but who needs your oversight and your test suite.&lt;/p&gt;
&lt;h2 id="conclusion-copilot-ships-locally-chatgpt-thinks-globallybut-both-need-you-to-finish"&gt;Conclusion: Copilot Ships Locally, ChatGPT Thinks Globally—but Both Need You to Finish&lt;/h2&gt;
&lt;p&gt;After 30 real tasks, the verdict is clear and a little deflating: &lt;strong&gt;Copilot Chat wins for in-context code modification&lt;/strong&gt; because it operates inside your workspace. &lt;strong&gt;ChatGPT wins for architectural discussions and learning&lt;/strong&gt; because it excels at long-form dialogue and reasoning. The surprise is that &lt;strong&gt;neither is reliably better at first-pass code generation&lt;/strong&gt;—both often require meaningful edits before they become correct, consistent, and review-ready.&lt;/p&gt;
&lt;p&gt;The best outcome isn’t choosing one tool. It’s using both the right way: ChatGPT to shape the design and challenge assumptions, Copilot Chat to implement changes that fit your codebase, and tests to decide what ships.&lt;/p&gt;</content></item><item><title>The State of Containers in 2023: Beyond Docker Compose</title><link>https://decastro.work/blog/state-of-containers-2023-beyond-docker-compose/</link><pubDate>Tue, 31 Oct 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/state-of-containers-2023-beyond-docker-compose/</guid><description>&lt;p&gt;For years, “containers” quietly meant “Docker Compose plus a Dockerfile.” In 2023, that shorthand is no longer enough—and that’s a good thing. The container ecosystem has unbundled into separate components with different responsibilities, trust boundaries, and performance characteristics. If you still think of containers as one product, you’ll keep hitting ceilings. If you understand the layers, you’ll build faster, secure better, and troubleshoot like a grown-up.&lt;/p&gt;
&lt;h2 id="docker-the-company-vs-docker-the-runtime-two-stories-that-dont-match"&gt;Docker the company vs. Docker the runtime: two stories that don’t match&lt;/h2&gt;
&lt;p&gt;Let’s start with the uncomfortable truth: “Docker” is both a company story and a runtime story, and they’re converging less than they used to. For many teams, Docker Desktop remains the smoothest path to running local containers, especially on developer laptops. Compose is still a practical way to coordinate services.&lt;/p&gt;</description><content>&lt;p&gt;For years, “containers” quietly meant “Docker Compose plus a Dockerfile.” In 2023, that shorthand is no longer enough—and that’s a good thing. The container ecosystem has unbundled into separate components with different responsibilities, trust boundaries, and performance characteristics. If you still think of containers as one product, you’ll keep hitting ceilings. If you understand the layers, you’ll build faster, secure better, and troubleshoot like a grown-up.&lt;/p&gt;
&lt;h2 id="docker-the-company-vs-docker-the-runtime-two-stories-that-dont-match"&gt;Docker the company vs. Docker the runtime: two stories that don’t match&lt;/h2&gt;
&lt;p&gt;Let’s start with the uncomfortable truth: “Docker” is both a company story and a runtime story, and they’re converging less than they used to. For many teams, Docker Desktop remains the smoothest path to running local containers, especially on developer laptops. Compose is still a practical way to coordinate services.&lt;/p&gt;
&lt;p&gt;But on the backend, reality diverges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some environments run workloads without a long-lived daemon.&lt;/li&gt;
&lt;li&gt;Kubernetes relies on a runtime layer that is not “Docker.”&lt;/li&gt;
&lt;li&gt;Image builds are often handled by builders that don’t care about Compose at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the real question isn’t “Are containers still Docker?” The real question is: &lt;em&gt;which layer are you actually using, and what assumptions did you inherit when you picked it?&lt;/em&gt; That’s where most teams start getting meaningful improvements.&lt;/p&gt;
&lt;h2 id="podman-rootless-by-default-and-why-it-changes-your-threat-model"&gt;Podman: rootless by default, and why it changes your threat model&lt;/h2&gt;
&lt;p&gt;Podman’s core value proposition is simple: you can run containers without a daemon, and you can do it rootless by default. That sounds like implementation detail—until you look at what it means for security and operations.&lt;/p&gt;
&lt;p&gt;In many Docker-based workflows, the daemon is a privileged point of control. When you run containers, you are indirectly asking that daemon to perform privileged operations on your behalf. Rootless operation flips the model: containers run with your user’s privileges, and the runtime is designed to work within those constraints.&lt;/p&gt;
&lt;h3 id="a-practical-example-local-dev-without-sudo-roulette"&gt;A practical example: local dev without “sudo roulette”&lt;/h3&gt;
&lt;p&gt;Imagine you’re developing a microservice that needs to write to a mounted directory and bind ports locally. With rootless runtimes, you’re less likely to reach for “just run it as root” when something goes sideways.&lt;/p&gt;
&lt;p&gt;Try this mental checklist when you evaluate rootless support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you need elevated privileges for file access, or can you use user-owned mounts?&lt;/li&gt;
&lt;li&gt;Are ports you expose compatible with your user namespace mapping (e.g., unprivileged ports)?&lt;/li&gt;
&lt;li&gt;When you map volumes, what does the container process actually get permission to do?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Podman doesn’t remove all complexity, but it forces you to confront privilege boundaries earlier. That’s exactly what you want. Better to discover “this workload shouldn’t need root” on day one than after a production incident.&lt;/p&gt;
&lt;h2 id="containerd-the-runtime-engine-kubernetes-actually-speaks"&gt;containerd: the runtime engine Kubernetes actually speaks&lt;/h2&gt;
&lt;p&gt;If you’ve been building on Kubernetes for more than a minute, you already live in the world of containerd—whether you realized it or not. containerd is the runtime layer that executes container workloads. It’s the piece that sits “under” higher-level orchestration and starts the containers.&lt;/p&gt;
&lt;p&gt;This matters because performance and security troubleshooting often comes down to runtime mechanics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How images are unpacked and stored&lt;/li&gt;
&lt;li&gt;How namespaces and cgroups are configured&lt;/li&gt;
&lt;li&gt;What happens when pulls fail, signatures don’t validate, or filesystem mounts behave differently than expected&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-advice-learn-the-language-of-the-runtime-not-just-the-cli"&gt;Practical advice: learn the language of the runtime, not just the CLI&lt;/h3&gt;
&lt;p&gt;When something breaks, you don’t want to guess at the boundary between “Kubernetes says it’s running” and “the container actually started correctly.” Knowing that containerd is the executor helps you frame investigations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is the issue image-related (pull/build/artifact), or execution-related (runtime configuration)?&lt;/li&gt;
&lt;li&gt;Are you dealing with permissions, filesystem behavior, or networking?&lt;/li&gt;
&lt;li&gt;Do you see errors in runtime logs that align with pod lifecycle events?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you never touch containerd directly, internalizing that Kubernetes talks to a runtime makes your debugging more precise.&lt;/p&gt;
&lt;h2 id="buildkit-stop-thinking-of-image-builds-as-one-monolithic-step"&gt;BuildKit: stop thinking of image builds as one monolithic step&lt;/h2&gt;
&lt;p&gt;Compose and “docker build” are easy mental models. The catch is that building images has become a sophisticated pipeline: caching, parallelization, cross-stage optimization, and reproducibility are all different problems than “run a Dockerfile.”&lt;/p&gt;
&lt;p&gt;BuildKit is a builder designed to handle that complexity. In practice, the difference shows up when you care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fast rebuilds (especially in monorepos)&lt;/li&gt;
&lt;li&gt;Deterministic output (so deployments are traceable)&lt;/li&gt;
&lt;li&gt;Efficient multi-stage builds (so your runtime image isn’t bloated by build tooling)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="example-the-monorepo-trap"&gt;Example: the monorepo trap&lt;/h3&gt;
&lt;p&gt;If you build multiple services from a shared repo, naive Docker builds often invalidate cache too easily. One small change forces everything to rebuild, even if 90% of layers are unchanged.&lt;/p&gt;
&lt;p&gt;With a BuildKit-style mindset, you start designing builds for cache stability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Split dependency installation steps from source compilation steps&lt;/li&gt;
&lt;li&gt;Use multi-stage builds so only the final artifacts ship&lt;/li&gt;
&lt;li&gt;Structure Dockerfile stages so changes only invalidate what truly depends on them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t just faster—it’s a reliability upgrade. Fewer unnecessary rebuilds means fewer opportunities to introduce transient failures and fewer changes to inspect when something regresses.&lt;/p&gt;
&lt;h2 id="the-ecosystem-unbundled-why-containers-is-now-a-stack-you-must-understand"&gt;The ecosystem unbundled: why “containers” is now a stack you must understand&lt;/h2&gt;
&lt;p&gt;The most important shift in 2023 is that the container ecosystem isn’t a single thing anymore. It’s a set of cooperating components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Image building (e.g., BuildKit)&lt;/li&gt;
&lt;li&gt;Image storage and distribution (registries and content-addressed layers)&lt;/li&gt;
&lt;li&gt;Runtime execution (e.g., containerd)&lt;/li&gt;
&lt;li&gt;Service orchestration/compose tooling (e.g., Compose, or Kubernetes for real clusters)&lt;/li&gt;
&lt;li&gt;Security and trust tooling (signing, scanning, policy enforcement)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When these were bundled tightly, teams could “set and forget.” Now you need to know what each layer does and where security controls live.&lt;/p&gt;
&lt;h3 id="security-stop-assuming-you-get-it-because-youre-using-containers"&gt;Security: stop assuming you get it “because you’re using containers”&lt;/h3&gt;
&lt;p&gt;Security posture depends on where you enforce it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you verify image provenance/signatures before runtime?&lt;/li&gt;
&lt;li&gt;Do you scan the right artifacts—the final image you deploy, not just intermediates?&lt;/li&gt;
&lt;li&gt;Are runtime privileges minimized (rootless where appropriate)?&lt;/li&gt;
&lt;li&gt;Do you understand what the filesystem mounts allow inside the container?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The unbundling forces better hygiene. You can’t outsource security to a single vendor setting anymore. But you also gain flexibility: you can pick the components and policies that fit your risk profile.&lt;/p&gt;
&lt;h3 id="performance-measure-the-right-bottleneck"&gt;Performance: measure the right bottleneck&lt;/h3&gt;
&lt;p&gt;Performance issues often trace back to layer boundaries:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slow pulls? That’s registry/storage/network.&lt;/li&gt;
&lt;li&gt;Slow rebuilds? That’s build caching and Dockerfile structure.&lt;/li&gt;
&lt;li&gt;Slow startup? That’s runtime execution and filesystem/cgroup configuration.&lt;/li&gt;
&lt;li&gt;High resource usage? That’s app/container config plus how the runtime constrains it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A stack-aware mindset turns “containers are slow” into a concrete investigation plan.&lt;/p&gt;
&lt;h2 id="docker-desktop-as-an-on-rampand-a-curriculum-for-escaping-abstraction"&gt;Docker Desktop as an on-ramp—and a curriculum for escaping abstraction&lt;/h2&gt;
&lt;p&gt;Docker Desktop remains the easiest on-ramp. It’s a polished developer experience: it spins up what you need, abstracts away runtime details, and gets people shipping.&lt;/p&gt;
&lt;p&gt;But if you stay at that abstraction level forever, you’ll eventually hit friction in production. And production is where the layers matter: rootless behavior, runtime logs, image build caching, and cluster execution semantics.&lt;/p&gt;
&lt;h3 id="a-simple-learning-path-that-pays-off"&gt;A simple learning path that pays off&lt;/h3&gt;
&lt;p&gt;Here’s a practical approach that keeps your team productive while building real understanding:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use Docker Desktop for local iteration&lt;/strong&gt;, but treat it as a convenience, not a reference architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practice with Podman rootless&lt;/strong&gt; for at least a few workflows. Learn what changes when you don’t have a privileged daemon.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Become fluent in Kubernetes-runtime boundaries&lt;/strong&gt; by learning what containerd is doing when a pod starts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize a Dockerfile with BuildKit-style thinking&lt;/strong&gt;, even if you still invoke it via higher-level tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write down your security pipeline&lt;/strong&gt;: where scanning happens, where signing is verified, and what policy blocks deployment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is how you turn “containers” from a button-click into an engineering discipline.&lt;/p&gt;
&lt;h2 id="conclusion-the-future-isnt-docker-vs-not-dockerits-layered-competence"&gt;Conclusion: the future isn’t “Docker vs. not Docker”—it’s layered competence&lt;/h2&gt;
&lt;p&gt;The container world in 2023 rewards people who can describe the stack accurately. Docker Compose was a great starting point, but it shouldn’t be your endpoint. Podman changes privilege assumptions with rootless execution. containerd anchors Kubernetes runtime behavior. BuildKit modernizes image building as a performance and reliability tool. And the unbundling of the ecosystem means you must understand the layers to secure and optimize your systems.&lt;/p&gt;
&lt;p&gt;If you want to be a better infrastructure engineer, don’t fight the abstraction—learn what it hides. Then build with intention.&lt;/p&gt;</content></item><item><title>Drizzle ORM: SQL for People Who Actually Like SQL</title><link>https://decastro.work/blog/drizzle-orm-sql-for-people-who-like-sql/</link><pubDate>Thu, 19 Oct 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/drizzle-orm-sql-for-people-who-like-sql/</guid><description>&lt;p&gt;If you’ve ever sworn off ORMs after fighting “magic” query builders, you’re not alone. Many tools treat SQL as an implementation detail—something you’re supposed to forget. Drizzle ORM takes the opposite stance: it assumes you &lt;em&gt;want&lt;/em&gt; SQL, then adds the TypeScript safety you came for. The result is an ORM that feels less like a translation layer and more like a typed extension of your existing habits.&lt;/p&gt;
&lt;h2 id="sql-first-not-sql-avoidant"&gt;SQL-first, not SQL-avoidant&lt;/h2&gt;
&lt;p&gt;Most ORMs try to win you over with abstraction: models, relations, and a query API that claims to be “better” than SQL. But if you’re already thinking in joins, aggregations, and explicit where-clauses, that abstraction often becomes a second language you must constantly relearn.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever sworn off ORMs after fighting “magic” query builders, you’re not alone. Many tools treat SQL as an implementation detail—something you’re supposed to forget. Drizzle ORM takes the opposite stance: it assumes you &lt;em&gt;want&lt;/em&gt; SQL, then adds the TypeScript safety you came for. The result is an ORM that feels less like a translation layer and more like a typed extension of your existing habits.&lt;/p&gt;
&lt;h2 id="sql-first-not-sql-avoidant"&gt;SQL-first, not SQL-avoidant&lt;/h2&gt;
&lt;p&gt;Most ORMs try to win you over with abstraction: models, relations, and a query API that claims to be “better” than SQL. But if you’re already thinking in joins, aggregations, and explicit where-clauses, that abstraction often becomes a second language you must constantly relearn.&lt;/p&gt;
&lt;p&gt;Drizzle’s philosophy is refreshingly direct: the query builder is deliberately shaped like SQL. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;select&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, and &lt;code&gt;delete&lt;/code&gt; map cleanly to SQL concepts.&lt;/li&gt;
&lt;li&gt;Expressions look like the SQL you’d write by hand.&lt;/li&gt;
&lt;li&gt;Joins are modeled explicitly instead of being hidden behind implicit relation magic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, this matters when you’re debugging. When a query returns unexpected results, you can read the builder code and recognize the SQL structure immediately—because it’s built from SQL-shaped primitives, not an opaque DSL.&lt;/p&gt;
&lt;p&gt;Here’s a small taste of that “reads like SQL” feel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;results&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;from&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No guessing what “eq” does, no wondering whether the tool silently rewrites your query in a surprising way. If you know SQL, you can navigate Drizzle.&lt;/p&gt;
&lt;h2 id="typescript-inference-that-doesnt-fight-you"&gt;TypeScript inference that doesn’t fight you&lt;/h2&gt;
&lt;p&gt;The real selling point of any TypeScript ORM isn’t just code completion—it’s &lt;em&gt;type-driven correctness&lt;/em&gt;. Drizzle focuses on making types flow from your schema into your queries, so you get feedback at development time rather than at runtime.&lt;/p&gt;
&lt;p&gt;When you structure queries in a SQL-aligned way, TypeScript can infer what the result should look like. That unlocks a practical workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write a query that matches how you’d do it in SQL.&lt;/li&gt;
&lt;li&gt;Let the type system validate shape and fields.&lt;/li&gt;
&lt;li&gt;Avoid accidental field mismatches or mis-typed parameters.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, assume a schema roughly like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;users(id, email, name, createdAt)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;posts(id, userId, title, createdAt)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A relational query becomes readable and type-safe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;posts.title&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;authorEmail&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;users.email&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;from&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;innerJoin&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;userId&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;gt&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;createdAt&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;since&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice what’s happening: you’re explicitly choosing output fields and joining conditions. That’s SQL thinking. And because the output is declared, TypeScript can infer the returned object shape so you don’t end up dereferencing fields that don’t exist.&lt;/p&gt;
&lt;p&gt;This is the sweet spot: Drizzle doesn’t make you abandon SQL; it makes SQL safer.&lt;/p&gt;
&lt;h2 id="relational-queries-that-stay-readable-at-scale"&gt;Relational queries that stay readable at scale&lt;/h2&gt;
&lt;p&gt;Joins are where many ORMs either shine—or collapse into unreadable abstractions. Drizzle keeps joins explicit, which is a big deal once your queries stop being toy examples.&lt;/p&gt;
&lt;p&gt;Consider a common “feed” query: fetch recent posts with author info, filter by author, and order by recency. With SQL-shaped constructs, you can keep it structured:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;feed&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;db&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;postId&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;posts.id&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;posts.title&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;authorName&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;users.name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;authorEmail&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;users.email&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;createdAt&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;posts.createdAt&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;from&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;innerJoin&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;userId&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;users&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;userId&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;authorId&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orderBy&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;desc&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;posts&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;createdAt&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;limit&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;limit&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you’ve written SQL before, you’ll recognize every clause. If you haven’t, you still get a consistent pattern. That predictability reduces cognitive load—especially when team members rotate or when you revisit code months later.&lt;/p&gt;
&lt;p&gt;Practical advice: treat your query builder code like SQL you’d be proud to review. Keep joins explicit, name your selected fields intentionally, and avoid building huge queries by accident. If the query becomes complex, break it into subqueries or stepwise operations (with clear comments), the same way you’d do in hand-written SQL.&lt;/p&gt;
&lt;h2 id="migrations-the-boring-part-you-actually-want-done-well"&gt;Migrations: the boring part you actually want done well&lt;/h2&gt;
&lt;p&gt;No one writes migrations because they’re fun. We write them because production needs change management. Drizzle’s migration story keeps schema evolution part of the normal developer workflow—without turning it into a separate universe.&lt;/p&gt;
&lt;p&gt;You define tables and relationships in code, then generate migrations from that source of truth. That aligns with how SQL developers already think: schema changes should be inspectable, reviewable, and repeatable.&lt;/p&gt;
&lt;p&gt;A practical workflow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update your Drizzle schema definitions.&lt;/li&gt;
&lt;li&gt;Generate a migration.&lt;/li&gt;
&lt;li&gt;Review the migration SQL (yes, review it).&lt;/li&gt;
&lt;li&gt;Apply it in dev/staging; only then promote to production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last step is where SQL-first tools win. You’re not surrendering visibility. When something goes wrong, you can trace it to an explicit change you can read.&lt;/p&gt;
&lt;h2 id="edge-runtime-friendly-less-infrastructure-more-focus"&gt;Edge runtime friendly: less infrastructure, more focus&lt;/h2&gt;
&lt;p&gt;One of Drizzle’s underappreciated advantages is that it’s built to work broadly, including edge runtimes. The reason matters: you don’t need a separate “binary engine” process sitting between your app and the database. That reduces operational friction and makes deployment more portable.&lt;/p&gt;
&lt;p&gt;If you’re building Next.js or similar apps and you want your data access layer to run where your code runs, this is a concrete advantage—not a marketing bullet. It keeps your architecture simpler: fewer moving parts, fewer environment-specific surprises.&lt;/p&gt;
&lt;p&gt;Practical advice: when targeting edge, pay attention to connection handling and how you configure your database driver. The ORM won’t save you from runtime constraints, but a leaner setup does mean fewer platform-specific gotchas.&lt;/p&gt;
&lt;h2 id="when-drizzle-is-the-right-choiceand-when-it-isnt"&gt;When Drizzle is the right choice—and when it isn’t&lt;/h2&gt;
&lt;p&gt;Drizzle is best for teams that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;think in SQL already (or want to, and don’t want to be punished for it),&lt;/li&gt;
&lt;li&gt;value TypeScript inference for correctness,&lt;/li&gt;
&lt;li&gt;want explicit joins and predictable query shape,&lt;/li&gt;
&lt;li&gt;prefer readable code over opaque magic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s not ideal if your team’s goal is “never think about SQL.” If you want to model everything as a purely object-oriented graph and avoid joins entirely, an ORM that treats SQL as an implementation detail may feel more comfortable. But you’ll likely trade away transparency exactly when your queries get interesting.&lt;/p&gt;
&lt;p&gt;A more honest way to put it: Drizzle rewards developers who enjoy understanding their queries. If you’re the kind of person who writes &lt;code&gt;WHERE&lt;/code&gt; clauses with intention, you’ll feel at home.&lt;/p&gt;
&lt;h2 id="conclusion-typed-sql-you-can-trust"&gt;Conclusion: typed SQL you can trust&lt;/h2&gt;
&lt;p&gt;Drizzle ORM makes a compelling argument: you don’t need a new query language to get type safety. You need better alignment between your mental model (SQL) and your tooling (TypeScript inference, explicit relational queries, and migrations you can review).&lt;/p&gt;
&lt;p&gt;SQL doesn’t have to be the old way. With Drizzle, it can be the &lt;em&gt;right&lt;/em&gt; way—typed, maintainable, and readable enough that your future self won’t dread the next “why is this query slow?” moment.&lt;/p&gt;</content></item><item><title>The HTMX + Go Combination Is Absurdly Productive</title><link>https://decastro.work/blog/htmx-go-combination-absurdly-productive/</link><pubDate>Sat, 14 Oct 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/htmx-go-combination-absurdly-productive/</guid><description>&lt;p&gt;Web development loves to pretend it’s always reinventing the wheel, but the best stacks rarely feel “new”—they feel &lt;em&gt;useful&lt;/em&gt;. The HTMX + Go combo is exactly that: server-rendered HTML from a compiled binary, with interactivity layered on through hypermedia instead of an entire JavaScript ecosystem. It’s the kind of setup that makes you ship faster, deploy simpler, and keep your sanity.&lt;/p&gt;
&lt;h2 id="why-this-pairing-works-and-why-its-not-just-a-hack"&gt;Why this pairing works (and why it’s not just “a hack”)&lt;/h2&gt;
&lt;p&gt;HTMX and Go share a core philosophy: treat the server as the source of truth and the browser as an intelligent viewer—not a separate universe. HTMX lets you define interactive behavior using HTML attributes like &lt;code&gt;hx-get&lt;/code&gt;, &lt;code&gt;hx-post&lt;/code&gt;, and &lt;code&gt;hx-target&lt;/code&gt;. When something changes, the browser asks the server for a fragment of HTML and swaps it into the page.&lt;/p&gt;</description><content>&lt;p&gt;Web development loves to pretend it’s always reinventing the wheel, but the best stacks rarely feel “new”—they feel &lt;em&gt;useful&lt;/em&gt;. The HTMX + Go combo is exactly that: server-rendered HTML from a compiled binary, with interactivity layered on through hypermedia instead of an entire JavaScript ecosystem. It’s the kind of setup that makes you ship faster, deploy simpler, and keep your sanity.&lt;/p&gt;
&lt;h2 id="why-this-pairing-works-and-why-its-not-just-a-hack"&gt;Why this pairing works (and why it’s not just “a hack”)&lt;/h2&gt;
&lt;p&gt;HTMX and Go share a core philosophy: treat the server as the source of truth and the browser as an intelligent viewer—not a separate universe. HTMX lets you define interactive behavior using HTML attributes like &lt;code&gt;hx-get&lt;/code&gt;, &lt;code&gt;hx-post&lt;/code&gt;, and &lt;code&gt;hx-target&lt;/code&gt;. When something changes, the browser asks the server for a fragment of HTML and swaps it into the page.&lt;/p&gt;
&lt;p&gt;Go, meanwhile, excels at building fast, reliable server processes. With &lt;code&gt;html/template&lt;/code&gt;, you can render secure, composable HTML on the backend. Put differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go renders HTML&lt;/strong&gt; (full pages and fragments) from real data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTMX turns user actions into HTTP requests&lt;/strong&gt; and swaps HTML responses into the DOM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your UI becomes a set of endpoints&lt;/strong&gt;, not a client-side application that must be kept in sync forever.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t a fragile “server-rendering with training wheels” approach. It’s a straightforward hypermedia workflow that stays coherent as your product grows.&lt;/p&gt;
&lt;h2 id="the-development-loop-less-framework-churn-more-shipping"&gt;The development loop: less framework churn, more shipping&lt;/h2&gt;
&lt;p&gt;If you’ve built CRUD tools with modern frontend frameworks, you already know the pattern: you spend time wiring state management, handling loading/error states everywhere, and chasing version compatibility. Even when the app is simple, the &lt;em&gt;ecosystem cost&lt;/em&gt; is real—node versions, dependency updates, build steps, and “why is this rerendering infinitely?” debugging.&lt;/p&gt;
&lt;p&gt;With HTMX + Go, the loop becomes brutally direct:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add a route in Go (e.g., &lt;code&gt;POST /users&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Render a template or partial for the response.&lt;/li&gt;
&lt;li&gt;Add an &lt;code&gt;hx-&lt;/code&gt; attribute to the HTML element that should trigger the request.&lt;/li&gt;
&lt;li&gt;Swap the response into the right part of the page.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="a-concrete-example-inline-user-updates"&gt;A concrete example: inline user updates&lt;/h3&gt;
&lt;p&gt;Imagine an admin page listing users. Each row has a “Change role” dropdown and a “Save” button. With HTMX, you can make that feel interactive without building a SPA:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;tr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;{{.Name}}&amp;lt;/&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;select&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;role&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;role-{{.ID}}&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;option&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;selected&lt;/span&gt;&amp;gt;{{.Role}}&amp;lt;/&lt;span style="color:#f92672"&gt;option&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;option&lt;/span&gt;&amp;gt;admin&amp;lt;/&lt;span style="color:#f92672"&gt;option&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;option&lt;/span&gt;&amp;gt;viewer&amp;lt;/&lt;span style="color:#f92672"&gt;option&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;select&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-post&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/admin/users/{{.ID}}/role&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-target&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;#user-{{.ID}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-swap&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;outerHTML&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Save
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;td&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;tr&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The server receives the POST, updates the role, and returns updated HTML for the row (or the whole table, if that’s simpler). No client-side state machine. No bespoke API client. No separate JSON-to-UI mapping layer.&lt;/p&gt;
&lt;p&gt;The “absurdly productive” part isn’t just that it’s fast—it’s that the &lt;em&gt;mental overhead stays low&lt;/em&gt; as features accumulate.&lt;/p&gt;
&lt;h2 id="the-deployment-story-one-binary-anywhere"&gt;The deployment story: one binary, anywhere&lt;/h2&gt;
&lt;p&gt;This stack collapses your infrastructure surface area. Instead of a Node build step, a frontend artifact pipeline, and a backend service with two different runtimes, you ship &lt;strong&gt;one compiled Go binary&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No node_modules&lt;/strong&gt; on the server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No frontend build step&lt;/strong&gt; in your CI/CD for every deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer runtime dependencies&lt;/strong&gt; to lock down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One container or one executable&lt;/strong&gt; that runs the same way locally, in staging, and in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For internal tools—dashboards, operators’ consoles, back-office CRUD—this matters more than people admit. You’re not trying to impress a build pipeline. You’re trying to deploy reliably.&lt;/p&gt;
&lt;p&gt;And because the UI is server-rendered, your “homepage” is not an empty skeleton waiting for JavaScript to finish booting. It’s HTML on first response. That makes behavior more predictable, especially under load or flaky networks.&lt;/p&gt;
&lt;h2 id="performance-concurrency-without-contortions"&gt;Performance: concurrency without contortions&lt;/h2&gt;
&lt;p&gt;There’s a temptation to equate “server-rendered HTML” with “slow.” In practice, with Go and HTMX, performance is often better than you’d expect for this class of apps—because you’re not asking the client to load and execute a heavy UI bundle, and you’re not shipping a ton of client logic just to do form posts.&lt;/p&gt;
&lt;p&gt;HTMX interaction is also inherently incremental. If a button click only needs to refresh one component, your request can return a targeted fragment, not an entire application state dump.&lt;/p&gt;
&lt;p&gt;Practical tips to keep things snappy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Render fragments intentionally&lt;/strong&gt;: use &lt;code&gt;hx-target&lt;/code&gt; and return HTML that matches the target’s structure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid chatty endpoints&lt;/strong&gt;: if multiple fields need validation feedback, consider returning a chunk containing all messages at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache what’s stable&lt;/strong&gt;: for pages where parts don’t change often (e.g., navigation, reference data), reuse computed fragments or templates where appropriate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep templates composable&lt;/strong&gt;: &lt;code&gt;template.ParseFiles&lt;/code&gt; / template definitions let you reuse markup and avoid copy-paste bloat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The big win is that you can move fast without turning performance into a rewrite later. The architecture supports incremental improvements instead of forcing a “done when it’s done” SPA optimization scramble.&lt;/p&gt;
&lt;h2 id="ux-without-a-javascript-framework-its-still-modern"&gt;UX without a JavaScript framework: it’s still modern&lt;/h2&gt;
&lt;p&gt;“Interactive without JavaScript” sounds like a limitation until you use it. HTMX gives you the tools to create real UX flows using plain HTML:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Submit forms asynchronously (&lt;code&gt;hx-post&lt;/code&gt;, &lt;code&gt;hx-put&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Replace parts of the page (&lt;code&gt;hx-swap&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Trigger requests on events (&lt;code&gt;hx-trigger&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Show confirmations or incremental prompts&lt;/li&gt;
&lt;li&gt;Validate and re-render server-side error states&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="error-handling-that-actually-works"&gt;Error handling that actually works&lt;/h3&gt;
&lt;p&gt;In a traditional API + frontend setup, form errors are often treated as a second system: the server returns JSON, the client maps it to UI, and everything can desync. With server-rendered templates, error states are just another HTML response.&lt;/p&gt;
&lt;p&gt;For instance, if &lt;code&gt;POST /admin/users/:id/role&lt;/code&gt; fails validation, your handler can render the same fragment with error messages included. The browser swaps it into place and the user stays in context.&lt;/p&gt;
&lt;p&gt;That’s “modern UX” in the most practical sense: predictable feedback, fewer edge cases, and fewer moving parts.&lt;/p&gt;
&lt;h2 id="when-you-should-and-shouldnt-choose-this-stack"&gt;When you should (and shouldn’t) choose this stack&lt;/h2&gt;
&lt;p&gt;This combination is ideal for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Internal tools&lt;/strong&gt; (admin panels, operations consoles, dashboards)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRUD-heavy applications&lt;/strong&gt; (users, records, approvals, workflows)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Form-centric products&lt;/strong&gt; (multi-step data entry, review screens)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Projects where speed-to-ship beats “maximal interactivity”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You should be more cautious when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Highly complex client-side state and animation&lt;/strong&gt; (think spreadsheet-grade interactions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time collaborative editing&lt;/strong&gt; with heavy synchronization logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline-first experiences&lt;/strong&gt; where the client must function without server access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even then, you can often still use HTMX for the majority of pages and reserve specialized client-side code for the handful of truly interactive screens. The point is not purity; it’s momentum.&lt;/p&gt;
&lt;h2 id="conclusion-the-simplest-architecture-that-keeps-winning"&gt;Conclusion: the simplest architecture that keeps winning&lt;/h2&gt;
&lt;p&gt;HTMX + Go is absurdly productive because it removes the two biggest sources of friction in typical web development: the client framework ecosystem and the state synchronization burden it creates. You render HTML where it belongs, make interactions HTTP-driven, and ship a single compiled binary that deploys anywhere.&lt;/p&gt;
&lt;p&gt;If you’re building an internal tool or a CRUD application and you want to stop wrestling with frontend churn, this is one of the rare stacks that makes “fast to develop” and “fast to deploy” feel like the same thing.&lt;/p&gt;</content></item><item><title>The JavaScript Build Tool Graveyard: A History of Things We Overcomplicated</title><link>https://decastro.work/blog/javascript-build-tool-graveyard-overcomplicated/</link><pubDate>Sat, 07 Oct 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/javascript-build-tool-graveyard-overcomplicated/</guid><description>&lt;p&gt;For a decade, JavaScript developers haven’t just built apps—we’ve built build systems. And then, inevitably, we tore them down. Grunt, Gulp, Webpack, then more layers around Webpack, and now Vite and esbuild: each new tool arrives with the same pitch—&lt;em&gt;this one will finally fix complexity.&lt;/em&gt; The joke is that the promise never really changes. The real lesson is staring us in the face: we keep optimizing the wrong thing.&lt;/p&gt;
&lt;h2 id="the-recurring-villain-complexity-you-have-to-configure"&gt;The recurring villain: complexity you have to configure&lt;/h2&gt;
&lt;p&gt;The story usually starts the same way. A team wants “modern tooling,” which quickly becomes “a build pipeline.” The pipeline needs bundling, transpilation, minification, CSS handling, image optimization, environment variables, live reload, testing integration, linting, type checking, and a development server that won’t make you scream.&lt;/p&gt;</description><content>&lt;p&gt;For a decade, JavaScript developers haven’t just built apps—we’ve built build systems. And then, inevitably, we tore them down. Grunt, Gulp, Webpack, then more layers around Webpack, and now Vite and esbuild: each new tool arrives with the same pitch—&lt;em&gt;this one will finally fix complexity.&lt;/em&gt; The joke is that the promise never really changes. The real lesson is staring us in the face: we keep optimizing the wrong thing.&lt;/p&gt;
&lt;h2 id="the-recurring-villain-complexity-you-have-to-configure"&gt;The recurring villain: complexity you have to configure&lt;/h2&gt;
&lt;p&gt;The story usually starts the same way. A team wants “modern tooling,” which quickly becomes “a build pipeline.” The pipeline needs bundling, transpilation, minification, CSS handling, image optimization, environment variables, live reload, testing integration, linting, type checking, and a development server that won’t make you scream.&lt;/p&gt;
&lt;p&gt;So the team picks a tool.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Grunt&lt;/strong&gt; arrives with task runners and a declarative config file that feels like control.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gulp&lt;/strong&gt; arrives with streams and a sense of speed—write JavaScript instead of YAML.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Webpack&lt;/strong&gt; arrives with a graph-centric worldview: everything becomes a dependency graph, so how could it ever be messy?&lt;/li&gt;
&lt;li&gt;Then the ecosystem grows: plugins, loaders, presets, dev server wrappers, custom scripts, and “just one more config tweak.”&lt;/li&gt;
&lt;li&gt;Eventually, &lt;strong&gt;Vite&lt;/strong&gt; and &lt;strong&gt;esbuild&lt;/strong&gt; show up: fewer layers, faster startup, less ceremony.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But notice the through-line. Every generation claims to reduce complexity, yet complexity reappears in a new costume. You don’t eliminate build complexity—you relocate it.&lt;/p&gt;
&lt;p&gt;In practice, that relocation often looks like this: the build script you just simplified now lives in a different place. Webpack complexity moves into loader/plugin choices. Gulp complexity moves into stream piping and edge cases. The “modern” tool replaces one set of configuration problems with another.&lt;/p&gt;
&lt;h2 id="grunt-to-gulp-when-automation-turned-into-another-job"&gt;Grunt to Gulp: when “automation” turned into another job&lt;/h2&gt;
&lt;p&gt;Grunt’s appeal was obvious: it offered a structured way to run tasks. If your build process was a series of commands, Grunt gave you a place to orchestrate them—minify, compile, copy, watch. The configuration style was friendly to people who wanted “knobs” and predictable execution.&lt;/p&gt;
&lt;p&gt;Then Gulp arrived and asked a tempting question: why not write the pipeline in JavaScript? Gulp’s stream-based approach felt more flexible, more “real code,” and easier to customize.&lt;/p&gt;
&lt;p&gt;Here’s the practical reality both tools eventually hit: once your build steps include branching logic, conditional environments, cross-platform path handling, and watch mode quirks, your build file becomes a mini application. It needs testing. It needs documentation. It needs someone to understand why task ordering matters.&lt;/p&gt;
&lt;p&gt;Even “simple” things become surprisingly political. Example: suppose you copy static assets from &lt;code&gt;src/assets&lt;/code&gt; to &lt;code&gt;dist/assets&lt;/code&gt;. In Grunt, that’s a task config. In Gulp, it’s a pipeline stage. In both cases, you eventually discover a subtle bug where assets are copied before compilation finishes—or overwritten by a later step.&lt;/p&gt;
&lt;p&gt;The build step isn’t just glue. It becomes the system.&lt;/p&gt;
&lt;h2 id="webpack-the-promise-of-a-single-tool-the-birth-of-an-ecosystem"&gt;Webpack: the promise of a single tool, the birth of an ecosystem&lt;/h2&gt;
&lt;p&gt;Webpack’s genius was conceptual: treat your project as a dependency graph and let the bundler do the heavy lifting. For many teams, it replaced a mess of scripts and task runners with one coherent engine. You could express “what depends on what,” and webpack would figure out the order.&lt;/p&gt;
&lt;p&gt;But the moment you introduce a graph-based bundler, you also introduce a new class of complexity: the boundary between “source code” and “build-time transformation.”&lt;/p&gt;
&lt;p&gt;Webpack is where transformation lives—transpiling, bundling, injecting globals, handling CSS modules, rewriting URLs, optimizing production output, and dealing with runtime differences. To make all of that possible, webpack leaned into loaders and plugins.&lt;/p&gt;
&lt;p&gt;Loaders are where projects tend to sprawl. A team starts with &lt;code&gt;babel-loader&lt;/code&gt; and a CSS loader. Then they need special handling for images, fonts, SVGs, and environment-specific config. Soon, you’re juggling a lineup of loaders and plugin combinations, each with its own failure modes.&lt;/p&gt;
&lt;p&gt;This is the hidden cost of an ecosystem model: the upgrade path becomes part of your build tooling. A team doesn’t just upgrade dependencies; they upgrade a delicate choreography of plugins that may not change together.&lt;/p&gt;
&lt;p&gt;Webpack also encouraged “everything in the bundle” thinking. That’s sensible until your bundle starts acting like a second product: performance tuning, chunk splitting, caching strategies, and “why is this module duplicated” mysteries.&lt;/p&gt;
&lt;p&gt;It worked. It also taught teams a habit: treat configuration complexity as an acceptable tax for shipping. Once you accept that tax, it grows.&lt;/p&gt;
&lt;h2 id="the-modern-shift-vite-esbuild-and-the-appeal-of-fewer-layers"&gt;The modern shift: Vite, esbuild, and the appeal of fewer layers&lt;/h2&gt;
&lt;p&gt;Vite’s pitch is brutally practical: don’t do work you don’t need. During development, it avoids the full bundling pipeline and leans on native module support (plus smart transforms). In production, it can still build optimized output, but the key idea is to keep the dev loop lean.&lt;/p&gt;
&lt;p&gt;esbuild takes the “do the minimum work fast” philosophy even further. Where webpack is built around the bundling graph and transformation pipeline, esbuild is built for speed and simplicity in transformation. It’s the opposite of “we’ll solve everything with a single heavyweight system.” It tries to solve the hot path: turning modern syntax into something your runtime understands.&lt;/p&gt;
&lt;p&gt;What changes psychologically is the &lt;em&gt;shape&lt;/em&gt; of the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of a build system that dominates the repository, you get a smaller set of build responsibilities.&lt;/li&gt;
&lt;li&gt;Instead of a build config that feels like infrastructure code, you get a build config that feels like setup.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You still need tooling. Linting doesn’t disappear. Testing doesn’t disappear. You still need bundling for many deployment targets. But the closer you get to native ESM in browsers and Node.js, the less you have to fake the module system with elaborate bundling and shimming.&lt;/p&gt;
&lt;p&gt;And that’s the deeper point: modern tools aren’t just “better versions of webpack.” They’re concessions to reality—ESM support, improved runtime compatibility, and the desire to make development feel like editing, not waiting.&lt;/p&gt;
&lt;h2 id="the-uncomfortable-conclusion-the-build-step-is-the-problem"&gt;The uncomfortable conclusion: the build step is the problem&lt;/h2&gt;
&lt;p&gt;It’s fashionable to blame tools. “Webpack is too complex.” “Gulp was hard to debug.” “Grunt felt too rigid.” Those complaints are often real. But the recurring behavior is the same: the build step becomes a place where we hide mismatches between how code runs and how code is authored.&lt;/p&gt;
&lt;p&gt;The build step grows because we insist on creating a universal runtime story out of limited one-size-fits-all assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browsers don’t always support the exact syntax we like.&lt;/li&gt;
&lt;li&gt;Node and browsers don’t always agree on module formats.&lt;/li&gt;
&lt;li&gt;APIs differ across environments.&lt;/li&gt;
&lt;li&gt;Performance constraints push us toward bundling and chunking strategies.&lt;/li&gt;
&lt;li&gt;Teams want the same local workflow for everything, including deployment differences.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every time you add a mismatch, you add a transformation. Every transformation introduces edge cases, configuration, and integration surface.&lt;/p&gt;
&lt;p&gt;So the build system isn’t merely a tool chain—it’s a patch. And patches accumulate.&lt;/p&gt;
&lt;p&gt;The more aggressively you pursue “shipping without a build step,” the more you force the ecosystem to meet you where you are. Native ESM, import maps (where appropriate), consistent module formats, and runtime-aligned development patterns reduce the need for heavy transformation.&lt;/p&gt;
&lt;p&gt;You don’t have to go all-in on “no build ever.” But you should treat the build step as a last resort, not a default lifestyle.&lt;/p&gt;
&lt;h2 id="practical-guidance-designing-a-toolchain-that-wont-rot"&gt;Practical guidance: designing a toolchain that won’t rot&lt;/h2&gt;
&lt;p&gt;If you’re starting fresh—or rescuing an existing project—here’s what I’d do, opinionated and practical:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separate dev workflow from production bundling.&lt;/strong&gt;&lt;br&gt;
The dev experience should be fast and predictable. Production can be more involved. Tools like Vite exist because development should not pay the full cost of production bundling every time you save a file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Minimize transformation.&lt;/strong&gt;&lt;br&gt;
If you don’t need to transpile, don’t. If you can target modern runtimes, do. Every transformation stage is a future maintenance obligation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Keep configuration close to intent.&lt;/strong&gt;&lt;br&gt;
If a config file becomes a program—with lots of custom logic—refactor it into fewer, well-understood conventions. “A messy but centralized webpack config” often becomes “a specialized snowflake build system.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Prefer fewer plugins, fewer loaders, and simpler graphs.&lt;/strong&gt;&lt;br&gt;
If you can replace a plugin chain with a single supported option or a smaller set of tools, do it. The goal is not minimal dependencies; it’s reduced integration complexity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat upgrades as part of engineering, not a surprise.&lt;/strong&gt;&lt;br&gt;
Pin what you must, update intentionally, and document why you chose each piece. The “graveyard” isn’t just tools—it’s teams who upgraded without understanding the coupling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Align runtime with authoring.&lt;/strong&gt;&lt;br&gt;
Use the module formats your runtime actually supports. Target modern environments when you can. If your code and deployment target speak the same language, your build step shrinks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Measure the real pain: build time and debugging time, not just “tool popularity.”&lt;/strong&gt;&lt;br&gt;
A tool can be “modern” and still slow your loop or make errors harder to interpret. Optimize for your team’s daily friction.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you follow these rules, you won’t avoid build steps entirely—but you’ll avoid the specific trap that created the graveyard: treating build tooling as an endless arms race.&lt;/p&gt;
&lt;h2 id="conclusion-fewer-steps-fewer-excuses"&gt;Conclusion: fewer steps, fewer excuses&lt;/h2&gt;
&lt;p&gt;The JavaScript build tool graveyard is less about tool choice and more about a recurring self-inflicted wound: we keep trying to solve the complexity created by the build step using more elaborate build steps. Grunt to Gulp to Webpack to Vite reflects the same emotional loop—&lt;em&gt;the old way is too hard; the new tool will simplify everything.&lt;/em&gt; Sometimes it does. Often it just moves the complexity.&lt;/p&gt;
&lt;p&gt;The path forward is to reduce the need for transformation and embrace native module realities where possible. The best build system is the one that doesn’t have to do much.&lt;/p&gt;</content></item><item><title>You Should Be Pair Programming with AI, Not Copy-Pasting from It</title><link>https://decastro.work/blog/pair-programming-with-ai-not-copy-pasting/</link><pubDate>Mon, 25 Sep 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/pair-programming-with-ai-not-copy-pasting/</guid><description>&lt;p&gt;It’s tempting to treat an LLM like a search engine with amnesia: paste your prompt, grab the answer, move on. But that workflow—copy, paste, hope—quietly drains your engineering judgment. If you want better code, faster, you need to change how you use AI: don’t outsource thinking. Pair with it.&lt;/p&gt;
&lt;p&gt;Most developers are using ChatGPT like Stack Overflow: “Here’s my problem. Here’s your solution.” The result is brittle code, mismatched architecture, and a growing sense that you’re shipping faster without actually getting better. The fix is not a new model or a magic prompt. It’s an iterative collaboration style—AI as a thought partner, not a clipboard.&lt;/p&gt;</description><content>&lt;p&gt;It’s tempting to treat an LLM like a search engine with amnesia: paste your prompt, grab the answer, move on. But that workflow—copy, paste, hope—quietly drains your engineering judgment. If you want better code, faster, you need to change how you use AI: don’t outsource thinking. Pair with it.&lt;/p&gt;
&lt;p&gt;Most developers are using ChatGPT like Stack Overflow: “Here’s my problem. Here’s your solution.” The result is brittle code, mismatched architecture, and a growing sense that you’re shipping faster without actually getting better. The fix is not a new model or a magic prompt. It’s an iterative collaboration style—AI as a thought partner, not a clipboard.&lt;/p&gt;
&lt;h2 id="stop-treating-the-llm-like-a-clipboard"&gt;Stop Treating the LLM Like a Clipboard&lt;/h2&gt;
&lt;p&gt;Copy-pasting an AI-generated snippet feels productive because it’s immediate. You get code without the tedium. But it’s also exactly how bugs multiply: you accept an answer before you understand its assumptions.&lt;/p&gt;
&lt;p&gt;Here’s a typical pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You paste an error message and a vague description.&lt;/li&gt;
&lt;li&gt;The model returns a polished-looking solution.&lt;/li&gt;
&lt;li&gt;You integrate it quickly.&lt;/li&gt;
&lt;li&gt;Then you discover edge cases, security issues, performance regressions, or architectural mismatch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you copy-paste, you rarely get to ask the most important questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why is this the right approach?&lt;/li&gt;
&lt;li&gt;What trade-offs are being made?&lt;/li&gt;
&lt;li&gt;What failure modes should I plan for?&lt;/li&gt;
&lt;li&gt;How does this fit into my system’s constraints?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The model can’t optimize for your production reality if you never interrogate it. And you can’t improve your own judgment if you never practice decision-making.&lt;/p&gt;
&lt;h2 id="use-ai-like-a-pair-programmer-dialogue-over-dumping"&gt;Use AI Like a Pair Programmer: Dialogue Over Dumping&lt;/h2&gt;
&lt;p&gt;Pair programming has a rhythm: you explain what you’re doing, your partner questions it, you adjust, and you converge. An LLM can mimic that rhythm—if you give it the context and the permission to critique.&lt;/p&gt;
&lt;p&gt;The key difference is input shape. Instead of sending a problem and asking for “the answer,” you send:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Your architecture&lt;/strong&gt; (or the piece you’re working on)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your constraints&lt;/strong&gt; (latency, scaling, data model, security requirements)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your current approach&lt;/strong&gt; (even if it’s rough)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your questions&lt;/strong&gt; (what you want challenged)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your acceptance criteria&lt;/strong&gt; (what “good” looks like)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Think of your prompt as a mini design doc plus a review request. Then run an iteration loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask it to critique.&lt;/li&gt;
&lt;li&gt;Challenge its suggestions.&lt;/li&gt;
&lt;li&gt;Request alternatives.&lt;/li&gt;
&lt;li&gt;Synthesize a final plan.&lt;/li&gt;
&lt;li&gt;Only then ask for code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s how you avoid the “snippet spiral,” where each pasted chunk introduces new unknowns.&lt;/p&gt;
&lt;h2 id="a-practical-workflow-critique-counter-example-alternative"&gt;A Practical Workflow: Critique, Counter-Example, Alternative&lt;/h2&gt;
&lt;p&gt;Let’s make this concrete with a common engineering task: implementing an API endpoint that performs a database-backed search.&lt;/p&gt;
&lt;h3 id="bad-use-paste-code"&gt;Bad use: “Paste code”&lt;/h3&gt;
&lt;p&gt;You might do something like:&lt;br&gt;
“Write a Node.js endpoint to search products by keyword. Use SQL.”&lt;/p&gt;
&lt;p&gt;The model responds with a plausible implementation. You paste it. It compiles. Then your logs tell you it’s slow, ignores indexing strategy, and mishandles special characters.&lt;/p&gt;
&lt;h3 id="better-use-pair-and-pressure-test"&gt;Better use: “Pair and pressure test”&lt;/h3&gt;
&lt;p&gt;Instead, start with context:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We have a PostgreSQL table &lt;code&gt;products(id, name, description, updated_at)&lt;/code&gt;. We need a &lt;code&gt;/search&lt;/code&gt; endpoint. Constraints: response under 200ms at p95 for common queries, must be SQL-injection safe, and we must support pagination with stable ordering. Current plan: use &lt;code&gt;ILIKE&lt;/code&gt; on &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, paginate by &lt;code&gt;updated_at DESC, id DESC&lt;/code&gt;. Critique this plan: performance risks, correctness risks, and edge cases. Suggest at least two alternatives and tell me when each alternative wins.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now the model is forced to reason about trade-offs. You can further challenge it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Counter-argument: We can’t deploy a full text search engine right now. Can you propose a low-risk incremental approach that improves latency using only Postgres features we already have? Also, what indices would you add, and what query shape would you use?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Only after you’ve locked the approach, you ask for implementation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Given the chosen approach, write the query and the endpoint code. Follow these acceptance criteria: parameterized queries only, deterministic pagination, and explain how it prevents injection and how it handles empty search terms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This workflow turns AI into a reviewer who cares about your specific system, not a random code generator.&lt;/p&gt;
&lt;h2 id="prompt-like-youre-designing-provide-inputs-ask-for-evaluation"&gt;Prompt Like You’re Designing: Provide Inputs, Ask for Evaluation&lt;/h2&gt;
&lt;p&gt;Quality prompts aren’t “more text.” They’re &lt;em&gt;better structure&lt;/em&gt;. Aim for prompts that force the model into roles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architect&lt;/strong&gt;: “Propose a design that meets these constraints.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviewer&lt;/strong&gt;: “Critique for correctness, security, and performance.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skeptic&lt;/strong&gt;: “What would break in production? Where are the hidden assumptions?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Planner&lt;/strong&gt;: “What are the next steps and tests I should write?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opponent&lt;/strong&gt;: “Argue against your own solution.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, give the model acceptance criteria. Vague instructions produce vague code. Concrete criteria produce usable outcomes.&lt;/p&gt;
&lt;p&gt;For example, instead of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Make it fast.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Benchmark expectation: keep the query under 150ms for queries containing 3+ keywords; add an index plan; avoid leading wildcards.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Secure it.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“No dynamic SQL string concatenation; validate inputs; use parameter binding; discuss how the approach handles UTF-8 and user-supplied wildcards.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then—critically—ask for failure modes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;List the top five production failure modes for this design and how we’d detect and mitigate them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That single line changes everything. It nudges the model away from “it works on my machine” and toward operational thinking.&lt;/p&gt;
&lt;h2 id="generate-alternatives-then-synthesizedont-choose-the-first-answer"&gt;Generate Alternatives, Then Synthesize—Don’t Choose the First Answer&lt;/h2&gt;
&lt;p&gt;One reason copy-pasting works “well enough” is that the first solution often looks reasonable. The danger is that you’re accepting a single point of view—usually the model’s default.&lt;/p&gt;
&lt;p&gt;A better approach is to explicitly request multiple strategies and then decide.&lt;/p&gt;
&lt;p&gt;Try a prompt like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Provide three alternative designs for this feature. For each: pros, cons, complexity, risks, and what I should test in staging. Assume we have limited time for refactors.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then you synthesize:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Based on our constraints (X, Y, Z), I’m choosing design B. Explain any remaining risks and suggest a test plan to validate it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This “alternatives → decision → verification” cycle is the core of engineering. AI shouldn’t replace it—it should accelerate it.&lt;/p&gt;
&lt;h2 id="treat-ai-code-as-a-draft-that-must-earn-its-place"&gt;Treat AI Code as a Draft That Must Earn Its Place&lt;/h2&gt;
&lt;p&gt;Even with excellent prompting, LLM output is still a draft. The goal is not to eliminate manual work; it’s to make the manual work smarter.&lt;/p&gt;
&lt;p&gt;So, when you request code, also request guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Invariants&lt;/strong&gt;: “List invariants the code must maintain.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt;: “Handle empty inputs, pagination boundaries, and null values.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests&lt;/strong&gt;: “Generate unit tests and integration tests for the failure modes you identified.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complexity&lt;/strong&gt;: “Explain time/space complexity and how it behaves under load.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then do the final step: review like you’d review a junior engineer’s PR. You’re looking for clarity, correctness, and fit—not for “looks right.”&lt;/p&gt;
&lt;p&gt;A useful mental model: AI can help you &lt;em&gt;write&lt;/em&gt; faster, but you’re responsible for &lt;em&gt;thinking&lt;/em&gt; faster. Pairing enforces that division of labor.&lt;/p&gt;
&lt;h2 id="conclusion-ai-as-a-thought-partner-makes-you-a-better-engineer"&gt;Conclusion: AI as a Thought Partner Makes You a Better Engineer&lt;/h2&gt;
&lt;p&gt;If you use AI like a clipboard, you’ll ship code that you didn’t truly evaluate. If you use AI like a pair programmer—iterating on architecture, challenging assumptions, generating alternatives, and validating with tests—you’ll ship code you actually understand.&lt;/p&gt;
&lt;p&gt;The best part is that this approach compounds. Every critique you force out of the model becomes a pattern you can apply later—whether or not you’re using AI.&lt;/p&gt;</content></item><item><title>The Postgres Extensions That Make Every Other Database Jealous</title><link>https://decastro.work/blog/postgres-extensions-make-every-database-jealous/</link><pubDate>Wed, 13 Sep 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgres-extensions-make-every-database-jealous/</guid><description>&lt;p&gt;Every time a project needs “just one more database,” the stack quietly grows teeth: a separate search engine, a dedicated time-series store, a geospatial platform, a job scheduler, maybe even a vector database. It’s not that those systems are bad—it’s that they’re optional. PostgreSQL is the ecosystem with a secret weapon: extensions. And if you choose the right ones, you can keep the operational surface area small without giving up capability.&lt;/p&gt;</description><content>&lt;p&gt;Every time a project needs “just one more database,” the stack quietly grows teeth: a separate search engine, a dedicated time-series store, a geospatial platform, a job scheduler, maybe even a vector database. It’s not that those systems are bad—it’s that they’re optional. PostgreSQL is the ecosystem with a secret weapon: extensions. And if you choose the right ones, you can keep the operational surface area small without giving up capability.&lt;/p&gt;
&lt;p&gt;The real question isn’t “Do we need another database?” It’s: &lt;strong&gt;can PostgreSQL do it via an extension?&lt;/strong&gt; In my experience, the answer is often yes—especially when the “missing feature” is one of the big modern categories: vectors, geospatial, time-series, scheduling, or horizontal scale.&lt;/p&gt;
&lt;h2 id="why-extensions-beat-add-another-database"&gt;Why Extensions Beat “Add Another Database”&lt;/h2&gt;
&lt;p&gt;PostgreSQL extensions aren’t just add-ons; they’re a philosophy. Instead of moving data between systems, you keep it in one place and let specialized features live inside the same engine, same transactions, same security model, same query planner.&lt;/p&gt;
&lt;p&gt;That matters because most production pain isn’t theoretical—it’s operational:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple databases&lt;/strong&gt; mean multiple backup/restore paths, monitoring, upgrade cycles, and failure modes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data duplication&lt;/strong&gt; means eventual consistency bugs and messy synchronization jobs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-system queries&lt;/strong&gt; often become application-layer glue, which is where correctness goes to die.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Extensions change the equation. You still deploy one database, but you can teach it new skills. When it works, the payoff is huge: fewer moving parts, simpler governance, and simpler developer experience.&lt;/p&gt;
&lt;p&gt;So when someone proposes a specialty database, I ask a blunt follow-up: &lt;strong&gt;“What exactly are we adding, and can Postgres do that natively via an extension?”&lt;/strong&gt; Most of the time, the answer is surprisingly straightforward.&lt;/p&gt;
&lt;h2 id="pgvector-make-your-relevance-engine-a-postgres-query"&gt;pgvector: Make Your Relevance Engine a Postgres Query&lt;/h2&gt;
&lt;p&gt;If your app needs semantic search, recommendations, or retrieval-augmented generation (RAG), you’re in “embeddings land.” The typical approach is to reach for a vector database. But PostgreSQL can host vectors just fine—using &lt;strong&gt;pgvector&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A practical way to think about it: your embeddings don’t have to live in a separate product category. They can live alongside the rest of your data—products, documents, users, permissions—so your similarity search becomes part of the same query that enforces business rules.&lt;/p&gt;
&lt;p&gt;Example pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store an embedding vector for each document.&lt;/li&gt;
&lt;li&gt;When a user searches, compute a query embedding.&lt;/li&gt;
&lt;li&gt;Run a &lt;code&gt;SELECT&lt;/code&gt; that ranks documents by vector similarity—&lt;em&gt;and&lt;/em&gt; filters by tenant, access level, language, or recency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last part is the killer feature. In a vector-only system, access control often becomes a post-filtering step, which can quietly ruin both ranking quality and latency. In Postgres, filtering and ranking can be combined so the engine does the work.&lt;/p&gt;
&lt;p&gt;Even if you’re not building full RAG yet, pgvector helps you avoid a common trap: treating embeddings as a separate universe. Keep them in Postgres and you can evolve from “semantic search” to “semantic search + transactional features” without rewriting your data model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Treat embeddings like any other field with lifecycle needs. Plan for re-embedding when your model changes, and version your embeddings (e.g., &lt;code&gt;embedding_model_version&lt;/code&gt;) so you can run migrations safely.&lt;/p&gt;
&lt;h2 id="postgis-geospatial-without-the-geospatial-stack"&gt;PostGIS: Geospatial Without the Geospatial Stack&lt;/h2&gt;
&lt;p&gt;Geospatial workloads have a long history of attracting special-purpose databases. That’s reasonable—until you realize you can do a lot of serious geo work in Postgres.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostGIS&lt;/strong&gt; is the industry standard for a reason: it’s mature, expressive, and designed for spatial querying and indexing.&lt;/p&gt;
&lt;p&gt;The key advantage isn’t just that PostGIS can store coordinates. It’s that it can answer spatial questions like they’re first-class:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Which delivery zones intersect this polygon?”&lt;/li&gt;
&lt;li&gt;“Which warehouses are within 10 km of this address?”&lt;/li&gt;
&lt;li&gt;“Are these two geometries touching, overlapping, or disjoint?”&lt;/li&gt;
&lt;li&gt;“Return nearest points, but only inside allowed regions.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, that means you can keep your “geo logic” in SQL with real indexes, instead of pushing it into application code or a separate geospatial service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concrete example:&lt;/strong&gt; Suppose you’re building an admin console for service areas. You likely need to validate boundaries, compute intersections, and generate “coverage maps.” With PostGIS, those operations can run directly over your canonical geometry tables. You avoid building a second source of truth just for maps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Don’t wait until you have a performance problem to design spatial indexes. Use the right geometry types and SRIDs consistently, and index the columns you’ll query with distance or containment patterns.&lt;/p&gt;
&lt;h2 id="timescaledb-time-series-features-where-you-already-store-everything"&gt;TimescaleDB: Time-Series Features Where You Already Store Everything&lt;/h2&gt;
&lt;p&gt;Time-series is another category that tends to pull in dedicated databases—because it’s tempting to think in terms of specialized storage. But if your app already uses Postgres as the system of record, you may not need to leave it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TimescaleDB&lt;/strong&gt; adds time-series capabilities to Postgres, notably the kind of partitioning and query optimizations that make time-series practical without turning your architecture into spaghetti.&lt;/p&gt;
&lt;p&gt;Where this pays off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metrics and events&lt;/strong&gt; tied to your domain entities (users, devices, accounts).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational telemetry&lt;/strong&gt; that you want to query alongside relational data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retention and downsampling policies&lt;/strong&gt; that you can manage without bespoke tooling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A common “gotcha” with time-series splits is that you end up with mismatched identifiers: your device registry lives in Postgres, but your metrics live elsewhere. Queries become two-step workflows—fetch IDs from one system, then query the other—and you introduce synchronization logic that never fully disappears.&lt;/p&gt;
&lt;p&gt;With TimescaleDB, you can store both relational data and time-series samples together, then join when it matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Model your hypertables thoughtfully—choose the right time column, understand your cardinality, and set retention/compression policies early. Time-series schema changes are rarely fun after you’ve shipped.&lt;/p&gt;
&lt;h2 id="pg_cron-scheduled-jobs-that-dont-need-another-service"&gt;pg_cron: Scheduled Jobs That Don’t Need Another Service&lt;/h2&gt;
&lt;p&gt;At some point, every system needs scheduling: periodic cleanup, batch processing, notifications, reindexing, refreshing aggregates, calling external APIs, rotating keys, exporting reports.&lt;/p&gt;
&lt;p&gt;The usual response is to bolt on a scheduler service or rely on an external cron environment. But PostgreSQL can schedule tasks internally with &lt;strong&gt;pg_cron&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That shifts work from “some machine somewhere runs cron” to “the database owns the schedule.” It’s a cleaner separation of responsibilities when you’re operating a managed environment or you want tight coupling between job execution and database state.&lt;/p&gt;
&lt;p&gt;Example uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nightly recomputation of search rankings or materialized views.&lt;/li&gt;
&lt;li&gt;Periodic expiration of stale records.&lt;/li&gt;
&lt;li&gt;Scheduled maintenance of derived tables for dashboard speed.&lt;/li&gt;
&lt;li&gt;Controlled batch writes for ETL jobs that must be consistent with transactional data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point isn’t that pg_cron replaces every job system—workflow orchestration still belongs somewhere else. It’s that &lt;strong&gt;simple, database-adjacent scheduling&lt;/strong&gt; is often best kept close to the data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Keep scheduled jobs idempotent. Assume retries. And log job activity so you can answer “what ran, when, and why it failed” without spelunking across systems.&lt;/p&gt;
&lt;h2 id="horizontal-scaling-when-you-need-it-still-consider-postgres"&gt;Horizontal Scaling: When You Need It, Still Consider Postgres&lt;/h2&gt;
&lt;p&gt;Not every scaling problem is solvable with an extension, but some are. When you need to distribute data across nodes—typically for high write throughput or multi-tenant scale—&lt;strong&gt;Citus&lt;/strong&gt; is the extension that often steps in.&lt;/p&gt;
&lt;p&gt;The value here is strategic: you keep the PostgreSQL programming model while enabling distribution patterns. Developers don’t have to relearn query language semantics or build cross-database aggregation layers just to scale out.&lt;/p&gt;
&lt;p&gt;If you’re evaluating whether to adopt another distributed database, the right framing is: &lt;strong&gt;are we replacing PostgreSQL because we can’t scale it, or because we didn’t try scaling it in the way PostgreSQL supports?&lt;/strong&gt; Citus is one of the best “try Postgres first” answers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Distribution is not a magic trick. Get your sharding key strategy right early, and test cross-shard query patterns. Distributed databases reward discipline.&lt;/p&gt;
&lt;h2 id="a-modern-postgres-first-architecture-checklist"&gt;A Modern “Postgres First” Architecture Checklist&lt;/h2&gt;
&lt;p&gt;Here’s the rule I use to prevent database sprawl from turning into long-term drag:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Vector search?&lt;/strong&gt; Try pgvector before committing to a separate vector database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Geospatial?&lt;/strong&gt; Use PostGIS and keep geo logic close to the data model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-series?&lt;/strong&gt; Add TimescaleDB when your “time” data is part of the same domain as everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduling?&lt;/strong&gt; Use pg_cron for database-owned periodic tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Need scale-out distribution?&lt;/strong&gt; Evaluate Citus rather than switching engines on day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you still need another system, make it for a reason Postgres can’t solve&lt;/strong&gt;—not because the stack template says you should.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This isn’t about ideology. It’s about minimizing complexity while keeping capability. PostgreSQL’s extension ecosystem lets you expand without abandoning the foundation.&lt;/p&gt;
&lt;h2 id="conclusion-fewer-databases-better-decisions"&gt;Conclusion: Fewer Databases, Better Decisions&lt;/h2&gt;
&lt;p&gt;Specialty databases can be excellent tools—but they come with costs: more infrastructure, more integration work, and more places for correctness to drift. The best engineering decision is often the one that preserves simplicity while increasing power.&lt;/p&gt;
&lt;p&gt;When your “missing feature” falls into vectors, geospatial, time-series, scheduling, or distributed scale, there’s a good chance PostgreSQL already has an extension for it. The next time someone proposes adding a new database, don’t argue—ask the more productive question: &lt;strong&gt;can this be a Postgres extension problem instead?&lt;/strong&gt;&lt;/p&gt;</content></item><item><title>Passkeys Will Kill Passwords and It Can't Happen Fast Enough</title><link>https://decastro.work/blog/passkeys-will-kill-passwords/</link><pubDate>Fri, 08 Sep 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/passkeys-will-kill-passwords/</guid><description>&lt;p&gt;Passwords are the worst authentication mechanism—except for every other option we’ve tried. They’re memorable, but they’re also phishable, reusable, and constantly mishandled. And even when users do everything “right,” attackers still have a playbook: trick them, capture the secret, and ride it to account takeover.&lt;/p&gt;
&lt;p&gt;Passkeys—FIDO2 credentials stored in platform authenticators—remove the secret from the equation. That change isn’t incremental. It’s structural. And it’s finally making phishing-resistant authentication the default experience users get without thinking.&lt;/p&gt;</description><content>&lt;p&gt;Passwords are the worst authentication mechanism—except for every other option we’ve tried. They’re memorable, but they’re also phishable, reusable, and constantly mishandled. And even when users do everything “right,” attackers still have a playbook: trick them, capture the secret, and ride it to account takeover.&lt;/p&gt;
&lt;p&gt;Passkeys—FIDO2 credentials stored in platform authenticators—remove the secret from the equation. That change isn’t incremental. It’s structural. And it’s finally making phishing-resistant authentication the default experience users get without thinking.&lt;/p&gt;
&lt;h2 id="why-passwords-keep-losing-even-when-everyone-agrees-theyre-bad"&gt;Why passwords keep losing (even when everyone agrees they’re bad)&lt;/h2&gt;
&lt;p&gt;Let’s be blunt: passwords don’t fail because users are careless. They fail because the threat model is stacked against them.&lt;/p&gt;
&lt;p&gt;Phishing turns passwords into a one-time ticket for criminals. Even perfect password habits don’t stop a user from being tricked into entering their secret into a fake login page. And once the attacker has a working password, the rest is easy: automated retries, credential stuffing, session hijacking attempts, and the same “you were logged in” patterns that still dominate modern account takeover workflows.&lt;/p&gt;
&lt;p&gt;Then there’s operational reality. Many breaches don’t involve “breaking encryption.” They involve mishandled secrets: leaked databases, weak password storage practices, credential reuse across services, and reset flows that become attack surfaces. Even with salted hashes (and even when you do everything correctly), the fact remains that passwords are an attractive target.&lt;/p&gt;
&lt;p&gt;Passwords also produce a UI/UX bargain that attackers exploit. Users type secrets. Attackers ask for them. Humans are predictable. Every time you build a login form, you’re building a stage for social engineering.&lt;/p&gt;
&lt;h2 id="passkeys-change-the-game-no-secret-to-steal-no-password-to-reuse"&gt;Passkeys change the game: no secret to steal, no password to reuse&lt;/h2&gt;
&lt;p&gt;Passkeys are FIDO2 credentials—typically bound to your device or platform authenticator—that authenticate using public-key cryptography. The key point is what doesn’t exist anymore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No password to phish.&lt;/strong&gt; There’s nothing for a malicious page to trick a user into typing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No credential to “stuff.”&lt;/strong&gt; Attackers can’t replay a captured secret because the authentication response is generated in context and typically protected by hardware-backed signing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reusable shared secret to exfiltrate from a database.&lt;/strong&gt; What you store server-side isn’t a password-equivalent secret waiting to be abused.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, the flow looks different from password login:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The server sends a challenge tied to the specific site and session.&lt;/li&gt;
&lt;li&gt;The user triggers a &lt;strong&gt;platform authenticator&lt;/strong&gt; (biometric prompt, device PIN, or secure element approval).&lt;/li&gt;
&lt;li&gt;The device signs the challenge with the credential private key.&lt;/li&gt;
&lt;li&gt;The server verifies the signature using the corresponding public key.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is more than “stronger passwords.” It’s a different mechanism with a different failure mode.&lt;/p&gt;
&lt;p&gt;And importantly: because passkeys are designed for the web with &lt;strong&gt;WebAuthn&lt;/strong&gt;, they can be implemented without a proprietary client app. The UX is also finally competitive: a biometric or device prompt that’s often quicker than typing.&lt;/p&gt;
&lt;h2 id="the-real-reason-passkeys-work-phishing-resistance-by-design"&gt;The real reason passkeys work: phishing resistance by design&lt;/h2&gt;
&lt;p&gt;A lot of security advice boils down to “be smarter than the attacker.” Passkeys are smarter than the attacker—at least for the phishing scenario that accounts for so much real-world trouble.&lt;/p&gt;
&lt;p&gt;Phishing usually relies on two weaknesses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The victim is convinced to enter credentials into a malicious site.&lt;/li&gt;
&lt;li&gt;The attacker’s site proxies or reuses that credential against the real service.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Passkeys break the first step because the “credential” is not something a web page can harvest through text entry. The second step breaks because authentication is tied to the relying party (the real site origin) and produces a response that can’t be simply replayed elsewhere.&lt;/p&gt;
&lt;p&gt;To make this concrete, imagine a fake login page for your fintech app. Under a password model, the attacker just asks for username and password and then forwards them. Under a passkey model, that fake page can’t trigger the right authenticator flow &lt;em&gt;for the correct relying party&lt;/em&gt; in a way the server will accept. The user may see a prompt, but the cryptographic verification still only succeeds for the genuine site configuration.&lt;/p&gt;
&lt;p&gt;Is the user still responsible for choosing the right button and device prompt? Yes. But the attacker can’t complete the job by collecting a reusable secret. That’s the crucial shift.&lt;/p&gt;
&lt;h2 id="platform-authenticators-and-native-support-the-ecosystem-finally-arrived"&gt;Platform authenticators and native support: the ecosystem finally arrived&lt;/h2&gt;
&lt;p&gt;Passkeys aren’t theoretical. They’re shipping across major platforms with native support from &lt;strong&gt;Apple, Google, and Microsoft&lt;/strong&gt;. That matters because passkeys live or die by usability. If every deployment required custom client software or a clunky enrollment process, adoption would stall.&lt;/p&gt;
&lt;p&gt;With platform authenticators, the “credential storage” problem is handled where it belongs: on-device, protected by OS-level security primitives. The user experiences it as a biometric prompt, often with familiar affordances like Face ID / Touch ID, device PIN, or an OS confirmation dialog.&lt;/p&gt;
&lt;p&gt;From a developer’s standpoint, the biggest advantage is straightforward integration. You can implement WebAuthn-based passkey registration and authentication inside your existing web auth flow—then offer passkeys alongside passwords during transition. Users who already use password managers aren’t forced to abandon them overnight; they’re given an upgrade path.&lt;/p&gt;
&lt;p&gt;The win is twofold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security improves immediately for users who adopt passkeys.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your system becomes less dependent on brittle password workflows&lt;/strong&gt;, like “forgot password” as a primary recovery path.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="practical-rollout-strategy-stop-dragging-your-feet-but-dont-brick-your-users"&gt;Practical rollout strategy: stop dragging your feet, but don’t brick your users&lt;/h2&gt;
&lt;p&gt;If you’re building authentication in 2023 and not supporting passkeys, you’re building yesterday’s security. But you still need to roll this out responsibly.&lt;/p&gt;
&lt;p&gt;Here’s a pragmatic plan that avoids the common trap of “we flipped the switch and everything broke.”&lt;/p&gt;
&lt;h3 id="1-add-passkey-registration-to-the-account-settings-flow"&gt;1) Add passkey registration to the account settings flow&lt;/h3&gt;
&lt;p&gt;Make it obvious and low-friction. A button like &lt;strong&gt;“Add a passkey”&lt;/strong&gt; paired with a clear explanation (“Use biometrics or your device to sign in—no password required”) goes a long way.&lt;/p&gt;
&lt;h3 id="2-support-passkey-authentication-on-the-login-screenalongside-passwords"&gt;2) Support passkey authentication on the login screen—alongside passwords&lt;/h3&gt;
&lt;p&gt;Don’t force an all-or-nothing migration. Offer passkeys as an alternative button or flow, then log outcomes so you can measure progress. The key is to make passkey usage the path of least resistance.&lt;/p&gt;
&lt;h3 id="3-treat-account-recovery-as-a-first-class-design-problem"&gt;3) Treat account recovery as a first-class design problem&lt;/h3&gt;
&lt;p&gt;Recovery is where attackers often strike, especially when password resets are weak. Once passkeys are available, consider using them as part of the recovery story—at minimum, ensure recovery flows can’t be trivially abused and that they’re hardened against account enumeration.&lt;/p&gt;
&lt;h3 id="4-handle-edge-cases-like-lost-devices-and-multi-device-enrollment"&gt;4) Handle edge cases like lost devices and multi-device enrollment&lt;/h3&gt;
&lt;p&gt;Encourage users to create passkeys on more than one device (especially if your service is important). In UI terms, this can be as simple as letting them add multiple passkeys and showing which devices they’ve registered.&lt;/p&gt;
&lt;h3 id="5-plan-telemetry-and-friction-measurement"&gt;5) Plan telemetry and friction measurement&lt;/h3&gt;
&lt;p&gt;Track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how many users have passkeys enrolled&lt;/li&gt;
&lt;li&gt;how often passkey auth succeeds&lt;/li&gt;
&lt;li&gt;where users drop off (registration errors, prompt cancellations, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Security wins mean nothing if the UX collapses. Measure it, iterate it, improve it.&lt;/p&gt;
&lt;h2 id="the-password-retirement-plan-make-it-optional-then-make-it-obsolete"&gt;The password retirement plan: make it optional, then make it obsolete&lt;/h2&gt;
&lt;p&gt;The goal isn’t “ban passwords tomorrow.” The goal is to &lt;strong&gt;make passwords progressively irrelevant&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Start by positioning passkeys as the modern default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a user has a passkey, offer sign-in with passkey prominently.&lt;/li&gt;
&lt;li&gt;If not, allow password login but make passkey enrollment part of the account experience (“Secure your account with a passkey”).&lt;/li&gt;
&lt;li&gt;After you see meaningful adoption, reduce password reliance: consider rate limits, stronger recovery requirements, and clearer warnings around risky login behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll notice something as usage grows: your support queues change. Fewer “I got phished” and “my credentials were stolen” incidents. More “I lost my phone” issues—which are manageable when recovery is designed well.&lt;/p&gt;
&lt;p&gt;Eventually, you’ll reach the stage where passwords are just a fallback. That’s the endgame.&lt;/p&gt;
&lt;h2 id="conclusion-passkeys-are-the-default-security-upgrade-and-waiting-costs-you"&gt;Conclusion: Passkeys are the default security upgrade, and waiting costs you&lt;/h2&gt;
&lt;p&gt;Passwords aren’t just insecure—they’re structurally vulnerable to phishing and account takeover. Passkeys fix that by removing the phishable secret and binding authentication to the legitimate origin using cryptography and platform authenticators.&lt;/p&gt;
&lt;p&gt;Adoption won’t happen fast enough if you treat it like a “someday” feature. The fastest way to improve your users’ security is to ship passkeys now: enroll them in settings, offer them on login, harden recovery, and then quietly let passwords fade into irrelevance. This is one of the rare security transitions that actually improves both protection and experience at the same time—and you should take it.&lt;/p&gt;</content></item><item><title>Biome Is the Linter/Formatter Combo That Will End the Prettier/ESLint Era</title><link>https://decastro.work/blog/biome-linter-formatter-end-prettier-eslint/</link><pubDate>Fri, 01 Sep 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/biome-linter-formatter-end-prettier-eslint/</guid><description>&lt;p&gt;For years, JavaScript developers have accepted an annoying tax: two tools, two configs, and the constant possibility that they’ll disagree. ESLint worries about code quality; Prettier reshapes your style. In theory they’re complementary. In practice, teams end up with drift, duplicated rules, and a pipeline that feels heavier than it should. Biome changes the equation: one fast, Rust-based tool that both formats and lints—without you hand-tuning a peace treaty between systems that were never meant to work together.&lt;/p&gt;</description><content>&lt;p&gt;For years, JavaScript developers have accepted an annoying tax: two tools, two configs, and the constant possibility that they’ll disagree. ESLint worries about code quality; Prettier reshapes your style. In theory they’re complementary. In practice, teams end up with drift, duplicated rules, and a pipeline that feels heavier than it should. Biome changes the equation: one fast, Rust-based tool that both formats and lints—without you hand-tuning a peace treaty between systems that were never meant to work together.&lt;/p&gt;
&lt;h2 id="the-real-problem-two-tools-one-codebase-endless-edge-cases"&gt;The real problem: two tools, one codebase, endless edge cases&lt;/h2&gt;
&lt;p&gt;ESLint and Prettier solve different problems, but they create shared responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ESLint&lt;/strong&gt; enforces rules (style, correctness, best practices) and can also format in limited ways.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prettier&lt;/strong&gt; enforces formatting (whitespace, quotes, commas, indentation) and will reformat whatever it’s pointed at.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The friction starts when teams try to make those responsibilities overlap &lt;em&gt;just enough&lt;/em&gt; to avoid conflicts. The common response is to configure ESLint with rules like “delegate formatting to Prettier,” disable stylistic rules, add plugins, and wire up both tools in scripts—often in a sequence that depends on file types, editor behavior, and CI.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in real projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers run &lt;code&gt;eslint&lt;/code&gt; locally and sometimes see issues that disappear after formatting.&lt;/li&gt;
&lt;li&gt;Prettier changes code in ways that make an ESLint rule complain differently (or vice versa).&lt;/li&gt;
&lt;li&gt;Teams accumulate “temporary” rules and overrides to quiet a specific case.&lt;/li&gt;
&lt;li&gt;Migrations become multi-step: update dependencies, adjust configs, then revisit rules, then fix formatting diffs that show up everywhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not just inconvenience—it’s a &lt;strong&gt;DX tax&lt;/strong&gt;. Every time formatting and linting are separate, you pay with complexity: more configuration, more tooling surface area, more chances for the editor to behave differently than CI, and more time watching diffs scroll by.&lt;/p&gt;
&lt;h2 id="what-biome-does-differently-one-pass-for-style-and-correctness"&gt;What Biome does differently: one pass for style and correctness&lt;/h2&gt;
&lt;p&gt;Biome is a single tool designed to be a formatter and linter in one. Conceptually, the win is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Formatting isn’t an external “afterthought” step.&lt;/li&gt;
&lt;li&gt;Linting isn’t blind to the formatting decisions that will be applied.&lt;/li&gt;
&lt;li&gt;Configuration doesn’t need to coordinate two separate rule engines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practically, Biome aims for a workflow where running it is deterministic: the same input yields the same output, and both linting feedback and formatted code come from the same source of truth.&lt;/p&gt;
&lt;p&gt;The result is a more cohesive developer experience. Instead of asking, “Should I run ESLint or Prettier first?” you just run one tool and move on. Instead of maintaining a brittle chain of config choices to prevent conflicts, you rely on one tool’s built-in philosophy about what good code looks like.&lt;/p&gt;
&lt;p&gt;And yes—the speed matters. When a tool is fast enough, it changes how people work: formatting-before-commit becomes default behavior instead of a background chore. On large repositories, the difference is visceral—less time waiting, fewer context switches, and fewer “why is CI taking so long” conversations.&lt;/p&gt;
&lt;h2 id="migration-replacing-a-two-tool-pipeline-with-one-command"&gt;Migration: replacing a two-tool pipeline with one command&lt;/h2&gt;
&lt;p&gt;Most teams don’t want a rewrite. They want a swap.&lt;/p&gt;
&lt;h3 id="step-1-install-biome-and-add-a-config"&gt;Step 1: Install Biome and add a config&lt;/h3&gt;
&lt;p&gt;You add Biome to your dev dependencies and let it generate or adopt configuration. Biome supports formats and linting in a way that maps cleanly to typical expectations: you don’t need to invent a new governance process, you replace the enforcement engine.&lt;/p&gt;
&lt;h3 id="step-2-start-by-wiring-biome-into-scripts"&gt;Step 2: Start by wiring Biome into scripts&lt;/h3&gt;
&lt;p&gt;A straightforward migration typically looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run Biome in CI to ensure formatted code and lint rules are satisfied.&lt;/li&gt;
&lt;li&gt;Optionally run Biome in a pre-commit hook for quick feedback.&lt;/li&gt;
&lt;li&gt;Update developer scripts so local checks match CI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of &lt;code&gt;eslint .&lt;/code&gt; and &lt;code&gt;prettier --check .&lt;/code&gt; (plus whatever plugins and ignore rules make those behave), you run Biome and let it do both. The key benefit isn’t just fewer commands—it’s fewer mismatched behaviors.&lt;/p&gt;
&lt;h3 id="step-3-accept-and-commit-the-formatting-baseline"&gt;Step 3: Accept and commit the formatting baseline&lt;/h3&gt;
&lt;p&gt;Your first Biome run will produce diffs. That’s normal, and it’s the point. Commit a clean formatting baseline so future diffs reflect &lt;em&gt;real changes&lt;/em&gt;, not tooling churn.&lt;/p&gt;
&lt;p&gt;A practical tip: do this as a single atomic commit (or a small number of commits) and communicate it clearly. Teams that treat migration diffs as background noise tend to lose trust in the toolchain. Treat it as “day one, reset the baseline,” and you’ll get cleaner downstream behavior.&lt;/p&gt;
&lt;h3 id="step-4-lock-in-linting-expectations"&gt;Step 4: Lock in linting expectations&lt;/h3&gt;
&lt;p&gt;Then you tune linting to your preferences—without maintaining two different rule systems. If you currently have ESLint rules that exist only to prevent formatting chaos, expect them to disappear in the new world. If you relied on Prettier to control style, those decisions become part of Biome’s formatting pipeline.&lt;/p&gt;
&lt;p&gt;The outcome: fewer “override files” and fewer “we had to disable that rule because Prettier changes it” conversations.&lt;/p&gt;
&lt;h2 id="editing-and-ci-making-works-on-my-machine-less-of-a-thing"&gt;Editing and CI: making “works on my machine” less of a thing&lt;/h2&gt;
&lt;p&gt;A large part of the ESLint/Prettier era is the mismatch between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Editor formatting&lt;/strong&gt; (what happens when you save)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local scripts&lt;/strong&gt; (what you run manually)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI checks&lt;/strong&gt; (what gatekeepers enforce)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when teams “configure correctly,” these three can still drift due to plugin versions, extension settings, file globs, ignore rules, and ordering.&lt;/p&gt;
&lt;p&gt;Biome’s value shows up when you make one tool authoritative:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the editor uses Biome for formatting, your saved code matches the CI expectation.&lt;/li&gt;
&lt;li&gt;If the same Biome config drives both lint and formatting checks, you avoid the “formatting passes but lint fails” loop.&lt;/li&gt;
&lt;li&gt;If it runs quickly, teams actually enable it everywhere (pre-commit, CI, and—critically—editor save hooks).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good target workflow is: &lt;strong&gt;save triggers formatting, and commits trigger lint + formatting checks&lt;/strong&gt;. When everything is the same tool, you reduce the number of moving pieces that can disagree.&lt;/p&gt;
&lt;h2 id="performance-is-not-a-nice-to-haveit-changes-team-behavior"&gt;Performance is not a “nice to have”—it changes team behavior&lt;/h2&gt;
&lt;p&gt;The slogan “100x faster” is marketing unless you’ve felt the alternative. In practice, slower formatter/linter combos produce predictable habits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers defer running formatting to “later.”&lt;/li&gt;
&lt;li&gt;People skip linting locally and rely on CI.&lt;/li&gt;
&lt;li&gt;Pre-commit hooks get disabled or weakened because they feel too slow.&lt;/li&gt;
&lt;li&gt;Large diffs become painful because the feedback loop is sluggish.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Biome shifts the incentives. When checks are quick, teams tighten the loop: run checks more often, fix issues sooner, and stop treating linting as a gate you survive at the end of the day. You get less friction and more consistency—and because it’s one tool, you get fewer “why did that change?” mysteries.&lt;/p&gt;
&lt;p&gt;On big monorepos, this matters even more. Toolchain overhead scales with the number of files and the number of times you run it. The best DX improvements aren’t always about adding features; sometimes they’re about removing the need to run two heavyweight passes across the same code. Biome’s approach is that removal made real.&lt;/p&gt;
&lt;h2 id="common-concerns-will-it-match-our-existing-standards-and-what-about-rule-parity"&gt;Common concerns: “Will it match our existing standards?” and “What about rule parity?”&lt;/h2&gt;
&lt;p&gt;Migration anxiety is healthy. Teams should ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Will Biome format code in a way that matches our current style expectations?&lt;/li&gt;
&lt;li&gt;Will our current ESLint rules translate cleanly?&lt;/li&gt;
&lt;li&gt;Will we lose coverage?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest answer: you’ll want to evaluate it in your codebase. But the migration path usually benefits from the same observation that motivates Biome in the first place: if you’re currently maintaining two tools to approximate one consistent style, you already know your setup isn’t perfectly coherent.&lt;/p&gt;
&lt;p&gt;A pragmatic plan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run Biome and review diffs&lt;/strong&gt; to ensure formatting isn’t wildly surprising.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port lint expectations&lt;/strong&gt; incrementally—start broad, then tighten.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure trust&lt;/strong&gt;: once developers see that Biome’s output is consistent and fixes issues quickly, adoption becomes easy.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you truly require specific custom ESLint rule behavior, you’ll need to verify Biome’s lint coverage for those exact cases. But even when teams find one or two rule gaps, the overall direction remains compelling: fewer conflicts, less configuration complexity, and faster feedback loops.&lt;/p&gt;
&lt;h2 id="conclusion-one-tool-to-enforce-one-standard"&gt;Conclusion: one tool to enforce one standard&lt;/h2&gt;
&lt;p&gt;The Prettier/ESLint era wasn’t a mistake—it was a necessary compromise for a messy time in tooling. But the compromise is over. Biome offers a simpler model: one Rust-based tool that formats and lints together, producing consistent results with less configuration and dramatically faster feedback.&lt;/p&gt;
&lt;p&gt;If your team is still juggling rule overrides, sequencing scripts to avoid conflicts, and watching formatting + lint diffs become background noise, it’s time to collapse the pipeline. The best DX upgrade isn’t another plugin. It’s fewer tools doing more correctly—faster—so developers can spend their attention on the code, not the toolchain.&lt;/p&gt;</content></item><item><title>Running LLMs Locally Changed How I Think About AI Privacy</title><link>https://decastro.work/blog/running-llms-locally-changed-ai-privacy-thinking/</link><pubDate>Sun, 20 Aug 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/running-llms-locally-changed-ai-privacy-thinking/</guid><description>&lt;p&gt;The first time I ran a local LLM on my MacBook, it didn’t feel like “AI development” so much as a privacy reality check. One minute I was tinkering with prompts. The next, I was reconsidering every AI feature I’d ever shipped—because the difference between “helpful assistant” and “data pipeline” stops being theoretical when your laptop can do the work.&lt;/p&gt;
&lt;p&gt;If you build software that touches user data, you should run a model locally at least once. Not because it’s always the best option, but because it makes the tradeoffs painfully concrete.&lt;/p&gt;</description><content>&lt;p&gt;The first time I ran a local LLM on my MacBook, it didn’t feel like “AI development” so much as a privacy reality check. One minute I was tinkering with prompts. The next, I was reconsidering every AI feature I’d ever shipped—because the difference between “helpful assistant” and “data pipeline” stops being theoretical when your laptop can do the work.&lt;/p&gt;
&lt;p&gt;If you build software that touches user data, you should run a model locally at least once. Not because it’s always the best option, but because it makes the tradeoffs painfully concrete.&lt;/p&gt;
&lt;h2 id="what-local-llms-actually-mean-and-why-it-matters"&gt;What “local LLMs” actually mean (and why it matters)&lt;/h2&gt;
&lt;p&gt;A cloud LLM is a service: you send text, the model runs elsewhere, and your input becomes part of a larger system—logs, telemetry, retention policies, access controls, incident response, and (often) additional vendors in the chain. Local LLMs flip that architecture: the model runs on your machine, and the only network path is whatever you choose to create.&lt;/p&gt;
&lt;p&gt;That distinction sounds obvious, but it’s easy to forget when you’re working at the product level. “We’ll send the user’s prompt to the model” becomes routine. “We’re now transferring potentially sensitive information to a third party” becomes a line item you may or may not revisit.&lt;/p&gt;
&lt;p&gt;Running locally reframes the whole conversation. You stop thinking in vague terms like “privacy” and start thinking in operational terms: where does the text go, how long is it kept, who can access it, and what happens when something goes wrong.&lt;/p&gt;
&lt;h2 id="my-weekend-experiment-capabilities-without-the-data-pipeline"&gt;My weekend experiment: capabilities without the data pipeline&lt;/h2&gt;
&lt;p&gt;I didn’t start with a grand plan. I just wanted to understand whether local models were usable for real development. Within an afternoon, I was able to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chat with a model about code structure,&lt;/li&gt;
&lt;li&gt;generate quick drafts for documentation,&lt;/li&gt;
&lt;li&gt;do lightweight classification tasks (e.g., “is this a bug report or a feature request?”),&lt;/li&gt;
&lt;li&gt;and sanity-check prompt behavior by iterating rapidly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key moment wasn’t that it was smarter than a cloud model—it wasn’t. It was that it was &lt;em&gt;predictably constrained in a way I could observe&lt;/em&gt;. Context windows were limited. Outputs were slower. Some tasks required more prompting discipline. But crucially, I never had to route user-like text to a third party to test those behaviors.&lt;/p&gt;
&lt;p&gt;I started imagining the same workflow inside a real product: “What if the user’s text is private by design?” “What if it’s regulated?” “What if the model’s failure mode leaks sensitive details?” With a local run, you feel these questions in your hands, not in policy documents.&lt;/p&gt;
&lt;h2 id="the-tradeoffs-you-cant-ignore-slower-smaller-and-pickier"&gt;The tradeoffs you can’t ignore: slower, smaller, and pickier&lt;/h2&gt;
&lt;p&gt;Local LLMs come with compromises. You’re replacing convenience and scale with control and proximity. That’s a good deal for some use cases—but it’s not a universal replacement for cloud inference.&lt;/p&gt;
&lt;p&gt;Here are the tradeoffs I think developers should internalize early:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance and latency.&lt;/strong&gt; Even on modern laptops, local inference can feel “glacial” compared to managed endpoints. That’s fine for editing support or internal tools, but it changes user experience expectations. If your AI feature is interactive and fast, you’ll need careful UX design (streaming outputs, optimistic UI, background processing, or smaller models).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Model capability and reliability.&lt;/strong&gt; Smaller local models often require tighter prompting, fewer steps, and better constraints. They can be very helpful, but they’re also more likely to produce plausible nonsense than a stronger cloud model. That means you must treat them like an assistant, not a source of truth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setup friction.&lt;/strong&gt; Local tooling varies. You’ll deal with model downloads, quantization choices, hardware limitations, and configuration quirks. This is where most teams stop experimenting—precisely because it’s inconvenient. But the effort is educational: it teaches you what “offline” really costs.&lt;/p&gt;
&lt;p&gt;My advice: don’t use local models to pretend you don’t need engineering. Use them to develop intuition about constraints—because intuition becomes a design tool when you later decide what to send to the cloud.&lt;/p&gt;
&lt;h2 id="how-local-models-change-your-privacy-decisions"&gt;How local models change your privacy decisions&lt;/h2&gt;
&lt;p&gt;This is the part that surprised me most. After running locally, I stopped asking only, “Is the provider trustworthy?” and started asking more precise questions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) What exactly are we transmitting?&lt;/strong&gt;&lt;br&gt;
In many apps, the “prompt” isn’t just a prompt—it’s user content, product context, or operational data. If you’re doing support chat, you might be transmitting message history. If you’re doing document analysis, you might be sending entire files. Local testing makes it easier to see what minimal text you could instead process on-device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Are we building a data pipeline accidentally?&lt;/strong&gt;&lt;br&gt;
When teams integrate cloud LLMs, they often create hidden data flows: debug logs, replay tools, monitoring dashboards, and vendor telemetry. Local runs help you notice what can be kept local in the first place, and what you truly need to externalize.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) What happens during failure modes?&lt;/strong&gt;&lt;br&gt;
Privacy isn’t only “where data goes when things are fine.” It’s what your system does when the model misunderstands, when users paste sensitive info unexpectedly, or when your prompt template accidentally includes extra context. With local models, you can repeatedly test those failure modes without depending on third-party handling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) Can we design for data minimization?&lt;/strong&gt;&lt;br&gt;
Once you’ve experienced local inference, you’ll be more willing to reduce the amount of text you send. Maybe you don’t need the full conversation—maybe you only need extracted fields. Maybe you can summarize locally and only transmit a structured, de-identified representation. Even if you still use the cloud for the final step, data minimization gets easier when you’ve built (and measured) the local alternative.&lt;/p&gt;
&lt;h2 id="practical-ways-to-use-local-models-in-real-workflows"&gt;Practical ways to use local models in real workflows&lt;/h2&gt;
&lt;p&gt;You don’t need to replace your entire AI stack. You need a strategy that matches risk and effort. Here are concrete patterns that tend to work well:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use local models for preprocessing.&lt;/strong&gt;&lt;br&gt;
If you have long documents, consider running local extraction or summarization first—turning “a wall of text” into “a small structured payload.” Then send only the essential data to a cloud model for higher-level reasoning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep sensitive interactions on-device.&lt;/strong&gt;&lt;br&gt;
For user-generated content that’s highly sensitive—health notes, internal incident details, personal drafts—local inference can prevent accidental disclosure. Even if your cloud model remains part of the product, you can gate certain flows behind local processing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build with “offline-first prompts.”&lt;/strong&gt;&lt;br&gt;
Before you ever call a hosted endpoint, try to make the task work locally. That forces you to craft prompts, templates, and output formats that don’t rely on magical abilities. It also gives you regression tests: you can compare outputs (or at least behaviors) across model versions without network variability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use local models as development-time tools.&lt;/strong&gt;&lt;br&gt;
A local model is perfect for drafting, code understanding, schema generation, and quick ideation—especially when developers are experimenting with prompt engineering. This reduces the chances that sensitive internal text ever touches external services during development.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treat local outputs as untrusted.&lt;/strong&gt;&lt;br&gt;
Whether the model is local or cloud, you still need guardrails: JSON schema validation, allowlists for actions, retrieval grounding when accuracy matters, and careful refusal handling. Local doesn’t mean “safe.” It means “contained.”&lt;/p&gt;
&lt;h2 id="a-weekend-plan-how-every-developer-can-try-this"&gt;A weekend plan: how every developer can try this&lt;/h2&gt;
&lt;p&gt;Here’s a straightforward approach that doesn’t waste your time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick one model and one task.&lt;/strong&gt;&lt;br&gt;
Don’t start with “I’ll build an AI app.” Start with something bounded: “summarize support emails” or “classify bug reports.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Decide your success criteria.&lt;/strong&gt;&lt;br&gt;
For example: “Can it produce JSON with the fields I need?” or “Does it follow a rubric without constant re-prompting?”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run the same prompt with sensitive-like text.&lt;/strong&gt;&lt;br&gt;
Use realistic examples from your domain (sanitized, of course). The point isn’t to expose real user data; it’s to simulate what your system would do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Measure what changes.&lt;/strong&gt;&lt;br&gt;
Track latency, failure rate, and how much prompting you need to get consistent structure. You’re learning the model’s operational character.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Only then compare to cloud.&lt;/strong&gt;&lt;br&gt;
After you understand local constraints, you can make an informed decision about what to externalize, what to minimize, and what to keep on-device.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do this once and you’ll stop treating privacy as an afterthought. You’ll start treating it as a system design problem—one you can validate by running code, not just reading policy.&lt;/p&gt;
&lt;h2 id="conclusion-local-models-are-a-privacy-education"&gt;Conclusion: Local models are a privacy education&lt;/h2&gt;
&lt;p&gt;Cloud LLMs can be incredibly powerful, and for many production scenarios they’ll remain the default. But running a model locally changed how I think about privacy because it collapsed the distance between “AI feature” and “data handling.” You learn the cost of convenience, the limits of control, and the practical ways to minimize what you transmit.&lt;/p&gt;
&lt;p&gt;Every developer building AI features should spend a weekend with a local model. Not to replace the cloud—just to understand what you’re risking, what you can avoid, and what “privacy by design” looks like when it’s not theoretical.&lt;/p&gt;</content></item><item><title>Property-Based Testing Is the Testing Approach You're Sleeping On</title><link>https://decastro.work/blog/property-based-testing-sleeping-on/</link><pubDate>Sun, 13 Aug 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/property-based-testing-sleeping-on/</guid><description>&lt;p&gt;Most teams think they’re “done with testing” once the unit tests compile and pass. But passing tests are only proof that your code can handle the inputs you bothered to imagine. Property-based testing flips that assumption: instead of hand-picking a few examples, you specify the truths your code must always satisfy—and let the test engine generate thousands of cases, including the ones you’d never write.&lt;/p&gt;
&lt;p&gt;If your current test suite feels like a collection of one-off scenarios, property-based testing is the upgrade you’ve been avoiding.&lt;/p&gt;</description><content>&lt;p&gt;Most teams think they’re “done with testing” once the unit tests compile and pass. But passing tests are only proof that your code can handle the inputs you bothered to imagine. Property-based testing flips that assumption: instead of hand-picking a few examples, you specify the truths your code must always satisfy—and let the test engine generate thousands of cases, including the ones you’d never write.&lt;/p&gt;
&lt;p&gt;If your current test suite feels like a collection of one-off scenarios, property-based testing is the upgrade you’ve been avoiding.&lt;/p&gt;
&lt;h2 id="why-example-based-tests-plateau-fast"&gt;Why example-based tests plateau fast&lt;/h2&gt;
&lt;p&gt;Example-based unit tests are straightforward: you assert that &lt;code&gt;function(x)&lt;/code&gt; equals &lt;code&gt;y&lt;/code&gt; for a small set of chosen inputs. That’s useful—especially early on. The problem is what happens when your domain gets even slightly gnarly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;parsing user input (“empty string” isn’t exotic—it’s inevitable)&lt;/li&gt;
&lt;li&gt;dealing with Unicode (normalization, combining characters, odd boundaries)&lt;/li&gt;
&lt;li&gt;handling integer extremes (&lt;code&gt;0&lt;/code&gt;, &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;MAX_INT&lt;/code&gt;, &lt;code&gt;MIN_INT&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;working with nested structures (depth, empty arrays, missing fields)&lt;/li&gt;
&lt;li&gt;enforcing invariants (sorting, idempotency, round-trips)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At some point, your tests become a patchwork of special cases. You add one more example because a bug slipped through. Then another. Eventually the suite is a map of your intuition rather than a specification of your logic.&lt;/p&gt;
&lt;p&gt;Property-based testing attacks the root cause: your “spec” should be the property, not the list of examples.&lt;/p&gt;
&lt;h2 id="the-core-idea-specify-invariants-not-inputs"&gt;The core idea: specify invariants, not inputs&lt;/h2&gt;
&lt;p&gt;In property-based testing, you write properties—statements that should hold for &lt;em&gt;all&lt;/em&gt; valid inputs. The framework then generates random inputs and checks whether the property holds. If it fails, it shrinks the failing input to a minimal counterexample. That last part matters: instead of “your test failed,” you get a small, human-readable input that breaks your assumption.&lt;/p&gt;
&lt;p&gt;A simple example: suppose you have a function that normalizes whitespace in a string. A typical example-based test might check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;input &lt;code&gt;&amp;quot;hello world&amp;quot;&lt;/code&gt; → output &lt;code&gt;&amp;quot;hello world&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;input &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; → output &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the real invariant is usually stronger:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After normalization, the string contains no consecutive whitespace sequences.&lt;/li&gt;
&lt;li&gt;Normalizing twice is the same as normalizing once (idempotence).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With property-based testing, you express those invariants and let the generator discover cases like &lt;code&gt;&amp;quot; \t\n &amp;quot;&lt;/code&gt; or strings full of weird spacing you didn’t think to include.&lt;/p&gt;
&lt;h3 id="a-concrete-typescript-example-with-fast-check"&gt;A concrete TypeScript example with fast-check&lt;/h3&gt;
&lt;p&gt;Let’s say we have a &lt;code&gt;normalizeSpaces&lt;/code&gt; function and we want to ensure idempotence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Property: &lt;code&gt;normalizeSpaces(normalizeSpaces(s)) === normalizeSpaces(s)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In &lt;code&gt;fast-check&lt;/code&gt;, that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fc&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;fast-check&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;normalizeSpaces&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;s&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;)&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;s&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;replace&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/\s+/g&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34; &amp;#34;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;trim&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;fc&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;assert&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fc&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;property&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;fc&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;(), (&lt;span style="color:#a6e22e"&gt;s&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;once&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;normalizeSpaces&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;s&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;twice&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;normalizeSpaces&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;once&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;twice&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;once&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the absence of hand-picked examples. We didn’t write tests for empty strings, whitespace-only strings, or long strings—we simply told the system what must always be true.&lt;/p&gt;
&lt;h2 id="edge-cases-you-didnt-write-become-the-point"&gt;Edge cases you didn’t write become the point&lt;/h2&gt;
&lt;p&gt;The strongest argument for property-based testing is what happens when randomness hits the boundaries of your assumptions. Example-based suites often overweight “happy path” values: typical lengths, typical characters, typical shapes.&lt;/p&gt;
&lt;p&gt;Property-based testing pushes toward values that are systematically likely to expose bugs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Empty strings&lt;/strong&gt;: trimming, splitting, regex edge cases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Max/min integers&lt;/strong&gt;: overflow, off-by-one logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unicode boundary characters&lt;/strong&gt;: surrogate pairs, combining marks, grapheme clusters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deep nesting&lt;/strong&gt;: recursion limits, stack behavior, quadratic work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nullability / missing fields&lt;/strong&gt; (when you model them): optional logic that forgot a branch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what’s different: you’re not just hoping the test generator covers edge cases. You’re designing properties that remain meaningful across the entire input space. That turns “randomness” into a practical strategy for discovering failures.&lt;/p&gt;
&lt;h3 id="what-shrinking-buys-you"&gt;What “shrinking” buys you&lt;/h3&gt;
&lt;p&gt;When a property fails, frameworks typically shrink the input to a minimal counterexample. That’s the difference between a failing test you can debug and a failing test you dread.&lt;/p&gt;
&lt;p&gt;For example, if your “round trip” property fails for some complex string, shrinking might reduce it to something like &lt;code&gt;&amp;quot;a\u0301&amp;quot;&lt;/code&gt; (a base character plus a combining mark) or &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; (empty) rather than a 2,000-character monstrosity. You’ll understand what broke much faster—and you can add a targeted regression test once you know the real failure mode.&lt;/p&gt;
&lt;h2 id="how-to-choose-the-right-properties-and-avoid-cargo-culting"&gt;How to choose the right properties (and avoid cargo culting)&lt;/h2&gt;
&lt;p&gt;Not every invariant is a good property. A property should be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Expressive&lt;/strong&gt;: it describes real correctness, not just “it doesn’t crash.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checkable&lt;/strong&gt;: you can verify it quickly during tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable&lt;/strong&gt;: it won’t flake due to nondeterminism or time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aligned with domain rules&lt;/strong&gt;: properties should reflect your actual specification.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Common high-value properties include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotence&lt;/strong&gt;: &lt;code&gt;f(f(x)) == f(x)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Round-trip&lt;/strong&gt;: &lt;code&gt;decode(encode(x)) == x&lt;/code&gt; (when the inverse relationship is true)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ordering guarantees&lt;/strong&gt;: &lt;code&gt;sort(xs)&lt;/code&gt; is monotonically non-decreasing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Length / structure&lt;/strong&gt;: transformations preserve size or expected shape&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Algebraic laws&lt;/strong&gt;: associativity, commutativity (when relevant), identity elements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building APIs, another pragmatic property is validating normalization behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“After parsing and re-serializing, the canonical form is stable.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of asserting 10 examples, you assert canonical stability across thousands of generated inputs.&lt;/p&gt;
&lt;h3 id="practical-tip-start-with-metamorphic-properties"&gt;Practical tip: start with “metamorphic” properties&lt;/h3&gt;
&lt;p&gt;If you struggle to find direct invariants, look for metamorphic ones—relationships between inputs and outputs. For instance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you sort a list, sorting it again shouldn’t change it (idempotence).&lt;/li&gt;
&lt;li&gt;If you concatenate two inputs and apply a transformation, the result should match applying the transformation to each part (under the right conditions).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Metamorphic properties are often easier to express than full correctness proofs.&lt;/p&gt;
&lt;h2 id="fast-check-hypothesis-and-proptest-choose-what-fits-your-stack"&gt;fast-check, Hypothesis, and proptest: choose what fits your stack&lt;/h2&gt;
&lt;p&gt;Property-based testing isn’t one tool—it’s a mindset supported by different ecosystems.&lt;/p&gt;
&lt;h3 id="fast-check-typescript--javascript"&gt;fast-check (TypeScript / JavaScript)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fast-check&lt;/code&gt; integrates well with modern TypeScript projects. It has rich arbitraries (generators) for common data types, and its failure reports are typically straightforward. It’s especially convenient if your domain model already uses TypeScript types—your properties can mirror those types cleanly.&lt;/p&gt;
&lt;p&gt;A big win in practice: you can write properties that use your existing pure functions, without ceremony. For teams that already test with Jest/Vitest, &lt;code&gt;fast-check&lt;/code&gt; feels like a natural extension.&lt;/p&gt;
&lt;h3 id="hypothesis-python"&gt;Hypothesis (Python)&lt;/h3&gt;
&lt;p&gt;Hypothesis is famously effective at finding minimal failing cases. It encourages you to write properties rather than examples, and its approach to generating data tends to produce useful counterexamples quickly.&lt;/p&gt;
&lt;p&gt;In Python codebases, it’s a strong fit when you have lots of data transformation logic—parsers, validators, serialization routines, business rules—and you want confidence that your assumptions hold beyond the examples you wrote down.&lt;/p&gt;
&lt;h3 id="proptest-rust"&gt;proptest (Rust)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;proptest&lt;/code&gt; matches Rust’s strengths: you can model constraints precisely using strategies, generate data that respects invariants, and lean on Rust’s type system to keep your properties honest.&lt;/p&gt;
&lt;p&gt;If you’re working in Rust with complex enums, algebraic data types, or low-level correctness concerns, &lt;code&gt;proptest&lt;/code&gt; is often a near-perfect complement to the language’s emphasis on safety.&lt;/p&gt;
&lt;h2 id="put-it-into-your-workflow-without-boiling-the-ocean"&gt;Put it into your workflow without boiling the ocean&lt;/h2&gt;
&lt;p&gt;Teams often fail at property-based testing adoption for one reason: they try to rewrite everything at once. Don’t.&lt;/p&gt;
&lt;p&gt;Here’s a practical rollout plan that works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick one module with clear invariants&lt;/strong&gt;&lt;br&gt;
Parsing, normalization, transformations, encoding/decoding, and pure business logic are ideal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write one property that replaces a cluster of tests&lt;/strong&gt;&lt;br&gt;
If you currently have “input/output” tests for many variants, identify the shared invariant. For example, multiple tests of string normalization likely share idempotence and “no consecutive whitespace.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with small input domains&lt;/strong&gt;&lt;br&gt;
Don’t immediately generate huge nested structures. Increase complexity after the first green runs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use failing counterexamples as regression seeds&lt;/strong&gt;&lt;br&gt;
When a property fails, keep the minimal counterexample as a concrete unit test. Property-based tests find bugs; unit tests make sure they stay fixed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tune performance intentionally&lt;/strong&gt;&lt;br&gt;
Properties can be expensive if you generate unbounded structures or perform heavy computations per case. Set reasonable limits and avoid doing deep parsing inside the property unless that’s what you’re testing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do this well, you’ll see immediate payoff: fewer brittle tests, more confidence at the edges, and fewer “we didn’t consider that input” surprises in production.&lt;/p&gt;
&lt;h2 id="conclusion-your-test-suite-should-represent-the-specification-not-your-imagination"&gt;Conclusion: your test suite should represent the specification, not your imagination&lt;/h2&gt;
&lt;p&gt;Example-based tests are necessary, but they’re not sufficient. Property-based testing turns your correctness criteria into executable specifications and continuously challenges your assumptions with thousands of generated inputs—especially the boundary cases that intuition misses.&lt;/p&gt;
&lt;p&gt;Start with one property. Let the framework do the annoying work of generating cases you wouldn’t think to write. Then use the shrinking counterexamples to fix the real issues and lock them in. Once you’ve seen how quickly it finds bugs, you won’t go back.&lt;/p&gt;</content></item><item><title>Hono: The Ultralight Framework That Runs Everywhere</title><link>https://decastro.work/blog/hono-ultralight-framework-runs-everywhere/</link><pubDate>Tue, 08 Aug 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/hono-ultralight-framework-runs-everywhere/</guid><description>&lt;p&gt;If you’ve ever watched a tiny API project spiral into a dependency forest, Hono will feel like a wake-up call. It’s a web framework built for modern runtimes—edge included—without the baggage. The headline is simple: Hono is ultralight, standards-based, and designed to run the same code across Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, and Node.js. The result is faster iteration, cleaner middleware, and a TypeScript experience that doesn’t fight you.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever watched a tiny API project spiral into a dependency forest, Hono will feel like a wake-up call. It’s a web framework built for modern runtimes—edge included—without the baggage. The headline is simple: Hono is ultralight, standards-based, and designed to run the same code across Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, and Node.js. The result is faster iteration, cleaner middleware, and a TypeScript experience that doesn’t fight you.&lt;/p&gt;
&lt;h2 id="built-on-web-standards-not-framework-isms"&gt;Built on Web Standards (Not “Framework-isms”)&lt;/h2&gt;
&lt;p&gt;Hono’s core philosophy is that the web already provides a great set of primitives. Instead of inventing a new request/response model or expecting you to learn a framework-specific abstraction, it leans on Web Standards. That matters because it keeps your mental model stable across environments.&lt;/p&gt;
&lt;p&gt;In practice, this means you’re writing handlers that resemble how the web works: you accept requests, you return responses, and you use familiar HTTP concepts. When you deploy to an edge runtime, you’re not “porting” your application so much as moving it.&lt;/p&gt;
&lt;p&gt;Concrete example: imagine you have a lightweight “health” endpoint and an API key gate.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;hono&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/health&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;ok&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt; }))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/secret&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;token&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;header&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;authorization&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;token&lt;/span&gt; &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`Bearer &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;env&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;API_TOKEN&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;unauthorized&amp;#39;&lt;/span&gt; }, &lt;span style="color:#ae81ff"&gt;401&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;classified&amp;#39;&lt;/span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the shape: concise routes, predictable request access, clean JSON responses. It’s the kind of code you don’t mind maintaining, even when the app grows.&lt;/p&gt;
&lt;h2 id="the-real-superpower-one-codebase-many-runtimes"&gt;The Real Superpower: One Codebase, Many Runtimes&lt;/h2&gt;
&lt;p&gt;Most frameworks make you choose a runtime—then punish you when you try to move. Hono is the opposite. You can use the same framework code across multiple deployment targets, including edge platforms and standard Node.js.&lt;/p&gt;
&lt;p&gt;That isn’t just a convenience feature; it changes how you build. You can prototype locally, validate behavior quickly, and then ship to an edge runtime without rewriting the app’s architecture.&lt;/p&gt;
&lt;p&gt;Think about common teams and workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edge-first products&lt;/strong&gt;: you start at the edge to reduce latency and keep compute close to users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prototyping&lt;/strong&gt;: you build locally (or in CI) with the same framework you’ll deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling needs&lt;/strong&gt;: you may later split heavier work into separate services while keeping the HTTP edge surface lightweight.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hono fits this reality because it doesn’t treat edge as a special snowflake.&lt;/p&gt;
&lt;h2 id="middleware-that-doesnt-feel-like-a-mess"&gt;Middleware That Doesn’t Feel Like a Mess&lt;/h2&gt;
&lt;p&gt;Express-style middleware patterns are familiar, but they can become tangled—especially as you start composing cross-cutting concerns like auth, logging, CORS, rate limiting, and request shaping.&lt;/p&gt;
&lt;p&gt;Hono’s middleware system is intentionally clean. The interfaces are straightforward, and the flow stays readable. Instead of scattering logic in ad hoc route handlers, you can assemble behavior at the app level and reuse it consistently.&lt;/p&gt;
&lt;p&gt;A practical pattern is to define middleware for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt; (attach user context)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observability&lt;/strong&gt; (log request IDs, timing)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Input shaping&lt;/strong&gt; (validate/normalize request bodies)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, you can create a middleware that ensures a request has a specific header before allowing access:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;hono&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;use&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/api/*&amp;#39;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;apiKey&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;header&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;x-api-key&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;apiKey&lt;/span&gt; &lt;span style="color:#f92672"&gt;||&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;apiKey&lt;/span&gt; &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;env&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;API_KEY&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;missing or invalid api key&amp;#39;&lt;/span&gt; }, &lt;span style="color:#ae81ff"&gt;401&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;next&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/api/items&amp;#39;&lt;/span&gt;, (&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;([{ &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;1&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Widget&amp;#39;&lt;/span&gt; }])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the kind of middleware you can reason about. It’s not hiding control flow in clever tricks; it’s explicit about “check, then next.”&lt;/p&gt;
&lt;h2 id="routing-and-typescript-fast-to-write-hard-to-break"&gt;Routing and TypeScript: Fast to Write, Hard to Break&lt;/h2&gt;
&lt;p&gt;Hono’s routing is intuitive, and that matters more than people admit. If routing feels frictionless, you write endpoints more frequently—and that’s how teams end up with smaller, testable surfaces rather than one monolithic handler.&lt;/p&gt;
&lt;p&gt;The TypeScript experience is also first-class. You get strong autocomplete, predictable handler signatures, and a codebase that stays friendly as it grows. TypeScript isn’t a bolt-on here; it’s part of the design.&lt;/p&gt;
&lt;p&gt;Here’s a simple example of route-driven typing and safe response shaping:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;hono&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Item&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;app&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Hono&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;app&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;/items/:id&amp;#39;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;) &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Number(&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;req&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;param&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;Number.isFinite(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;)) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;error&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;invalid id&amp;#39;&lt;/span&gt; }, &lt;span style="color:#ae81ff"&gt;400&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;item&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;Item&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;Example Item&amp;#39;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;item&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The handler stays compact, but you’re still protected against sloppy assumptions. That’s how TypeScript should feel: helping without getting in your way.&lt;/p&gt;
&lt;h2 id="performance-without-the-ego-small-framework-big-output"&gt;Performance Without the Ego: Small Framework, Big Output&lt;/h2&gt;
&lt;p&gt;Hono is built to be lightweight—around 12KB and with zero dependencies. That matters because dependency graphs create overhead: more installs, more build steps, more places for things to break, and more time debugging version mismatch issues.&lt;/p&gt;
&lt;p&gt;The framework also targets performance realistically. It’s been benchmarked as faster than Express by an “embarrassing margin” in common scenarios. Even if you don’t obsess over microbenchmarks, the underlying point holds: fewer layers and fewer moving parts usually translate to better runtime efficiency and lower latency variance.&lt;/p&gt;
&lt;p&gt;Where this shows up in the real world:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Edge response times&lt;/strong&gt; feel snappier because the request path stays lean.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cold starts&lt;/strong&gt; are less painful because there’s less to load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer iteration&lt;/strong&gt; speeds up because the framework doesn’t drag your build pipeline around.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building an API gateway, a webhook receiver, a set of CRUD endpoints, or any “lots of requests, little logic” service, Hono is a strong default choice.&lt;/p&gt;
&lt;h2 id="when-to-use-hono-and-when-not-to"&gt;When to Use Hono (and When Not To)&lt;/h2&gt;
&lt;p&gt;Hono shines when you want modern HTTP behavior, clean composition, and runtime portability—especially for edge deployments and lightweight APIs.&lt;/p&gt;
&lt;p&gt;Use it when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’re deploying to &lt;strong&gt;edge runtimes&lt;/strong&gt; and want to keep the code portable.&lt;/li&gt;
&lt;li&gt;You want a &lt;strong&gt;minimal&lt;/strong&gt; server layer for an API or backend-for-frontend.&lt;/li&gt;
&lt;li&gt;You care about &lt;strong&gt;readable middleware composition&lt;/strong&gt; and predictable TypeScript.&lt;/li&gt;
&lt;li&gt;You’re building services that should be easy to split, scale, and maintain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Be cautious when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need heavy, deeply specialized ecosystem features that you already depend on in a different framework.&lt;/li&gt;
&lt;li&gt;Your team is deeply invested in a specific middleware ecosystem and doesn’t want to migrate patterns.&lt;/li&gt;
&lt;li&gt;You’re building something that’s less “HTTP service” and more “full application framework” with lots of built-in conventions—because Hono’s strength is staying focused.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My opinionated take: if your service is fundamentally an HTTP API, Hono is the kind of framework you’ll be glad you chose early. Migration later is harder than it should be—so pick the lightweight, standards-based option now.&lt;/p&gt;
&lt;h2 id="conclusion-a-modern-default-for-edge-and-apis"&gt;Conclusion: A Modern Default for Edge and APIs&lt;/h2&gt;
&lt;p&gt;Hono is what Express would look like if it were designed in 2023 for today’s runtime reality: standards-based, edge-friendly, dependency-free, and genuinely pleasant to use in TypeScript. It’s small enough to stay out of your way, fast enough to matter, and consistent enough to deploy confidently across multiple platforms.&lt;/p&gt;
&lt;p&gt;If you’re building APIs—especially edge-first ones—Hono deserves a spot in your toolkit.&lt;/p&gt;</content></item><item><title>tRPC Made Me Forget REST Exists</title><link>https://decastro.work/blog/trpc-made-me-forget-rest-exists/</link><pubDate>Thu, 27 Jul 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/trpc-made-me-forget-rest-exists/</guid><description>&lt;p&gt;REST isn’t “dead,” but after using tRPC in a TypeScript-heavy codebase, it’s hard to unsee how much ceremony you inherit—versioning chaos, schema drift, and that nagging question of whether the backend and frontend still agree.&lt;/p&gt;
&lt;p&gt;tRPC doesn’t just make API calls &lt;em&gt;feel&lt;/em&gt; nicer. It removes the API contract problem at the source: the same TypeScript types that describe your server procedure become available to your client automatically—no OpenAPI spec, no code generation, and far fewer “surprise” mismatches.&lt;/p&gt;</description><content>&lt;p&gt;REST isn’t “dead,” but after using tRPC in a TypeScript-heavy codebase, it’s hard to unsee how much ceremony you inherit—versioning chaos, schema drift, and that nagging question of whether the backend and frontend still agree.&lt;/p&gt;
&lt;p&gt;tRPC doesn’t just make API calls &lt;em&gt;feel&lt;/em&gt; nicer. It removes the API contract problem at the source: the same TypeScript types that describe your server procedure become available to your client automatically—no OpenAPI spec, no code generation, and far fewer “surprise” mismatches.&lt;/p&gt;
&lt;h2 id="the-real-problem-isnt-httpits-the-contract"&gt;The real problem isn’t HTTP—it’s the contract&lt;/h2&gt;
&lt;p&gt;REST is fundamentally an HTTP convention. The contract—the part everyone actually cares about—is usually captured somewhere else: an OpenAPI document, a hand-written JSON schema, a shared type package, or the worst option of all: documentation that developers interpret differently.&lt;/p&gt;
&lt;p&gt;That’s where problems breed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Input shape drift:&lt;/strong&gt; frontend sends &lt;code&gt;{ userId: &amp;quot;123&amp;quot; }&lt;/code&gt;, backend expects &lt;code&gt;{ id: number }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Output shape drift:&lt;/strong&gt; frontend reads &lt;code&gt;data.items&lt;/code&gt;, backend returns &lt;code&gt;data.results&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version mismatches:&lt;/strong&gt; you deploy the backend, the frontend lags, and nobody realizes the procedure signature changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintenance overhead:&lt;/strong&gt; keeping schemas, generators, and types in sync becomes a job of its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can paper over this with discipline, but discipline doesn’t fix structural risk. You can be the best engineer on the team and still get burned when the system encourages divergence.&lt;/p&gt;
&lt;h2 id="what-trpc-changes-one-source-of-truth-for-both-sides"&gt;What tRPC changes: one source of truth for both sides&lt;/h2&gt;
&lt;p&gt;With tRPC, you define &lt;em&gt;procedures&lt;/em&gt; on the server in TypeScript. The client consumes them via a type-safe API that’s inferred from the server definition.&lt;/p&gt;
&lt;p&gt;The important part: the contract is not “described” in a separate artifact. It is the code itself.&lt;/p&gt;
&lt;p&gt;Here’s the basic shape of the mental model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server:&lt;/strong&gt; you declare a procedure with input validation and an output type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client:&lt;/strong&gt; you call that procedure and TypeScript already knows the input/output types.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync by construction:&lt;/strong&gt; change the server procedure, and the client immediately reflects the new types (or fails to compile).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No OpenAPI spec. No generator pipeline. No “run codegen, commit output, pray.” If you’re using a TypeScript monorepo, it gets even cleaner because the server router and client can share types directly.&lt;/p&gt;
&lt;h3 id="a-concrete-example-getuser-that-cant-drift"&gt;A concrete example: “getUser” that can’t drift&lt;/h3&gt;
&lt;p&gt;Suppose your server has a procedure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;input: &lt;code&gt;{ userId: string }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;output: &lt;code&gt;{ id: string; name: string; role: &amp;quot;admin&amp;quot; | &amp;quot;user&amp;quot; }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With tRPC, when you call it from the client:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TypeScript will enforce the input shape.&lt;/li&gt;
&lt;li&gt;TypeScript will tell you exactly what fields exist in the result.&lt;/li&gt;
&lt;li&gt;If you rename &lt;code&gt;userId&lt;/code&gt; to &lt;code&gt;id&lt;/code&gt;, or add/remove a field, the client compile fails right away.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not “nice autocomplete.” That’s eliminating a whole class of integration bugs before runtime even has a chance to happen.&lt;/p&gt;
&lt;h2 id="compile-time-safety-isnt-enoughzod-closes-the-runtime-gap"&gt;Compile-time safety isn’t enough—Zod closes the runtime gap&lt;/h2&gt;
&lt;p&gt;If you stop at TypeScript inference, you still have a runtime problem: the network doesn’t care about your types. A malicious client (or a buggy client) can send invalid data.&lt;/p&gt;
&lt;p&gt;tRPC typically pairs with &lt;strong&gt;Zod&lt;/strong&gt; (or similar validators) so you get the best of both worlds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compile-time types:&lt;/strong&gt; derived from the Zod schema and the procedure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime validation:&lt;/strong&gt; enforced on the server before your logic runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means your server doesn’t just &lt;em&gt;hope&lt;/em&gt; the input matches—it checks it. And your client doesn’t just &lt;em&gt;trust&lt;/em&gt; the types—it’s guided by them.&lt;/p&gt;
&lt;h3 id="practical-takeaway-validate-at-the-boundary-type-everywhere"&gt;Practical takeaway: validate at the boundary, type everywhere&lt;/h3&gt;
&lt;p&gt;A pattern I now treat as non-negotiable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use Zod to define the input schema.&lt;/li&gt;
&lt;li&gt;Let tRPC infer the TypeScript types from that schema.&lt;/li&gt;
&lt;li&gt;Keep business logic focused on valid inputs, not defensive parsing everywhere.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your procedure becomes easier to read because you remove a layer of clutter—“validate this, check that, handle weird shapes”—and move it into a clear schema definition.&lt;/p&gt;
&lt;h2 id="eliminating-contract-maintenance-changes-how-you-ship"&gt;Eliminating “contract maintenance” changes how you ship&lt;/h2&gt;
&lt;p&gt;Once the contract is shared automatically, development velocity improves in a way that’s hard to quantify but easy to feel.&lt;/p&gt;
&lt;h3 id="you-refactor-without-fear"&gt;You refactor without fear&lt;/h3&gt;
&lt;p&gt;In traditional REST setups, refactoring a request or response means juggling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;update backend code&lt;/li&gt;
&lt;li&gt;update OpenAPI docs (if you have them)&lt;/li&gt;
&lt;li&gt;regenerate client types (if you have them)&lt;/li&gt;
&lt;li&gt;audit call sites across the app&lt;/li&gt;
&lt;li&gt;coordinate deployments so clients don’t break&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With tRPC, the “audit across the app” part becomes the compiler’s job. When you change the server procedure signature, the client sees it immediately. You fix broken call sites as you go, and you don’t ship until everything compiles cleanly.&lt;/p&gt;
&lt;h3 id="you-dont-design-contracts-separately-from-code"&gt;You don’t design contracts separately from code&lt;/h3&gt;
&lt;p&gt;REST often leads you to think: “First we define the API. Then we implement it.” That separation sounds tidy, but it creates drift. tRPC collapses that workflow. You build the procedure, define the validation, define the return type, and the client just follows.&lt;/p&gt;
&lt;p&gt;This has a subtle effect: API design becomes less of a heavyweight phase and more of an iterative practice.&lt;/p&gt;
&lt;h2 id="rest-and-graphql-feel-like-ceremony-when-typescript-does-the-syncing"&gt;REST and GraphQL feel like ceremony when TypeScript does the syncing&lt;/h2&gt;
&lt;p&gt;tRPC also changes the emotional comparison. Without code generation, without schemas-as-docs, without “versioned contracts,” REST and GraphQL start to feel like you’re doing extra work to keep two systems aligned.&lt;/p&gt;
&lt;p&gt;REST still has strengths—caching semantics, broad ecosystem support, and a universal language for HTTP clients. GraphQL still offers flexible querying and tooling around schemas and resolvers.&lt;/p&gt;
&lt;p&gt;But if your world is already TypeScript end-to-end, tRPC brings the contract problem to a stop.&lt;/p&gt;
&lt;h3 id="the-monorepo-advantage-is-real"&gt;The monorepo advantage is real&lt;/h3&gt;
&lt;p&gt;In a TypeScript monorepo, the server and client often evolve together. tRPC turns that advantage into a workflow: shared types, inferred procedure signatures, and a unified developer experience.&lt;/p&gt;
&lt;p&gt;If you’re working across package boundaries, you also get a practical enforcement mechanism: changes to the server router break the client build immediately. That’s the kind of feedback loop you want—fast, local, and unavoidable.&lt;/p&gt;
&lt;h2 id="where-trpc-fits-best-and-where-you-should-still-be-careful"&gt;Where tRPC fits best (and where you should still be careful)&lt;/h2&gt;
&lt;p&gt;Let’s be honest: you shouldn’t declare REST irrelevant in every context. The value of tRPC is strongest when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;your stack is TypeScript-heavy&lt;/li&gt;
&lt;li&gt;backend and frontend live in the same repo (or at least share types through a package boundary)&lt;/li&gt;
&lt;li&gt;you care deeply about developer experience and integration correctness&lt;/li&gt;
&lt;li&gt;your API surface benefits from procedure-level modeling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You still need to think about compatibility for external consumers. If third-party clients must integrate, you may still need public, language-agnostic contracts (OpenAPI, documentation, or a published schema). tRPC doesn’t magically replace that. It excels at the internal “frontend ↔ backend” relationship where TypeScript can be the contract layer.&lt;/p&gt;
&lt;p&gt;Also, don’t treat runtime validation as optional. Type inference doesn’t validate network inputs; Zod does.&lt;/p&gt;
&lt;h2 id="conclusion-stop-negotiating-contractsmake-them-compile"&gt;Conclusion: stop negotiating contracts—make them compile&lt;/h2&gt;
&lt;p&gt;tRPC “made me forget REST exists” not because HTTP disappeared, but because the contract stopped being fragile. When the backend procedure definition and the frontend usage share types through inference, you eliminate drift by construction. Add Zod validation and you get runtime safety too.&lt;/p&gt;
&lt;p&gt;If your team lives in TypeScript and you want fewer moving parts—no OpenAPI file, no codegen pipeline, fewer version mismatch headaches—tRPC is one of the rare upgrades that changes both the reliability &lt;em&gt;and&lt;/em&gt; the feel of building APIs.&lt;/p&gt;</content></item><item><title>Next.js App Router: Brilliant Architecture, Terrible Developer Experience</title><link>https://decastro.work/blog/nextjs-app-router-brilliant-terrible-dx/</link><pubDate>Sat, 15 Jul 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/nextjs-app-router-brilliant-terrible-dx/</guid><description>&lt;p&gt;Next.js App Router is the rare framework feature that feels like a major step forward—right up until you try to use it on a real codebase. The architecture is genuinely exciting: server components, streaming SSR, and caching controls that promise to eliminate whole classes of client-side waterfalls. But the migration from the Pages Router? That’s where the glow fades. In practice, you’re handed unpredictable caching behavior, confusing mental models, missing documentation details, and error messages that read like they were generated by a haunted toaster.&lt;/p&gt;</description><content>&lt;p&gt;Next.js App Router is the rare framework feature that feels like a major step forward—right up until you try to use it on a real codebase. The architecture is genuinely exciting: server components, streaming SSR, and caching controls that promise to eliminate whole classes of client-side waterfalls. But the migration from the Pages Router? That’s where the glow fades. In practice, you’re handed unpredictable caching behavior, confusing mental models, missing documentation details, and error messages that read like they were generated by a haunted toaster.&lt;/p&gt;
&lt;p&gt;If you’re deciding whether to adopt App Router now, here’s the unvarnished truth: it will likely be excellent in a year. Today, it’s a beta disguised as stability.&lt;/p&gt;
&lt;h2 id="the-real-win-server-components-that-actually-change-the-game"&gt;The Real Win: Server Components That Actually Change the Game&lt;/h2&gt;
&lt;p&gt;App Router’s central idea is React Server Components (RSC): components rendered on the server by default, with no need to ship their code to the browser. That architectural choice eliminates a lot of the “fetch everything in a client effect” anti-pattern. Instead of rendering a shell and then progressively hydrating data, you can fetch data where it belongs.&lt;/p&gt;
&lt;p&gt;A typical App Router pattern looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt; routes define your UI as nested layouts and pages.&lt;/li&gt;
&lt;li&gt;Server components are the default.&lt;/li&gt;
&lt;li&gt;Client components are opt-in via &lt;code&gt;&amp;quot;use client&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a route might fetch directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// app/products/[id]/page.tsx (server component by default)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductPage&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;params&lt;/span&gt; }&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; } }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;product&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getProduct&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;params&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// server-side fetch
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;{&lt;span style="color:#a6e22e"&gt;product&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;}&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The practical benefit is immediate: no client waterfall waiting for a &lt;code&gt;useEffect&lt;/code&gt; to run before data appears. Combined with streaming SSR, users can see meaningful UI quickly—sometimes before the entire tree resolves.&lt;/p&gt;
&lt;p&gt;On paper, this is exactly how modern web apps should work: push work to the server, reduce bundle size, and keep rendering deterministic.&lt;/p&gt;
&lt;h2 id="but-the-migration-is-where-productivity-goes-to-die"&gt;But the Migration Is Where Productivity Goes to Die&lt;/h2&gt;
&lt;p&gt;The Pages Router and App Router share a name—“Next.js”—but they don’t share a developer experience. Moving an existing app is less like upgrading and more like rewriting.&lt;/p&gt;
&lt;p&gt;In Pages Router, you might rely on familiar primitives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pages/&lt;/code&gt; routes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getServerSideProps&lt;/code&gt; / &lt;code&gt;getStaticProps&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next/head&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;predictable client/server boundaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In App Router, you’re instead navigating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app/&lt;/code&gt; segments, layouts, and route groups&lt;/li&gt;
&lt;li&gt;server components by default&lt;/li&gt;
&lt;li&gt;data loading through server code (often inside components)&lt;/li&gt;
&lt;li&gt;a different approach to head management and metadata&lt;/li&gt;
&lt;li&gt;caching semantics tied to both fetch and route behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first “gotcha” most teams hit is that boundaries are enforced differently. You might have an existing component that “used to be safe” to import anywhere, but in App Router it can silently become a client component (or fail when you accidentally use server-only features in the wrong place). The fix is rarely just one line—it’s often a refactor of how data and UI are separated.&lt;/p&gt;
&lt;p&gt;Here’s a common trap: importing something that uses browser APIs into a component that App Router expects to run on the server. You then discover that the fix involves carving the component into server + client halves, and passing data through props (which changes serialization constraints).&lt;/p&gt;
&lt;p&gt;Even experienced Next.js developers end up spending time on “plumbing work” that doesn’t deliver user value: reorganizing folders, splitting components, chasing build errors, and re-learning what runs where.&lt;/p&gt;
&lt;h2 id="caching-the-promise-of-control-the-reality-of-confusion"&gt;Caching: The Promise of Control, the Reality of Confusion&lt;/h2&gt;
&lt;p&gt;Next.js App Router’s caching story is one of its biggest selling points: you can aim for granular caching with the framework doing the heavy lifting. And yes, the system is powerful. But the behavior can feel like you’re wrestling a black box—especially when you’re migrating from a system where caching was explicit and isolated.&lt;/p&gt;
&lt;p&gt;The core tension is simple: caching is now tied to the semantics of fetch and the rendering lifecycle. That means small changes—moving a fetch into a server component, changing where a value is computed, tweaking fetch options—can change the caching outcome.&lt;/p&gt;
&lt;p&gt;In practical terms, teams often experience some combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“It works locally but not in production.”&lt;/li&gt;
&lt;li&gt;“My page updates sometimes, but not consistently.”&lt;/li&gt;
&lt;li&gt;“I expected revalidation to happen, but stale data persists longer than I can explain.”&lt;/li&gt;
&lt;li&gt;“Changing the code invalidates cache in one environment but not another.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What helps is adopting a disciplined approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Decide your caching policy per request type&lt;/strong&gt;, not per developer preference. For example: “product detail pages are revalidated every 60 seconds” vs “admin pages are never cached.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep data-fetching logic in one place&lt;/strong&gt; (e.g., a &lt;code&gt;lib/&lt;/code&gt; function) so you don’t end up with caching differences across components.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instrument and verify.&lt;/strong&gt; Don’t guess. Add visible “data age” markers in development, and log request identifiers server-side so you can tell which requests were served from cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use fetch options intentionally.&lt;/strong&gt; Treat &lt;code&gt;cache&lt;/code&gt; and revalidation-related settings as part of your product requirements, not implementation details you can sprinkle later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do nothing else, make caching testable. App Router makes caching powerful enough that you should demand it behave predictably.&lt;/p&gt;
&lt;h2 id="error-messages-and-documentation-gaps-youll-learn-through-pain"&gt;Error Messages and Documentation Gaps: You’ll Learn Through Pain&lt;/h2&gt;
&lt;p&gt;The architecture is modern. The developer experience is… not.&lt;/p&gt;
&lt;p&gt;App Router error messages can be cryptic in a way that wastes hours. The framework sometimes tells you what you did wrong without providing actionable context—especially around server/client boundaries and invalid imports. You’ll see failures that feel detached from the line of code that triggered them, because the underlying problem may occur earlier in the render tree or during build compilation.&lt;/p&gt;
&lt;p&gt;Documentation gaps are the second productivity killer. You’ll find pages that explain the concepts, but not always the edge cases you need during migration. The missing details tend to be exactly where teams get stuck:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how to structure layouts and nested routes for common patterns&lt;/li&gt;
&lt;li&gt;how metadata and head-like concerns map from Pages Router equivalents&lt;/li&gt;
&lt;li&gt;what runs where when you mix server logic with interactive UI&lt;/li&gt;
&lt;li&gt;how to reason about caching when code structure changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The “truck-sized gap” feeling isn’t because documentation is absent—it’s because it’s incomplete for the workflow you’re actually trying to execute. You’ll often end up triangulating from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;examples that assume a greenfield project&lt;/li&gt;
&lt;li&gt;GitHub issues with partial answers&lt;/li&gt;
&lt;li&gt;“tribal knowledge” from other teams who already banged their heads against the same wall&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My advice is to plan for that reality. If your roadmap depends on migrating dozens of routes quickly, you’ll want a buffer for experimentation and refactoring. Treat the migration as a project, not a simple branch upgrade.&lt;/p&gt;
&lt;h2 id="a-better-migration-strategy-build-a-safe-path-not-a-leap-of-faith"&gt;A Better Migration Strategy: Build a Safe Path, Not a Leap of Faith&lt;/h2&gt;
&lt;p&gt;If you’re moving to App Router, don’t do it as a big-bang rewrite. Do it like you’re managing risk—because you are.&lt;/p&gt;
&lt;p&gt;Here’s a pragmatic strategy that works better than “convert everything”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with low-risk routes.&lt;/strong&gt; Public content pages, simple lists, and read-only detail pages are great candidates. Save authentication-heavy and highly interactive screens for later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a compatibility plan for shared components.&lt;/strong&gt; Establish conventions:
&lt;ul&gt;
&lt;li&gt;Server components by default&lt;/li&gt;
&lt;li&gt;Client components only where interactivity is required&lt;/li&gt;
&lt;li&gt;A clear folder structure for UI primitives vs data logic&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralize your data layer.&lt;/strong&gt; Put fetching and transformation in &lt;code&gt;lib/&lt;/code&gt; functions so caching policies don’t drift across components.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write migration tests.&lt;/strong&gt; Not just unit tests—behavior checks.
&lt;ul&gt;
&lt;li&gt;Confirm loading states render as expected.&lt;/li&gt;
&lt;li&gt;Confirm data freshness matches your caching intent.&lt;/li&gt;
&lt;li&gt;Confirm redirects and error pages behave correctly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document the rules your team adopts.&lt;/strong&gt; For example: “No browser APIs in server components” or “All fetches go through &lt;code&gt;lib/&lt;/code&gt;.” The goal is to prevent accidental inconsistencies that are hard to debug later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The win here is that you reduce the scope of uncertainty. App Router is best when you can isolate its effects while you learn the mechanics.&lt;/p&gt;
&lt;p&gt;And yes: you may need to “unlearn” some habits from Pages Router. But if you approach it as incremental learning with guardrails, the migration becomes survivable—and eventually rewarding.&lt;/p&gt;
&lt;h2 id="conclusion-the-architecture-deserves-the-attentionthe-adoption-needs-guardrails"&gt;Conclusion: The Architecture Deserves the Attention—The Adoption Needs Guardrails&lt;/h2&gt;
&lt;p&gt;Next.js App Router is a real architectural breakthrough that aligns with where React is headed: server-first rendering, streaming SSR, and caching control that can eliminate performance footguns. But today’s developer experience—especially during migration—is still rough around the edges. Caching behavior can feel unintuitive, documentation gaps will cost time, and error messages won’t always help you fix the real underlying issue.&lt;/p&gt;
&lt;p&gt;So here’s the bottom line: adopt App Router when you can afford iterative migration and you’re willing to treat caching and boundaries as first-class concerns. Give it time, build with discipline, and you’ll likely end up with a codebase that performs better than what Pages Router could ever deliver. Jump in recklessly, and you’ll spend the next few weeks debugging the framework instead of building your product.&lt;/p&gt;</content></item><item><title>OpenTelemetry Is the Observability Standard You Should Adopt Now</title><link>https://decastro.work/blog/opentelemetry-observability-standard-adopt-now/</link><pubDate>Sat, 08 Jul 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/opentelemetry-observability-standard-adopt-now/</guid><description>&lt;p&gt;If your observability stack feels like a set of bespoke duct-tape integrations—each new service, new vendor, new dashboard, new “how did we even measure that?”—OpenTelemetry is the reset button. It’s vendor-neutral instrumentation designed to work across backends, so you instrument once and route telemetry anywhere you want. That means fewer rewrites, less vendor lock-in, and a cleaner path to better reliability.&lt;/p&gt;
&lt;h2 id="why-instrumentation-should-not-mean-vendor-lock-in"&gt;Why “instrumentation” should not mean “vendor lock-in”&lt;/h2&gt;
&lt;p&gt;Most teams don’t start with lock-in—they start with urgency. Something goes wrong, logs are noisy, metrics are inconsistent, traces are missing, and suddenly you’re “just integrating with Vendor X” to ship a fix. The problem is that instrumentation tends to calcify. Dashboards and alerts become tightly coupled to the vendor’s data model and query language. Even worse, app code often ends up calling vendor SDKs directly.&lt;/p&gt;</description><content>&lt;p&gt;If your observability stack feels like a set of bespoke duct-tape integrations—each new service, new vendor, new dashboard, new “how did we even measure that?”—OpenTelemetry is the reset button. It’s vendor-neutral instrumentation designed to work across backends, so you instrument once and route telemetry anywhere you want. That means fewer rewrites, less vendor lock-in, and a cleaner path to better reliability.&lt;/p&gt;
&lt;h2 id="why-instrumentation-should-not-mean-vendor-lock-in"&gt;Why “instrumentation” should not mean “vendor lock-in”&lt;/h2&gt;
&lt;p&gt;Most teams don’t start with lock-in—they start with urgency. Something goes wrong, logs are noisy, metrics are inconsistent, traces are missing, and suddenly you’re “just integrating with Vendor X” to ship a fix. The problem is that instrumentation tends to calcify. Dashboards and alerts become tightly coupled to the vendor’s data model and query language. Even worse, app code often ends up calling vendor SDKs directly.&lt;/p&gt;
&lt;p&gt;OpenTelemetry (OTel) changes the default workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You instrument your code with OTel SDKs (or via auto-instrumentation).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You export telemetry through a standardized protocol (OTLP).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You configure where it goes—Jaeger, Grafana, Datadog, or any OTLP-compatible backend—without reworking application code.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, this means you can start with one backend today and keep the option to migrate later. You’re not buying observability twice: once in your vendor relationship, and again in the engineering effort required to leave.&lt;/p&gt;
&lt;h2 id="instrument-once-traces-metrics-and-logs-in-one-system"&gt;Instrument once: traces, metrics, and logs in one system&lt;/h2&gt;
&lt;p&gt;OpenTelemetry isn’t only for traces. A modern observability program needs all three pillars:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Traces&lt;/strong&gt; to understand request flow and latency bottlenecks across services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; to track performance over time (latency percentiles, error rates, saturation signals).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs&lt;/strong&gt; to capture event context that doesn’t fit traces or metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is &lt;strong&gt;consistent correlation&lt;/strong&gt;. When your trace IDs and resource attributes are standardized, you can connect “what happened” to “why it happened” without manual glue code.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine an e-commerce checkout pipeline.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You instrument the checkout service (OTel spans for inbound HTTP requests, outgoing calls, database queries).&lt;/li&gt;
&lt;li&gt;You export metrics like request latency and error count.&lt;/li&gt;
&lt;li&gt;You include log records that automatically attach trace context.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, when a user reports a timeout, your on-call flow becomes: open trace → see which downstream dependency slowed down → jump to the matching logs/events → correlate with metrics trends. You stop treating observability as disconnected artifacts and start treating it as a navigable system.&lt;/p&gt;
&lt;h2 id="the-real-power-move-the-collector-as-your-routing-brain"&gt;The real power move: the Collector as your routing brain&lt;/h2&gt;
&lt;p&gt;Here’s where teams either win or stall. If you export telemetry directly from every app to every backend, you reintroduce complexity and brittleness. The OpenTelemetry Collector fixes that by acting as a &lt;strong&gt;central telemetry processing layer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of the Collector as your “observability gateway.” It can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Receive telemetry&lt;/strong&gt; from your applications (or agents).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transform and enrich&lt;/strong&gt; data (add or normalize attributes).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filter&lt;/strong&gt; noisy signals so you don’t drown in cost or irrelevant detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch and retry&lt;/strong&gt; to improve reliability of exports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Route&lt;/strong&gt; telemetry to one or more backends—often simultaneously.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Operationally, this is gold. You can change routing or processing rules without shipping application updates. For instance, during an incident you might temporarily increase sampling for one service to get higher-fidelity traces, then dial it back once stability returns. With the Collector, that’s a configuration change, not a code release.&lt;/p&gt;
&lt;h2 id="auto-instrumentation-baseline-observability-without-developer-tax"&gt;Auto-instrumentation: baseline observability without developer tax&lt;/h2&gt;
&lt;p&gt;Manual instrumentation is valuable, but it’s not sustainable as a universal strategy. You want “good enough” coverage immediately—especially for common libraries like HTTP frameworks, database drivers, and messaging clients.&lt;/p&gt;
&lt;p&gt;OpenTelemetry supports &lt;strong&gt;auto-instrumentation&lt;/strong&gt; for many ecosystems, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You get default spans for inbound/outbound requests.&lt;/li&gt;
&lt;li&gt;You capture key attributes automatically (route, method, peer service, etc.).&lt;/li&gt;
&lt;li&gt;You reduce the cognitive load on developers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t about removing all manual work. It’s about ensuring that the first deploy already produces useful traces and metrics. Then developers can selectively add custom spans around business-critical operations—like “checkout authorization” or “credit card verification”—to make traces meaningful to humans.&lt;/p&gt;
&lt;p&gt;A practical workflow many teams adopt:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deploy auto-instrumentation broadly to establish coverage.&lt;/li&gt;
&lt;li&gt;Turn on Collector-based enrichment (service names, environments, tenancy keys, etc.).&lt;/li&gt;
&lt;li&gt;Add targeted manual spans for workflows that matter most.&lt;/li&gt;
&lt;li&gt;Use dashboards and alerts to validate signal quality.&lt;/li&gt;
&lt;li&gt;Iterate once the system is live—without rewriting from scratch.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="choose-your-backend-now-and-keep-your-options-later"&gt;Choose your backend now (and keep your options later)&lt;/h2&gt;
&lt;p&gt;The strongest argument for OTel is not theoretical portability—it’s strategic flexibility. If your environment supports OTLP, you can forward the same telemetry to different backends.&lt;/p&gt;
&lt;p&gt;Examples of common setups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local development with Jaeger&lt;/strong&gt; for quick trace exploration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production with a managed backend&lt;/strong&gt; like Grafana-based tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel export&lt;/strong&gt; during migrations, so you validate new dashboards against existing traces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost-aware routing&lt;/strong&gt;: keep high-cardinality detail for a subset of traffic, and aggregate the rest.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re currently locked into a single vendor, OpenTelemetry doesn’t demand you abandon everything overnight. You can start by integrating OTel while continuing to use your current backend. Then, as you validate parity (dashboards, alerting logic, trace navigation), you can gradually shift exports.&lt;/p&gt;
&lt;p&gt;This is also how you avoid paying twice. Vendor pricing models often punish growth in ways that are hard to predict. Moving to a routing architecture where you can control what you send—and where—gives you leverage. You’re not trapped into one pricing scheme for the entire lifespan of your platform.&lt;/p&gt;
&lt;p&gt;And yes, the economics matter. If you’re paying per-host or per-agent for a commercial backend, it’s worth comparing what you can achieve with an OTLP-compatible path that fits your budget. For example, teams that already use Grafana tooling may find meaningful value using Grafana Cloud’s free tier for initial onboarding and evaluation—especially when combined with OTel’s ability to export once and route anywhere. (Treat “80% of the value” as a goal, not a guarantee: the real win is that you can measure your results quickly and adjust.)&lt;/p&gt;
&lt;h2 id="a-practical-adoption-plan-that-wont-break-your-week"&gt;A practical adoption plan that won’t break your week&lt;/h2&gt;
&lt;p&gt;Adopting OpenTelemetry can feel intimidating, but it doesn’t have to be a rewrite. Here’s a pragmatic path that minimizes risk:&lt;/p&gt;
&lt;h3 id="1-pick-a-language-and-start-with-one-service"&gt;1) Pick a language and start with one service&lt;/h3&gt;
&lt;p&gt;Choose a service that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;is already instrumented with some logging/metrics,&lt;/li&gt;
&lt;li&gt;handles meaningful user traffic,&lt;/li&gt;
&lt;li&gt;and has clear downstream dependencies.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start with traces first—because they immediately unlock root-cause analysis.&lt;/p&gt;
&lt;h3 id="2-enable-auto-instrumentation-then-verify-output"&gt;2) Enable auto-instrumentation, then verify output&lt;/h3&gt;
&lt;p&gt;Bring up your Collector and confirm you see spans end-to-end. Don’t worry about perfect attribute naming on day one. The goal is signal flow: instrumentation → Collector → backend.&lt;/p&gt;
&lt;h3 id="3-standardize-resource-attributes-early"&gt;3) Standardize “resource” attributes early&lt;/h3&gt;
&lt;p&gt;Decide on consistent values for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;service.name&lt;/li&gt;
&lt;li&gt;service.version (if available)&lt;/li&gt;
&lt;li&gt;environment (prod/staging/dev)&lt;/li&gt;
&lt;li&gt;deployment metadata (region, cluster, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is what makes dashboards useful instead of confusing.&lt;/p&gt;
&lt;h3 id="4-add-filtering-and-sampling-rules-in-the-collector"&gt;4) Add filtering and sampling rules in the Collector&lt;/h3&gt;
&lt;p&gt;Cost control belongs at the Collector layer. Start with conservative defaults:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sample high-volume endpoints more aggressively,&lt;/li&gt;
&lt;li&gt;keep error traces at higher priority (via status-based sampling if supported),&lt;/li&gt;
&lt;li&gt;drop or reduce noisy attributes that explode cardinality.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-roll-out-iteratively-and-teach-the-team-the-workflow"&gt;5) Roll out iteratively and teach the team the workflow&lt;/h3&gt;
&lt;p&gt;Once traces exist, your team needs a shared mental model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Start with trace, then pivot to logs and metrics.”&lt;/li&gt;
&lt;li&gt;“Use service boundaries and span names consistently.”&lt;/li&gt;
&lt;li&gt;“When you deploy, you should be able to see the impact within minutes.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That cultural piece is the difference between “we installed OTel” and “we use OTel to get better.”&lt;/p&gt;
&lt;h2 id="conclusion-adopt-otel-now-then-refine-with-confidence"&gt;Conclusion: adopt OTel now, then refine with confidence&lt;/h2&gt;
&lt;p&gt;OpenTelemetry isn’t just another observability tool—it’s a standard approach to instrumentation that protects you from vendor lock-in and reduces future migration pain. By instrumenting once and using the Collector to route, transform, and control telemetry, you gain flexibility, cost leverage, and a cleaner operational model. Start small with auto-instrumentation, verify end-to-end traces, then iterate into richer metrics and logs correlation. If your observability strategy is still tightly coupled to a single vendor, OpenTelemetry is the modern way out.&lt;/p&gt;</content></item><item><title>WebAssembly Will Eat the World (Just Not How You Think)</title><link>https://decastro.work/blog/webassembly-eat-the-world-not-how-you-think/</link><pubDate>Mon, 03 Jul 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/webassembly-eat-the-world-not-how-you-think/</guid><description>&lt;p&gt;For years, the WebAssembly story has been sold as a clever way to run C++ in the browser. That’s not wrong—it’s just incomplete. The real revolution is happening in the places you can’t easily “just ship to the browser”: servers, edges, embedded systems, database extensions, and the permissioned chaos of smart contract runtimes. WebAssembly isn’t becoming the web’s new app platform. It’s becoming the plumbing layer for &lt;em&gt;every&lt;/em&gt; platform that needs safe, fast, language-agnostic execution.&lt;/p&gt;</description><content>&lt;p&gt;For years, the WebAssembly story has been sold as a clever way to run C++ in the browser. That’s not wrong—it’s just incomplete. The real revolution is happening in the places you can’t easily “just ship to the browser”: servers, edges, embedded systems, database extensions, and the permissioned chaos of smart contract runtimes. WebAssembly isn’t becoming the web’s new app platform. It’s becoming the plumbing layer for &lt;em&gt;every&lt;/em&gt; platform that needs safe, fast, language-agnostic execution.&lt;/p&gt;
&lt;h2 id="stop-worshiping-the-browser-use-case"&gt;Stop worshiping the browser use case&lt;/h2&gt;
&lt;p&gt;The browser is the shiny demo. It’s also the constraint. Browser-based WASM is still bound by sandboxing, execution models, and the web’s expectations around networking, storage, and user interaction. Plenty of teams will continue using WASM for interactive workloads—games, image/video pipelines, and compute-heavy UI logic—and those use cases are real.&lt;/p&gt;
&lt;p&gt;But the “WASM in the browser” narrative misses the point: the industry already knows how to ship web apps. What it didn’t know how to standardize is &lt;em&gt;how to run untrusted or semi-trusted code safely and consistently across heterogeneous environments&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When you look at WASM as a compilation target rather than a browser feature, the real story gets obvious. WASM gives you a portable, verifiable binary format plus an execution contract. That combination is the foundation for runtime ecosystems far beyond the DOM.&lt;/p&gt;
&lt;h2 id="the-universe-of-wasmtime-style-runtimes"&gt;The universe of Wasmtime-style runtimes&lt;/h2&gt;
&lt;p&gt;If you’ve only encountered WASM through browser tooling, you might not realize how quickly it turns into “just another runtime” on the server.&lt;/p&gt;
&lt;p&gt;Server-side WASM runtimes—think Wasmtime-style engines—let you run WASM modules in environments where you otherwise might deploy containers, custom plugins, or heavyweight microservices. The key difference isn’t only performance. It’s the ability to treat code as a deployable artifact with a stable contract: inputs/outputs, resource limits, and predictable execution semantics.&lt;/p&gt;
&lt;p&gt;A practical pattern is “small compute modules” that your service loads on demand. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A request pipeline that calls a WASM module to compute pricing rules.&lt;/li&gt;
&lt;li&gt;A fraud-scoring module that runs with strict CPU/memory caps.&lt;/li&gt;
&lt;li&gt;A compliance checker that rejects payloads before they reach downstream systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this model, the runtime handles isolation and resource boundaries. Your application becomes an orchestrator rather than a monolith of business logic. If you’ve ever built a plugin system in production, you know how fragile those get—ABI mismatches, dependency hell, and “hot reload” drama. WASM narrows the surface area of failure because it doesn’t care what language produced the module.&lt;/p&gt;
&lt;p&gt;And importantly, you can version and test modules more deterministically than with ad-hoc binaries.&lt;/p&gt;
&lt;h2 id="edge-computing-wasm-as-the-portable-compute-cartridge"&gt;Edge computing: WASM as the portable “compute cartridge”&lt;/h2&gt;
&lt;p&gt;Edge platforms have a different problem than browsers and traditional servers: they need to run code near users without giving every customer a free pass on resources, security posture, or performance predictability.&lt;/p&gt;
&lt;p&gt;This is where WASM shines. When an edge provider offers a WASM-friendly execution model, you can ship compute as a portable artifact. The runtime can impose quotas, limit outbound network access, and keep the execution sandboxed. From your perspective, you’re not building per-edge-language custom stacks. You compile once, target the WASM module interface, and let the edge runtime do the hosting.&lt;/p&gt;
&lt;p&gt;A concrete example: a content routing service at the edge.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You compile a WASM module that parses headers and rewrites routing decisions.&lt;/li&gt;
&lt;li&gt;The edge platform calls the module for each request.&lt;/li&gt;
&lt;li&gt;The module returns an action: rewrite, cache key, or bypass cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if the module logic changes frequently, you still get a clean deployment unit that doesn’t require you to rebuild an entire service. This is the “compute cartridge” mindset: small, isolated, swappable logic that moves with the traffic.&lt;/p&gt;
&lt;h2 id="databases-and-plugins-making-extensions-actually-extensible"&gt;Databases and plugins: making “extensions” actually extensible&lt;/h2&gt;
&lt;p&gt;The internet is full of extension systems that weren’t designed for safety, portability, or long-term maintainability. Databases are the perfect example: users want custom functions, transforms, and policy checks—yet every extension model tends to become a tangle of ABI constraints, version lock-in, and security trade-offs.&lt;/p&gt;
&lt;p&gt;WASM is an unusually good fit for database extensions because it can serve as a common execution substrate between the host database and many extension languages.&lt;/p&gt;
&lt;p&gt;Instead of requiring extensions to be compiled as native code against a specific database version, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expose a narrow host interface (how to read rows, how to return results, how to handle errors).&lt;/li&gt;
&lt;li&gt;Run extensions as WASM modules under the database’s runtime policy.&lt;/li&gt;
&lt;li&gt;Enforce resource limits and sandbox boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The benefit is cultural as much as technical: you can build an extension ecosystem where people don’t need deep knowledge of your database’s internal build system. They just need to target the WASM module contract. A Rust author and a Go author can both ship compatible extensions without you maintaining two different plugin SDKs forever.&lt;/p&gt;
&lt;p&gt;If you’ve ever watched an extension community collapse because “we only support language X,” you already understand why this matters.&lt;/p&gt;
&lt;h2 id="smart-contracts-and-the-universal-runtime-effect"&gt;Smart contracts and the “universal runtime” effect&lt;/h2&gt;
&lt;p&gt;Smart contracts already have their own execution environments and languages. But WASM’s influence here is about standardizing the &lt;em&gt;execution layer&lt;/em&gt;, not just providing another syntax.&lt;/p&gt;
&lt;p&gt;When contract platforms use WASM-based execution, you get a pipeline where developers can compile from multiple languages into the same portable binary format. That reduces the friction of tooling, improves portability, and makes the runtime more uniform across contracts.&lt;/p&gt;
&lt;p&gt;Even in ecosystems where developers still “write in Solidity” or a domain-specific language, the underlying execution model often becomes the real battlefield: determinism, metering, and state interaction. A WASM-based runtime can unify these concerns in a way that language choices can largely ignore.&lt;/p&gt;
&lt;p&gt;The outcome is language interoperability that feels inevitable in hindsight. Today, smart contract developers deal with fragmented toolchains, unique runtime semantics per platform, and incompatible ABI rules. A shared execution target doesn’t magically solve every semantic mismatch—but it does remove a large category of “rewrite the whole stack” problems.&lt;/p&gt;
&lt;h2 id="wasms-real-superpower-the-component-model-and-interoperability"&gt;WASM’s real superpower: the component model and interoperability&lt;/h2&gt;
&lt;p&gt;If you want one reason to believe the “eat the world” headline without hand-waving, it’s the WASM component model. The browser can be the first landing zone, but the component model is about defining &lt;em&gt;interfaces&lt;/em&gt; between modules and hosts in a portable, language-agnostic way.&lt;/p&gt;
&lt;p&gt;In practice, this is the difference between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Here’s a blob of code you can run,” and&lt;/li&gt;
&lt;li&gt;“Here’s a code package with a well-defined interface contract you can reliably wire into other systems.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think about what that enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A WASM module written in one language can call a capability exposed by another module written in a different language.&lt;/li&gt;
&lt;li&gt;Hosts can provide standardized capability imports (files, networking-like abstractions, key-value storage access, cryptographic primitives, application-specific services).&lt;/li&gt;
&lt;li&gt;Tooling can reason about interfaces, generate bindings, and reduce the brittle glue code that makes interoperability a fantasy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the kind of progress that makes platforms feel boring—in the best way. When the plumbing is stable, innovation accelerates because teams stop re-solving the same integration problems.&lt;/p&gt;
&lt;p&gt;And yes, this will still show up in the browser eventually. But the component model is more valuable anywhere you have multiple teams, multiple languages, and frequent composition of behavior: edge + backend pipelines, database extensions + policy modules, orchestration layers + sandboxed compute.&lt;/p&gt;
&lt;h2 id="so-what-should-you-do-with-this"&gt;So what should you do with this?&lt;/h2&gt;
&lt;p&gt;If you’re building products, the best approach is pragmatic, not ideological.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Treat WASM as a deployment artifact, not a marketing slogan.&lt;/strong&gt; Start with “small logic modules” that are expensive to iterate on, risky to run natively, or hard to version safely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design a narrow interface.&lt;/strong&gt; The moment your WASM boundary becomes “everything,” you’ll recreate the same complexity you tried to escape. Keep it tight: inputs, outputs, and a few well-defined capabilities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make isolation a first-class feature.&lt;/strong&gt; Use resource limits and sandbox policies intentionally. Decide what’s allowed before you ship—not after someone needs “just one more permission.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for an ecosystem, not a one-off.&lt;/strong&gt; If you can standardize your module interface, you can eventually support third-party extensions without becoming the bottleneck.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re a developer evaluating tooling: learn the “compile-to-WASM then run in a runtime” workflow and get comfortable with the host interface model. Don’t start with building a web app in WASM. Start by building a module that does one job well and can run under constraints.&lt;/p&gt;
&lt;h2 id="conclusion-the-web-is-just-the-first-room"&gt;Conclusion: The web is just the first room&lt;/h2&gt;
&lt;p&gt;WebAssembly won’t “replace” the web. It will replace the need to invent new execution environments every time you want safe, portable code. The browser is a compelling proving ground, but WASM’s real power is broader: server-side runtimes for modular compute, edge-friendly cartridges for low-latency logic, plugin systems for extensible databases, and language interoperability made practical through interface-focused component design.&lt;/p&gt;
&lt;p&gt;WASM isn’t eating the world because it’s cool in the browser. It’s eating the world because it’s a universal execution target—and universality is the rarest form of platform power.&lt;/p&gt;</content></item><item><title>Playwright Killed Cypress and I'm Not Sorry</title><link>https://decastro.work/blog/playwright-killed-cypress-not-sorry/</link><pubDate>Wed, 21 Jun 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/playwright-killed-cypress-not-sorry/</guid><description>&lt;p&gt;For years, Cypress felt like the rare end-to-end (E2E) testing tool that didn’t make developers want to set fire to their laptops. It was fast, friendly, and made “write integration tests” sound almost enjoyable. But good tools get replaced when the fundamentals stop matching how real apps work—multi-browser reality, auth flows, multiple tabs, and flaky timing. Playwright doesn’t just improve the experience. It fixes the structural problems that made Cypress feel like a good first act instead of the final one.&lt;/p&gt;</description><content>&lt;p&gt;For years, Cypress felt like the rare end-to-end (E2E) testing tool that didn’t make developers want to set fire to their laptops. It was fast, friendly, and made “write integration tests” sound almost enjoyable. But good tools get replaced when the fundamentals stop matching how real apps work—multi-browser reality, auth flows, multiple tabs, and flaky timing. Playwright doesn’t just improve the experience. It fixes the structural problems that made Cypress feel like a good first act instead of the final one.&lt;/p&gt;
&lt;h2 id="why-cypress-was-a-revelation-and-why-that-matters"&gt;Why Cypress Was a Revelation (and Why That Matters)&lt;/h2&gt;
&lt;p&gt;Cypress arrived at the exact moment E2E testing was in decline. Selenium-based approaches were brittle, slow, and miserable to debug. Cypress changed the emotional tone: tests ran in the browser, the UI showed what happened, and writing “realistic” user flows wasn’t a chore.&lt;/p&gt;
&lt;p&gt;It also normalized a development workflow many teams wanted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write tests alongside the app.&lt;/li&gt;
&lt;li&gt;See failures immediately.&lt;/li&gt;
&lt;li&gt;Debug with a time-travel style runner.&lt;/li&gt;
&lt;li&gt;Get deterministic-ish results through built-in waiting and control.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you used Cypress for a year and felt like your testing strategy finally stopped fighting you, you’re not imagining it. Cypress had momentum for a reason.&lt;/p&gt;
&lt;p&gt;But here’s the trade-off: Cypress made certain assumptions that are great for some apps and painful for others. Over time, those assumptions became the reasons teams either grew around Cypress—or grew out of it.&lt;/p&gt;
&lt;p&gt;And once you’ve been burned by enough “works on my machine / only in Chrome / only in one tab / only same-origin” edge cases, the honeymoon ends.&lt;/p&gt;
&lt;h2 id="the-dealbreakers-cypress-couldnt-outgrow"&gt;The Dealbreakers Cypress Couldn’t Outgrow&lt;/h2&gt;
&lt;p&gt;Let’s be blunt about the flaws that started as limitations and turned into dealbreakers.&lt;/p&gt;
&lt;h3 id="1-single-browser-at-a-time-execution"&gt;1) Single-browser-at-a-time execution&lt;/h3&gt;
&lt;p&gt;Modern frontends aren’t just “Chrome apps.” They’re shipped to users running Chromium, Firefox, and Safari/WebKit, often all at once in the same CI pipeline.&lt;/p&gt;
&lt;p&gt;Cypress’s execution model made “run the exact same E2E suite everywhere” more cumbersome than it should be. You can compensate with parallelization and configuration gymnastics, but you still end up with a testing strategy that feels heavier than it needs to.&lt;/p&gt;
&lt;h3 id="2-authentication-and-state-sharing-got-awkward"&gt;2) Authentication and state sharing got awkward&lt;/h3&gt;
&lt;p&gt;E2E testing always hits auth. Whether you’re using cookies, tokens, SSO redirects, or multi-step login flows, you don’t want to repeat the same expensive login for every test case.&lt;/p&gt;
&lt;p&gt;Cypress can do auth workflows, but the model often pushes teams toward workarounds—custom helpers, manual state management, or patterns that feel brittle when the app changes.&lt;/p&gt;
&lt;h3 id="3-multi-tab-and-window-behavior-arent-optional"&gt;3) Multi-tab and window behavior aren’t optional&lt;/h3&gt;
&lt;p&gt;Real apps open new tabs, spawn popups, and rely on window context. OAuth and some third-party integrations effectively demand it.&lt;/p&gt;
&lt;p&gt;If your E2E suite can’t reliably handle multi-tab workflows, you either stop testing the real user path or you accept chronic flakiness. Both are bad. One hides bugs. The other wastes developer time.&lt;/p&gt;
&lt;h3 id="4-same-origin-constraints-can-force-unnatural-testing-patterns"&gt;4) Same-origin constraints can force unnatural testing patterns&lt;/h3&gt;
&lt;p&gt;Many apps are cleanly same-origin. Many aren’t. Even when your app is “mostly” same-origin, modern architecture introduces cross-origin behavior—CDNs, identity providers, payment pages, embedded widgets, and more.&lt;/p&gt;
&lt;p&gt;When your testing tool struggles with cross-origin realism, your E2E suite becomes less of a mirror and more of a simplified cartoon of the product.&lt;/p&gt;
&lt;p&gt;Those are not minor inconveniences. They’re the difference between “tests that build confidence” and “tests that create doubt.”&lt;/p&gt;
&lt;h2 id="why-playwright-wins-speed-reliability-and-true-multi-browser"&gt;Why Playwright Wins: Speed, Reliability, and True Multi-Browser&lt;/h2&gt;
&lt;p&gt;Playwright addresses these problems in a way that feels like a design decision, not a collection of plugins.&lt;/p&gt;
&lt;h3 id="multi-browser-by-default"&gt;Multi-browser by default&lt;/h3&gt;
&lt;p&gt;Playwright runs against Chromium, Firefox, and WebKit. That means the same E2E suite is exercised across rendering engines without you rewriting everything or maintaining separate strategies.&lt;/p&gt;
&lt;p&gt;Practically, this changes how teams plan releases. Instead of “we’ll see Safari issues later,” you get feedback early because Safari/WebKit is part of the routine.&lt;/p&gt;
&lt;h3 id="better-reliability-through-auto-waiting"&gt;Better reliability through auto-waiting&lt;/h3&gt;
&lt;p&gt;Flakiness is rarely about your app suddenly turning evil. It’s about mismatched assumptions: the test thinks an element is ready before the browser does, or the DOM is in flux and the click happens “too early.”&lt;/p&gt;
&lt;p&gt;Playwright’s auto-waiting model is built to reduce these timing mismatches. You don’t have to litter tests with arbitrary &lt;code&gt;wait(1000)&lt;/code&gt; calls just to keep them green. The result is an E2E suite that behaves more like a conversation with the UI rather than a rigid script.&lt;/p&gt;
&lt;h3 id="authentication-state-sharing-that-doesnt-feel-like-a-hack"&gt;Authentication state sharing that doesn’t feel like a hack&lt;/h3&gt;
&lt;p&gt;Instead of forcing every test to walk through login like a ritual, Playwright lets you reuse authenticated state across the suite. In practice, teams often move toward a pattern like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticate once (e.g., via a storage state file).&lt;/li&gt;
&lt;li&gt;Run many tests as the authenticated user.&lt;/li&gt;
&lt;li&gt;Keep the suite focused on the behaviors that matter.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is one of those “infrastructure” improvements that compounds. Faster tests lead to more frequent runs, which lead to earlier bug discovery, which leads to fewer “big bang” releases where nobody trusts the pipeline.&lt;/p&gt;
&lt;h3 id="multi-tab-and-window-flows-that-match-reality"&gt;Multi-tab and window flows that match reality&lt;/h3&gt;
&lt;p&gt;OAuth, payment providers, “open in new tab,” embedded flows—these are not exotic edge cases. They’re core to modern apps.&lt;/p&gt;
&lt;p&gt;Playwright can handle multi-page scenarios cleanly, so your E2E tests can model the real browser experience instead of forcing the product into a single-tab testing fantasy.&lt;/p&gt;
&lt;h2 id="the-most-magic-part-recording-tests-that-actually-help"&gt;The Most Magic Part: Recording Tests that Actually Help&lt;/h2&gt;
&lt;p&gt;Cypress had an ace here: the runner and debugging experience made you feel like you were driving the browser. Playwright takes it further with a test generator that records your actions and produces usable code.&lt;/p&gt;
&lt;p&gt;And “usable” is the key word. Many record-and-replay tools spit out output that looks clever but quickly turns into archaeology. Playwright’s generator produces code you can refine rather than code you must rewrite from scratch.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in a practical workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You start by recording a key user journey—say, “create an account, land on the dashboard, add an item.”&lt;/li&gt;
&lt;li&gt;The generator gives you a scaffold for selectors, navigation, and assertions.&lt;/li&gt;
&lt;li&gt;You review the output and replace brittle selectors with stable ones (data attributes).&lt;/li&gt;
&lt;li&gt;You add assertions that reflect business outcomes, not just UI states.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means you can bootstrap coverage quickly without sacrificing engineering discipline. You get the speed of automation with the maintainability you’d expect from handwritten tests.&lt;/p&gt;
&lt;h2 id="a-practical-migration-playbook-without-the-pain"&gt;A Practical Migration Playbook (Without the Pain)&lt;/h2&gt;
&lt;p&gt;“Cypress to Playwright” can sound like a rewrite project. It doesn’t have to be. You can migrate in slices and keep shipping.&lt;/p&gt;
&lt;h3 id="1-start-with-the-highest-signal-flows"&gt;1) Start with the highest-signal flows&lt;/h3&gt;
&lt;p&gt;Don’t port everything at once. Pick the E2E tests that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;represent core user journeys,&lt;/li&gt;
&lt;li&gt;cover the most bug-prone areas,&lt;/li&gt;
&lt;li&gt;run frequently in CI,&lt;/li&gt;
&lt;li&gt;and are the most painful today (flaky timing, auth overhead, cross-origin weirdness).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your Cypress suite is already healthy, you might migrate less urgently. But if it’s slowing you down or forcing workarounds, prioritize what hurts.&lt;/p&gt;
&lt;h3 id="2-standardize-selectors-early"&gt;2) Standardize selectors early&lt;/h3&gt;
&lt;p&gt;A migration goes smoother when you pick a selector strategy and stick to it. The simplest, most reliable approach is usually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data-testid&lt;/code&gt; for test hooks,&lt;/li&gt;
&lt;li&gt;and minimal reliance on fragile DOM structures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reduces churn when UI changes.&lt;/p&gt;
&lt;h3 id="3-centralize-auth-state"&gt;3) Centralize auth state&lt;/h3&gt;
&lt;p&gt;If you’re wasting time logging in during every test, fix that first. In Playwright, treat authentication state as an artifact of your suite—generated once, reused everywhere.&lt;/p&gt;
&lt;p&gt;Even if you’re migrating gradually, centralizing auth will make the new tests instantly faster and more stable than the ones that repeat login workflows.&lt;/p&gt;
&lt;h3 id="4-use-playwrights-expectations-like-product-contracts"&gt;4) Use Playwright’s expectations like product contracts&lt;/h3&gt;
&lt;p&gt;E2E tests should assert business-relevant outcomes. Instead of “click happened,” assert:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the user sees the created entity,&lt;/li&gt;
&lt;li&gt;the cart totals are correct,&lt;/li&gt;
&lt;li&gt;the success message appears with the right content,&lt;/li&gt;
&lt;li&gt;and navigation lands on the right page.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is how you avoid “tests that pass but don’t matter.”&lt;/p&gt;
&lt;h3 id="5-run-both-suites-during-transition"&gt;5) Run both suites during transition&lt;/h3&gt;
&lt;p&gt;For a limited time, keep Cypress and Playwright running for coverage overlap. That gives you confidence and helps you identify gaps without halting development.&lt;/p&gt;
&lt;p&gt;You’ll also learn quickly what kinds of flows your team needs to model differently in Playwright (especially multi-tab behavior).&lt;/p&gt;
&lt;h2 id="conclusion-cypress-had-a-great-runbut-playwright-is-the-real-platform"&gt;Conclusion: Cypress Had a Great Run—But Playwright Is the Real Platform&lt;/h2&gt;
&lt;p&gt;Cypress earned its reputation for making E2E testing feel approachable. But the industry moved: multi-browser needs are non-negotiable, auth is too complex for repetitive flows, and modern apps increasingly rely on multi-page realities.&lt;/p&gt;
&lt;p&gt;Playwright isn’t a “better Cypress.” It’s a more complete E2E platform designed around how browsers actually behave—fewer timing problems, first-class multi-browser support, cleaner auth state sharing, and recording that accelerates implementation without sabotaging maintainability.&lt;/p&gt;
&lt;p&gt;If your E2E strategy is still making you fight the tool, stop fighting it. Use the one that doesn’t require you to compromise with the truth of the browser.&lt;/p&gt;</content></item><item><title>Vite Won the Bundler Wars and Webpack Didn't Even See It Coming</title><link>https://decastro.work/blog/vite-won-bundler-wars-webpack-lost/</link><pubDate>Fri, 09 Jun 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/vite-won-bundler-wars-webpack-lost/</guid><description>&lt;p&gt;For years, JavaScript tooling followed a familiar pattern: every release came with “just one more configuration knob,” and every upgrade came with a ritual sacrifice to the build system. Webpack helped define what modern bundling could be—but it also normalized the idea that developer experience was something you had to earn through pain. Then Vite arrived with a different premise: ship less configuration, leverage the language you already use, and make the feedback loop feel instant. The result wasn’t just a new tool. It was a shift in what developers expect from the stack.&lt;/p&gt;</description><content>&lt;p&gt;For years, JavaScript tooling followed a familiar pattern: every release came with “just one more configuration knob,” and every upgrade came with a ritual sacrifice to the build system. Webpack helped define what modern bundling could be—but it also normalized the idea that developer experience was something you had to earn through pain. Then Vite arrived with a different premise: ship less configuration, leverage the language you already use, and make the feedback loop feel instant. The result wasn’t just a new tool. It was a shift in what developers expect from the stack.&lt;/p&gt;
&lt;h2 id="webpack-created-the-erathen-it-created-the-problem"&gt;Webpack created the era—then it created the problem&lt;/h2&gt;
&lt;p&gt;Webpack’s rise wasn’t accidental. It offered a powerful mental model: everything is a module, dependency graphs get built, assets get processed, and the browser finally receives a coherent bundle. That model scaled, and it became the default language of JavaScript build tooling.&lt;/p&gt;
&lt;p&gt;But power came with tradeoffs. Over time, Webpack didn’t merely require configuration—it &lt;em&gt;incentivized complexity&lt;/em&gt;. Teams accumulated layered rules across &lt;code&gt;webpack.config.js&lt;/code&gt;, custom loaders, plugin stacks, environment-specific overrides, and a constellation of flags that only a few people really understood. The build might be technically correct while still feeling operationally hostile.&lt;/p&gt;
&lt;p&gt;You’ve seen the symptoms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Why is this change not reflected?” (caches, rebuilds, watch config)&lt;/li&gt;
&lt;li&gt;“Why did the build take 90 seconds again?” (broad recompilation)&lt;/li&gt;
&lt;li&gt;“Why does it work on my machine?” (environment divergence)&lt;/li&gt;
&lt;li&gt;“Can someone just tweak the loader chain?” (tribal knowledge)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Webpack made bundling effective. It also made &lt;em&gt;iteration expensive&lt;/em&gt;—and developer experience became a second-class citizen.&lt;/p&gt;
&lt;h2 id="the-esm-native-bet-that-changed-everything"&gt;The “ESM-native” bet that changed everything&lt;/h2&gt;
&lt;p&gt;Vite’s breakthrough was philosophical as much as technical. Instead of treating your codebase as a bundle-first artifact, it treated the browser as a runtime for modules that can be loaded directly via ES modules.&lt;/p&gt;
&lt;p&gt;Concretely, Vite starts a dev server that serves your source using native ESM semantics. That means you’re no longer waiting for a full production-style build just to see changes. When you edit a file, Vite can serve the updated module quickly and let the browser reload precisely what’s needed.&lt;/p&gt;
&lt;p&gt;The impact on day-to-day work is immediate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fast startup&lt;/strong&gt;: the dev server boots without performing a heavy “bundle everything” pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Near-instant HMR&lt;/strong&gt;: edits propagate quickly without the ritual of full rebuilds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower cognitive load&lt;/strong&gt;: the config story is simpler and more legible.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever compared a slow “webpack dev server” experience to a snappy one where HMR feels like typing is directly connected to what you see on screen, you already understand why this mattered. Developer experience isn’t a luxury—it’s velocity.&lt;/p&gt;
&lt;h2 id="configuration-became-readable-again"&gt;Configuration became readable again&lt;/h2&gt;
&lt;p&gt;The most underrated win isn’t raw speed. It’s that Vite makes configuration feel like code you can reason about rather than a mystery incantation.&lt;/p&gt;
&lt;p&gt;Webpack configurations often grow into sprawling files where order matters, plugin timing matters, and loader chains interact in non-obvious ways. Even when you understand it, you still have to &lt;em&gt;hold the whole system in your head&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Vite flips that default. For many projects, you can start with a minimal &lt;code&gt;vite.config.*&lt;/code&gt; and only add complexity where you actually need it. For example, a typical Vite config for a React app might look like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;defineConfig&lt;/span&gt; } &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;vite&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;react&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;@vitejs/plugin-react&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defineConfig&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;plugins&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;react&lt;/span&gt;()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice what’s missing: elaborate entry/output configuration, long loader arrays, and a dependency graph that you’re forced to model up front just to get started.&lt;/p&gt;
&lt;p&gt;And when you &lt;em&gt;do&lt;/em&gt; need customization, Vite’s structure tends to map cleanly to what you mean: dev server behavior, plugins for frameworks, build options, asset handling, and path aliases. In practice, this makes onboarding faster and reduces the odds that a “simple change” becomes a two-week debugging quest.&lt;/p&gt;
&lt;h2 id="migration-is-straightforwardif-you-plan-it-like-an-engineer"&gt;Migration is straightforward—if you plan it like an engineer&lt;/h2&gt;
&lt;p&gt;The reason Vite didn’t just win on paper is that migration doesn’t have to be a rewrite. The key is to treat it as a series of controlled transitions:&lt;/p&gt;
&lt;h3 id="1-start-by-swapping-the-tooling-not-the-app-architecture"&gt;1) Start by swapping the tooling, not the app architecture&lt;/h3&gt;
&lt;p&gt;Create a new Vite project scaffold for your target framework (React/Vue/Svelte/etc.) and use it as a baseline. Then move your existing source files and adapt configuration. Keep behavior stable while you change the build pipeline.&lt;/p&gt;
&lt;h3 id="2-map-your-current-bundler-expectations-to-vite-equivalents"&gt;2) Map your current bundler expectations to Vite equivalents&lt;/h3&gt;
&lt;p&gt;Common migration points include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entry points&lt;/strong&gt;: Webpack’s &lt;code&gt;entry&lt;/code&gt; becomes Vite’s &lt;code&gt;index.html&lt;/code&gt; plus standard module imports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aliases&lt;/strong&gt;: Webpack &lt;code&gt;resolve.alias&lt;/code&gt; maps naturally to Vite &lt;code&gt;resolve.alias&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static assets&lt;/strong&gt;: Move public assets to Vite’s &lt;code&gt;public/&lt;/code&gt; directory and adjust references accordingly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment variables&lt;/strong&gt;: Webpack often uses custom conventions; Vite expects &lt;code&gt;import.meta.env&lt;/code&gt; for client-side variables.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple alias example in Vite:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;defineConfig&lt;/span&gt; } &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;vite&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;defineConfig&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;resolve&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alias&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;@&amp;#39;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;/src&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-verify-hmr-behavior-early"&gt;3) Verify HMR behavior early&lt;/h3&gt;
&lt;p&gt;Many migrations succeed technically but feel wrong because dev workflows change. Make sure your team’s “edit → see changes” loop behaves as expected. This is where Vite usually shines, but you still need to validate framework integrations and any special tooling.&lt;/p&gt;
&lt;h3 id="4-fix-the-build-output-last"&gt;4) Fix the build output last&lt;/h3&gt;
&lt;p&gt;Only after dev is comfortable should you focus on production parity: output paths, minification expectations, and any CDN/static hosting rules. In most cases, Vite’s build is simpler to reason about, but you want to avoid surprises for deployment.&lt;/p&gt;
&lt;p&gt;If your team treats migration like a controlled engineering rollout—not a big bang rewrite—you’ll be surprised how quickly the new workflow feels normal.&lt;/p&gt;
&lt;h2 id="rollup-compatibility-made-vite-the-default-not-just-an-alternative"&gt;Rollup compatibility made Vite the default, not just an alternative&lt;/h2&gt;
&lt;p&gt;It’s not enough for a dev server to be fast; the production build matters. Vite’s use of Rollup under the hood is a key reason it’s been easy for teams to adopt.&lt;/p&gt;
&lt;p&gt;Rollup’s approach encourages predictable bundling and produces outputs that many tooling chains already understand. In practical terms, that means you can often migrate without losing the “I know how to debug build output” muscle memory your team built over time with bundlers.&lt;/p&gt;
&lt;p&gt;This is also why the ecosystem rallied quickly. Plugins for framework integrations, TypeScript support, CSS preprocessors, and common patterns fell into place fast. When a tool has a strong default path and a robust plugin system, teams stop treating configuration as a personal project and start treating it like infrastructure.&lt;/p&gt;
&lt;p&gt;And yes—Turbopack from Vercel is chasing the “faster than fast” dream. But speed alone doesn’t win long-term. The developer experience equation includes clarity, compatibility, and the ability to survive upgrades without turning your build config into archaeology. Vite’s balance—simplicity plus production compatibility—has made it the default choice for many teams.&lt;/p&gt;
&lt;h2 id="what-webpack-isnt-dead-actually-means-and-why-its-still-worth-switching"&gt;What “Webpack isn’t dead” actually means (and why it’s still worth switching)&lt;/h2&gt;
&lt;p&gt;Let’s be precise: Webpack isn’t dead. It still exists, and some large, customized configurations will keep running for years. If you’ve invested heavily in a specialized Webpack setup—custom loaders, legacy build quirks, tightly coupled internal tooling—it may be unrealistic to migrate immediately.&lt;/p&gt;
&lt;p&gt;But “not dead” is not the same as “the future.” Webpack is in hospice in the sense that the center of gravity has moved. New projects overwhelmingly favor workflows that reduce iteration time, decrease configuration burden, and make the build system more legible.&lt;/p&gt;
&lt;p&gt;The best argument for switching isn’t that Vite is trendy. It’s that Vite aligns the tooling with how modern JavaScript works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;native module semantics&lt;/li&gt;
&lt;li&gt;fast feedback loops&lt;/li&gt;
&lt;li&gt;configuration that scales with needs instead of forcing complexity up front&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your current Webpack setup feels like it taxes every change—even when it works—Vite offers a way out without demanding you rebuild your application from scratch.&lt;/p&gt;
&lt;h2 id="conclusion-the-bundler-wars-ended-where-they-should-haveat-developer-experience"&gt;Conclusion: the bundler wars ended where they should have—at developer experience&lt;/h2&gt;
&lt;p&gt;Webpack defined an era, but it also taught teams to accept friction as the price of building JavaScript. Vite won because it challenged the bargain: make the dev loop fast by default, make configuration understandable, and let the language ecosystem do more of the heavy lifting.&lt;/p&gt;
&lt;p&gt;Turbopack may chase further performance gains, but Vite’s real victory is broader. It changed expectations. And once a tool makes iteration feel effortless, “impenetrable configuration” stops being acceptable—even if it used to be industry standard.&lt;/p&gt;</content></item><item><title>The Surprising Value of Learning a Language You'll Never Use Professionally</title><link>https://decastro.work/blog/learning-language-never-use-professionally/</link><pubDate>Fri, 02 Jun 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/learning-language-never-use-professionally/</guid><description>&lt;p&gt;You don’t learn a strange programming language just to add another syntax high to your résumé. You learn it because it rewires your mental habits—so even if you never ship production code in that language, you start building better software elsewhere. After learning Haskell, Clojure, and Prolog, I stopped thinking of “different languages” as different tools and started thinking of them as different ways to reason.&lt;/p&gt;
&lt;p&gt;And yes, I know the obvious objection: &lt;em&gt;“I don’t need monads. I don’t write Clojure. I don’t deploy Prolog.”&lt;/em&gt; That’s exactly the point. The value isn’t the feature set. It’s the mental model.&lt;/p&gt;</description><content>&lt;p&gt;You don’t learn a strange programming language just to add another syntax high to your résumé. You learn it because it rewires your mental habits—so even if you never ship production code in that language, you start building better software elsewhere. After learning Haskell, Clojure, and Prolog, I stopped thinking of “different languages” as different tools and started thinking of them as different ways to reason.&lt;/p&gt;
&lt;p&gt;And yes, I know the obvious objection: &lt;em&gt;“I don’t need monads. I don’t write Clojure. I don’t deploy Prolog.”&lt;/em&gt; That’s exactly the point. The value isn’t the feature set. It’s the mental model.&lt;/p&gt;
&lt;h2 id="why-useless-languages-arent-useless"&gt;Why “useless” languages aren’t useless&lt;/h2&gt;
&lt;p&gt;Most engineers treat language learning like skill stacking: if you don’t use the language directly, the time investment feels wasteful. But the real payoff is cognitive, not credential-based.&lt;/p&gt;
&lt;p&gt;Programming is not only about writing code—it’s about designing decisions under constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What can vary?&lt;/li&gt;
&lt;li&gt;What must never be wrong?&lt;/li&gt;
&lt;li&gt;How do we represent uncertainty?&lt;/li&gt;
&lt;li&gt;How do we decompose a problem?&lt;/li&gt;
&lt;li&gt;How do we avoid bugs that only appear when state gets complicated?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Different languages force you to answer those questions differently. Even when you return to your “main” stack—TypeScript, Python, React—you carry the patterns that prevented mistakes in the other ecosystem.&lt;/p&gt;
&lt;p&gt;Think of it like cross-training. Learning a different sport doesn’t make you a professional in that sport. It improves your coordination—and changes what “good form” feels like when you go back to your own.&lt;/p&gt;
&lt;h2 id="haskell-monads-dont-mattertypes-first-does"&gt;Haskell: monads don’t matter—types-first does&lt;/h2&gt;
&lt;p&gt;I learned Haskell expecting to admire functional purity. What surprised me was how quickly it trained me to design interfaces before implementing them. Not “monads in production,” but types-first thinking.&lt;/p&gt;
&lt;p&gt;In Haskell, you feel the pressure of types the way you feel gravity: consistently. If you want to represent failure, you don’t shrug and return &lt;code&gt;null&lt;/code&gt;; you use a type that forces callers to handle it. If you want to represent optionality, you don’t pretend it’s the default case; you make “maybe present” explicit.&lt;/p&gt;
&lt;p&gt;Back in TypeScript, the difference shows up immediately in API design.&lt;/p&gt;
&lt;h3 id="example-turning-hope-into-a-type-contract"&gt;Example: turning “hope” into a type contract&lt;/h3&gt;
&lt;p&gt;Suppose you’re writing a function that might fail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bad habit:&lt;/strong&gt; return &lt;code&gt;T | null&lt;/code&gt; and hope callers remember to check.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Haskell-shaped habit:&lt;/strong&gt; return a discriminated union that makes handling explicit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In TypeScript, you can mirror Haskell’s “constructor tells you what happened” approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{ ok: true, value: ... }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{ ok: false, error: ... }&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the caller &lt;em&gt;can’t&lt;/em&gt; ignore failure without the compiler nagging them. More importantly, the interface tells the truth: failure is part of the contract, not an exception hiding in the shadows.&lt;/p&gt;
&lt;h3 id="another-shift-designing-for-composition-not-side-effects"&gt;Another shift: designing for composition, not side effects&lt;/h3&gt;
&lt;p&gt;Haskell encourages you to build pipelines where each step transforms data and each transformation is typed. You start valuing small, pure functions—not because you love purity, but because you can reason about them.&lt;/p&gt;
&lt;p&gt;So when I went back to TypeScript, I stopped sprinkling logic across UI components and started extracting functions that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;take well-typed inputs,&lt;/li&gt;
&lt;li&gt;return well-typed outputs,&lt;/li&gt;
&lt;li&gt;avoid hidden side effects.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You don’t need monads to get the benefits—you need the mindset that “an interface is a promise,” and types are how you keep that promise.&lt;/p&gt;
&lt;h2 id="clojure-immutable-data-is-a-state-superpower"&gt;Clojure: immutable data is a state superpower&lt;/h2&gt;
&lt;p&gt;Clojure’s biggest lesson wasn’t syntax or functional flair. It was an obsession with immutable data—and the way that obsession changes how you structure state in real applications, especially React.&lt;/p&gt;
&lt;p&gt;In mutable designs, state bugs often show up as “impossible” behavior: the UI is wrong, effects run in odd orders, a value “shouldn’t have changed,” and you spend a day hunting down who mutated it.&lt;/p&gt;
&lt;p&gt;Clojure doesn’t let you hand-wave that away. Immutability makes every state transition explicit. You stop treating state as a thing that gets edited and start treating it as a value that gets replaced.&lt;/p&gt;
&lt;h3 id="example-make-updates-explicit-with-pure-reducers"&gt;Example: make updates explicit with pure reducers&lt;/h3&gt;
&lt;p&gt;In React, state management often devolves into “setState whenever something happens.” With Clojure’s influence, I lean toward reducer-like patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define a state shape.&lt;/li&gt;
&lt;li&gt;Define a small set of actions that represent transitions.&lt;/li&gt;
&lt;li&gt;Implement transitions as pure functions: &lt;code&gt;nextState = reducer(state, action)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you don’t use Redux, you can adopt the mental model with React hooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compute next state from current state and an event.&lt;/li&gt;
&lt;li&gt;Keep transitions deterministic.&lt;/li&gt;
&lt;li&gt;Avoid “update in place” patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t mean your app will magically become bug-free. It means that when something goes wrong, you can inspect the transition logic and understand how the state evolved, rather than chasing accidental mutations.&lt;/p&gt;
&lt;h3 id="practical-advice-treat-state-as-values-not-containers"&gt;Practical advice: treat state as values, not containers&lt;/h3&gt;
&lt;p&gt;A simple rule of thumb I adopted from Clojure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If a value represents your current app state, never “edit” it—derive the next value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In TypeScript and Python, that means you’re less likely to mutate arrays in a reducer, less likely to patch nested objects in place, and more likely to return fresh objects.&lt;/p&gt;
&lt;p&gt;It’s not about aesthetics. It’s about making “what happened” inspectable.&lt;/p&gt;
&lt;h2 id="prolog-logic-thinking-for-constraint-problems"&gt;Prolog: logic thinking for constraint problems&lt;/h2&gt;
&lt;p&gt;Then there’s Prolog—logic programming that feels alien until you notice what it’s really doing: it turns certain classes of problems from “how do I compute this?” into “what conditions must be satisfied?”&lt;/p&gt;
&lt;p&gt;That mental shift is incredibly useful in TypeScript and Python, even if you never embed Prolog in production.&lt;/p&gt;
&lt;h3 id="example-search-and-constraints-are-the-same-beast"&gt;Example: search and constraints are the same beast&lt;/h3&gt;
&lt;p&gt;A lot of real engineering boils down to constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find assignments that satisfy requirements.&lt;/li&gt;
&lt;li&gt;Validate a schedule.&lt;/li&gt;
&lt;li&gt;Determine which combinations are allowed.&lt;/li&gt;
&lt;li&gt;Solve dependency rules.&lt;/li&gt;
&lt;li&gt;Derive eligibility conditions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In imperative code, you often write loops with clever pruning. In logic code, you write rules that declare relationships, and let the search engine do the exploring.&lt;/p&gt;
&lt;p&gt;You don’t need Prolog to write constraint-focused solutions. But learning it trains you to ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are the predicates?&lt;/li&gt;
&lt;li&gt;What must be true for a solution?&lt;/li&gt;
&lt;li&gt;How do we express relationships?&lt;/li&gt;
&lt;li&gt;Where does backtracking happen (even if simulated)?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-framing-represent-rules-not-steps"&gt;Practical framing: represent rules, not steps&lt;/h3&gt;
&lt;p&gt;Back in TypeScript or Python, I now approach some problems by defining rule functions or declarative constraints first. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“An item is eligible if it satisfies A and B and not C.”&lt;/li&gt;
&lt;li&gt;“A route is valid if each segment allows the transition and the total cost is under budget.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you end up implementing it imperatively, the structure you choose is more aligned with the problem. Your code becomes easier to test: you can test each predicate independently, then test the combination.&lt;/p&gt;
&lt;p&gt;Prolog also makes you respect ambiguity and multiplicity. Instead of “the answer,” you start expecting “zero or more solutions.” That mindset reduces the temptation to oversimplify—another quiet source of production bugs.&lt;/p&gt;
&lt;h2 id="diversify-your-mental-models-and-your-interface-instincts"&gt;Diversify your mental models (and your interface instincts)&lt;/h2&gt;
&lt;p&gt;The real theme across these languages is not that they share features. They don’t. It’s that each one attacks a different assumption:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Haskell&lt;/strong&gt; pressures you to make states and failures explicit via types.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clojure&lt;/strong&gt; pressures you to treat data as immutable values and transitions as deliberate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prolog&lt;/strong&gt; pressures you to state constraints and relationships rather than only computation steps.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you go back to your everyday stack, you don’t just write different code—you think differently about what a “good interface” is, what “correct state” means, and how to structure complex reasoning.&lt;/p&gt;
&lt;h3 id="a-practical-way-to-apply-this-immediately"&gt;A practical way to apply this immediately&lt;/h3&gt;
&lt;p&gt;Pick one current project (or one recurring kind of bug). Then apply a single “language-derived” rule:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Haskell-style:&lt;/strong&gt; Replace &lt;code&gt;null&lt;/code&gt;/&lt;code&gt;undefined&lt;/code&gt; return values with explicit result types. Make failure a first-class outcome.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clojure-style:&lt;/strong&gt; Refactor one state update path into a pure transition function. Avoid in-place mutation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prolog-style:&lt;/strong&gt; For one gnarly decision feature, identify predicates and constraints. Implement them as composable checks before wiring them together.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You’ll get benefits without changing your deployment strategy, your tooling, or your hiring posture. You’ll just improve your reasoning.&lt;/p&gt;
&lt;p&gt;And that’s what makes the time investment feel less like trivia and more like engineering.&lt;/p&gt;
&lt;h2 id="conclusion-learning-unused-languages-is-training-your-engineering-judgment"&gt;Conclusion: learning “unused” languages is training your engineering judgment&lt;/h2&gt;
&lt;p&gt;If you’re waiting for the day you’ll need Haskell, Clojure, or Prolog professionally, you’ll miss the point. The surprising value is that these languages teach you new ways to model correctness: types that force handling, immutable state that makes transitions legible, and logic constraints that clarify what must be true.&lt;/p&gt;
&lt;p&gt;Learn languages you won’t ship. Let them break your habits. Then let your production code benefit from the better mental models you bring back—especially in interface design, state management, and constraint-heavy logic.&lt;/p&gt;</content></item><item><title>Strict TypeScript or GTFO</title><link>https://decastro.work/blog/strict-typescript-or-gtfo/</link><pubDate>Sun, 28 May 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/strict-typescript-or-gtfo/</guid><description>&lt;p&gt;If your TypeScript config doesn’t run with &lt;code&gt;strict: true&lt;/code&gt;, you’re not using TypeScript—you’re writing JavaScript with extra keystrokes and a false sense of safety. TypeScript can’t magically fix sloppy typing after the fact. The discipline has to start at the compiler. Turn on strict mode, embrace the friction, and watch your codebase get cleaner, faster to navigate, and dramatically easier to change.&lt;/p&gt;
&lt;h2 id="what-strict-actually-changes-and-why-it-matters"&gt;What “strict” actually changes (and why it matters)&lt;/h2&gt;
&lt;p&gt;TypeScript’s &lt;code&gt;strict&lt;/code&gt; flag is a shortcut for enabling a bundle of stricter type-checking behaviors. When it’s off, the compiler becomes permissive in ways that directly undermine TypeScript’s value: it allows missing or inconsistent types, tolerates nullable mistakes, and lets implicit &lt;code&gt;any&lt;/code&gt; slip through your codebase.&lt;/p&gt;</description><content>&lt;p&gt;If your TypeScript config doesn’t run with &lt;code&gt;strict: true&lt;/code&gt;, you’re not using TypeScript—you’re writing JavaScript with extra keystrokes and a false sense of safety. TypeScript can’t magically fix sloppy typing after the fact. The discipline has to start at the compiler. Turn on strict mode, embrace the friction, and watch your codebase get cleaner, faster to navigate, and dramatically easier to change.&lt;/p&gt;
&lt;h2 id="what-strict-actually-changes-and-why-it-matters"&gt;What “strict” actually changes (and why it matters)&lt;/h2&gt;
&lt;p&gt;TypeScript’s &lt;code&gt;strict&lt;/code&gt; flag is a shortcut for enabling a bundle of stricter type-checking behaviors. When it’s off, the compiler becomes permissive in ways that directly undermine TypeScript’s value: it allows missing or inconsistent types, tolerates nullable mistakes, and lets implicit &lt;code&gt;any&lt;/code&gt; slip through your codebase.&lt;/p&gt;
&lt;p&gt;With strict mode on, TypeScript forces you to be explicit about the boundaries of your program:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is a value possibly &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;, or is it guaranteed?&lt;/li&gt;
&lt;li&gt;Does a function accept the arguments you think it does?&lt;/li&gt;
&lt;li&gt;Are you using a property that might not exist?&lt;/li&gt;
&lt;li&gt;Are you returning a value of the type you claimed?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t academic. Those questions correlate closely to real runtime failures—especially null/undefined crashes and “shape drift” bugs where objects change and code keeps compiling anyway.&lt;/p&gt;
&lt;h2 id="the-sprinkled-any-problem-how-teams-sabotage-typescript"&gt;The “sprinkled any” problem: how teams sabotage TypeScript&lt;/h2&gt;
&lt;p&gt;Here’s the pattern I’ve seen over and over: teams start with &lt;code&gt;strict: false&lt;/code&gt;, ship quickly, and convince themselves it’s fine. Then the codebase grows. Eventually you get this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers add &lt;code&gt;// @ts-ignore&lt;/code&gt; to silence errors.&lt;/li&gt;
&lt;li&gt;Others introduce &lt;code&gt;any&lt;/code&gt; because the compiler is “too strict.”&lt;/li&gt;
&lt;li&gt;Functions accept values typed as &lt;code&gt;any&lt;/code&gt; and cast their way through the mess.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once &lt;code&gt;any&lt;/code&gt; exists, TypeScript stops being a safety net. &lt;code&gt;any&lt;/code&gt; is contagious: when a value is &lt;code&gt;any&lt;/code&gt;, operations on it don’t get checked. You’ve effectively turned the compiler into a suggestion engine.&lt;/p&gt;
&lt;p&gt;A typical example looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;saveUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;any&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// TypeScript can’t help; everything is allowed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toLowerCase&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users&amp;#34;&lt;/span&gt;, { &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;JSON.stringify&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt; }) });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When this runs, you’re trusting that &lt;code&gt;input.email&lt;/code&gt; exists and is a string. Strict mode would force you to say what you accept—or validate it. That’s the difference between “works on my machine” and “works because it’s correct.”&lt;/p&gt;
&lt;p&gt;With strict mode, you’d end up writing something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SaveUserInput&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;saveUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;SaveUserInput&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;input&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toLowerCase&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/users&amp;#34;&lt;/span&gt;, { &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;JSON.stringify&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt; }) });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or if you’re dealing with untrusted data, you’d validate it at the boundary. Strict mode nudges you toward that healthier architecture: types at the edge, certainty inside.&lt;/p&gt;
&lt;h2 id="null-safety-the-bug-you-dont-want-in-production"&gt;Null safety: the bug you don’t want in production&lt;/h2&gt;
&lt;p&gt;If there’s one “entire class of failures” strict mode reliably reduces, it’s null reference errors—code that assumes a value exists when it doesn’t.&lt;/p&gt;
&lt;p&gt;Consider this non-strict code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;greet&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;name?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Hello &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toUpperCase&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;name&lt;/code&gt; is optional, then &lt;code&gt;user.name&lt;/code&gt; can be &lt;code&gt;undefined&lt;/code&gt;. Without strict null checks, TypeScript may let this slide. In production, that becomes a runtime exception:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cannot read properties of undefined (reading 'toUpperCase')&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With strict mode enabled, you’re forced to handle that reality:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;greet&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;name?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt; &lt;span style="color:#f92672"&gt;??&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Anonymous&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Hello &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toUpperCase&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or if &lt;code&gt;name&lt;/code&gt; must exist, the type should reflect that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;greet&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Hello &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toUpperCase&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s the key: strict mode makes invalid assumptions unignorable. You either change the code, validate inputs, or tighten the types until the program matches the contract.&lt;/p&gt;
&lt;h2 id="function-signatures-and-refactors-confidence-comes-from-the-compiler"&gt;Function signatures and refactors: confidence comes from the compiler&lt;/h2&gt;
&lt;p&gt;Strict typing isn’t only about preventing runtime crashes. It also prevents slow, painful refactors.&lt;/p&gt;
&lt;p&gt;When you change a function signature, strict mode helps ensure every call site updates correctly. Without strict mode, mismatches can compile and fail later—often in places you didn’t think to check.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;formatUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;age&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; (&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;age&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;)`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If a new team member starts calling it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;formatUser&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Sam&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;age&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;unknown&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Strict mode will catch it immediately. Without strict mode, you can end up passing wrong types through the system, then debugging the symptoms later.&lt;/p&gt;
&lt;p&gt;Even better, strict mode improves editor intelligence. With correct types everywhere, autocompletion and jump-to-definition become trustworthy. Onboarding improves because the codebase is readable: the types explain the shape of your system without forcing developers to memorize it.&lt;/p&gt;
&lt;p&gt;The result is not just fewer bugs—it’s cheaper change.&lt;/p&gt;
&lt;h2 id="practical-setup-turning-strict-mode-on-without-panicking"&gt;Practical setup: turning strict mode on without panicking&lt;/h2&gt;
&lt;p&gt;“Yes, it’s harder. That’s the point.” Still, you don’t have to flip the switch and walk into a compile-error avalanche. The goal is to make the compiler your ally, not your enemy.&lt;/p&gt;
&lt;p&gt;Start by checking your &lt;code&gt;tsconfig.json&lt;/code&gt;. At minimum:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;compilerOptions&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you’re currently off strict mode, the first build might produce a lot of errors. Resist the urge to spam &lt;code&gt;any&lt;/code&gt; everywhere to “get green.” Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fix the boundary errors first.&lt;/strong&gt; Areas that ingest data from the outside world (API responses, form input, message queues, file parsing) should define clear types and perform validation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add precise types instead of &lt;code&gt;any&lt;/code&gt;.&lt;/strong&gt; When you truly don’t know the type, use &lt;code&gt;unknown&lt;/code&gt; and narrow it. &lt;code&gt;unknown&lt;/code&gt; is strict; it forces you to check.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use narrowing properly.&lt;/strong&gt; Optional fields and discriminated unions should be handled with &lt;code&gt;if&lt;/code&gt; checks, &lt;code&gt;in&lt;/code&gt; operators, or switch statements on tagged types.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, suppose you have an API response that may include different shapes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ApiResult&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; } }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;|&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;handleResult&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;ApiResult&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Error(&lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;message&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Strict mode will help you maintain exhaustiveness. That’s how you prevent “new variant arrives and nothing updates” bugs.&lt;/p&gt;
&lt;p&gt;If you want a migration strategy that’s actually humane, consider tightening one strictness area at a time, but keep the long-term destination clear: you want the full &lt;code&gt;strict: true&lt;/code&gt; eventually. Half-measures tend to become permanent.&lt;/p&gt;
&lt;h2 id="when-strict-mode-is-painful-the-right-kind-of-pain"&gt;When strict mode is painful: the right kind of pain&lt;/h2&gt;
&lt;p&gt;Strict TypeScript isn’t about being precious. It’s about forcing your code to earn its correctness.&lt;/p&gt;
&lt;p&gt;If strict mode flags dozens of issues, that’s not “the compiler being annoying.” It’s the compiler pointing at places where your program currently has implicit assumptions. Those assumptions might not fail today—but they will fail eventually, and usually during the most inconvenient moment: a refactor, a deploy, an edge-case input, or a new data shape.&lt;/p&gt;
&lt;p&gt;So what do you do when strict mode is painful?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stop hiding behind &lt;code&gt;any&lt;/code&gt;.&lt;/strong&gt; Replace it with &lt;code&gt;unknown&lt;/code&gt; and narrow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don’t weaken types just to satisfy the compiler.&lt;/strong&gt; If you lie to TypeScript, you’ll pay later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make nullability explicit.&lt;/strong&gt; If a value can be absent, encode that in the type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat types as documentation.&lt;/strong&gt; Your future self will thank you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re building a real product, strictness is a form of respect—for users, for teammates, and for the person who inherits the code next.&lt;/p&gt;
&lt;h2 id="conclusion-strict-mode-is-the-default-your-future-needs"&gt;Conclusion: strict mode is the default your future needs&lt;/h2&gt;
&lt;p&gt;Turning on TypeScript strict mode is the simplest high-leverage decision you can make in a JavaScript-to-TypeScript world. It catches null reference errors, enforces correct function signatures, and prevents a flood of silent &lt;code&gt;any&lt;/code&gt; that erodes TypeScript’s value. The friction up front buys you reliability, onboarding speed, and refactor confidence that doesn’t rely on vibes.&lt;/p&gt;
&lt;p&gt;So enable &lt;code&gt;strict: true&lt;/code&gt;. Don’t “maybe later” it. If your codebase isn’t ready for strict mode, that’s your real roadmap—not an excuse to keep writing JavaScript with a TypeScript wrapper.&lt;/p&gt;</content></item><item><title>Turso and the Rise of Edge Databases</title><link>https://decastro.work/blog/turso-rise-of-edge-databases/</link><pubDate>Tue, 16 May 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/turso-rise-of-edge-databases/</guid><description>&lt;p&gt;For years, “move the database closer to the user” sounded like a nice optimization—until you tried to actually do it without turning your system into a distributed-systems science fair. Turso changes the equation by taking SQLite (yes, the tiny, ubiquitous database embedded in countless apps) and operationalizing it at the edge. The result is an architectural pattern that feels obvious in hindsight: local reads in milliseconds, global writes through replication, and application code that can live just as close to users as the data does.&lt;/p&gt;</description><content>&lt;p&gt;For years, “move the database closer to the user” sounded like a nice optimization—until you tried to actually do it without turning your system into a distributed-systems science fair. Turso changes the equation by taking SQLite (yes, the tiny, ubiquitous database embedded in countless apps) and operationalizing it at the edge. The result is an architectural pattern that feels obvious in hindsight: local reads in milliseconds, global writes through replication, and application code that can live just as close to users as the data does.&lt;/p&gt;
&lt;h2 id="why-edge-databases-suddenly-make-sense"&gt;Why edge databases suddenly make sense&lt;/h2&gt;
&lt;p&gt;Edge computing has matured fast: runtimes like Cloudflare Workers, Vercel Edge Functions, and similar platforms make it practical to run code near users. But most traditional database choices fight that model. If your database sits in a distant region, every query pays the latency tax. If you “shard by region,” you immediately inherit distributed consistency problems. And if you try to replicate, you need a replication strategy, conflict handling, and tooling that doesn’t turn every deploy into a gamble.&lt;/p&gt;
&lt;p&gt;The core promise of an edge database is simple: keep the critical working set close to where the user is. Not in theory—on purpose. Not “eventually,” unless that tradeoff is acceptable. Practically, it means you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Low-latency reads&lt;/strong&gt; near the user&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable write behavior&lt;/strong&gt; that still scales globally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A model you can operate&lt;/strong&gt; without rewriting your organization’s entire playbook&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where Turso’s approach lands: SQLite at the edge, with replication that keeps regional data patterns coherent enough for real applications.&lt;/p&gt;
&lt;h2 id="sqlite-at-the-edge-the-underrated-starting-point"&gt;SQLite at the edge: the underrated starting point&lt;/h2&gt;
&lt;p&gt;SQLite’s reputation is usually confined to embedded devices and single-node apps. But SQLite has two traits that make it a surprisingly good candidate for edge distribution:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;It’s compact and fast to initialize.&lt;/strong&gt; You don’t need a heavyweight server process per region just to serve basic queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its developer experience is straightforward.&lt;/strong&gt; SQL schemas, migrations, and local testing are familiar and cheap.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Turso effectively treats “SQLite everywhere” as a deployable architecture rather than a local convenience. Instead of building a custom distributed database system, you distribute the SQLite data plane. Reads happen locally at the edge location. Writes replicate so that other regions converge over time.&lt;/p&gt;
&lt;p&gt;That combination matters. The “edge database” pattern fails when you optimize only for locality (fast reads) but ignore correctness and operational realism (how writes propagate, how you handle failures, how you roll out schema changes). Turso’s core value is giving you a pragmatic path to that balance, using a database model many teams already know.&lt;/p&gt;
&lt;h2 id="the-replication-model-local-reads-global-writes"&gt;The replication model: local reads, global writes&lt;/h2&gt;
&lt;p&gt;Think about what you’re actually optimizing. For most apps, the biggest user-perceived latency is in the time to serve reads—fetching feeds, retrieving profiles, loading cached metadata, or rendering parts of a page. Writes are usually less frequent and more forgiving in how long they can take, as long as the system eventually reflects changes reliably.&lt;/p&gt;
&lt;p&gt;An edge SQLite system aligns with that reality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Local reads:&lt;/strong&gt; Your edge worker hits a nearby database replica, so “get this record” feels instant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global writes:&lt;/strong&gt; Updates are sent through replication to keep other edge locations informed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistency by design choice:&lt;/strong&gt; You’re not trying to pretend the speed of light doesn’t exist. You’re designing for the tradeoffs your application can tolerate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make this tangible, consider an e-commerce browsing experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A user in London requests the latest product availability and pricing.&lt;/li&gt;
&lt;li&gt;The edge database serves that read locally—no cross-Atlantic delay.&lt;/li&gt;
&lt;li&gt;When the inventory system updates a product, the write replicates out so that Paris, Frankfurt, and other nearby edges reflect the change without waiting for a centralized database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t “one true database” in the philosophical sense. It’s a practical distributed system where performance is local and propagation is global.&lt;/p&gt;
&lt;h2 id="pairing-turso-with-edge-compute-without-overengineering"&gt;Pairing Turso with edge compute (without overengineering)&lt;/h2&gt;
&lt;p&gt;Edge databases become powerful when you combine them with edge compute—especially request-driven runtimes like Cloudflare Workers. The pattern is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run your API logic at the edge.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query the edge SQLite replica for reads.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write updates when necessary, letting replication handle distribution.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s the practical advantage: your application’s hot path can avoid round-trips to a far-away data center. Instead, your “code and data are near the user,” which reduces latency and simplifies the tuning you’d otherwise do with caching layers.&lt;/p&gt;
&lt;p&gt;A concrete example: a multi-region content app.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writers update articles.&lt;/li&gt;
&lt;li&gt;Readers browse feeds and view article summaries.&lt;/li&gt;
&lt;li&gt;The edge layer needs to answer “what should I show right now?” with minimal latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With edge SQLite, the feed and summary reads can be local. When writers publish or update content, the writes replicate. The system can be designed so that clients tolerate a short propagation window—often acceptable for feeds and non-critical UI content.&lt;/p&gt;
&lt;p&gt;Where you should resist temptation: don’t try to force complex transactional workflows into an edge replication model. If you’re building a ledger-like system where every operation must be strongly consistent across regions, PostgreSQL (or another strongly consistent database) will likely fit better. Edge databases shine when your workload is &lt;strong&gt;read-heavy&lt;/strong&gt; and your data naturally clusters by region or by access pattern.&lt;/p&gt;
&lt;h2 id="when-edge-sqlite-is-a-great-fit-and-when-it-isnt"&gt;When edge SQLite is a great fit (and when it isn’t)&lt;/h2&gt;
&lt;p&gt;The most productive way to think about Turso is not “replace PostgreSQL,” but “apply a different database geometry to different problems.”&lt;/p&gt;
&lt;h3 id="great-fits"&gt;Great fits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-heavy applications:&lt;/strong&gt; dashboards, feeds, browsing experiences, search-adjacent metadata, configuration retrieval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regional data patterns:&lt;/strong&gt; content localized by geography, latency-sensitive personalization, or region-specific datasets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-facing performance wins:&lt;/strong&gt; anything where tens or hundreds of milliseconds matter to conversion or perceived responsiveness.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateless edge APIs:&lt;/strong&gt; edge functions that act like fast query frontends with occasional updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="less-ideal-fits"&gt;Less ideal fits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex cross-record transactions across regions:&lt;/strong&gt; if correctness depends on strict global ordering, edge replication may complicate matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-dominant workloads:&lt;/strong&gt; replication cost and propagation windows can outweigh benefits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Systems requiring strong, immediate consistency guarantees everywhere:&lt;/strong&gt; you can often build around eventual behavior, but you shouldn’t pretend it’s the same as single-node transactional semantics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good rule of thumb: if your product can tolerate “the update is visible everywhere soon,” edge SQLite is compelling. If it requires “the update is visible everywhere right now,” you’ll probably want a centralized, strongly consistent store or a hybrid design.&lt;/p&gt;
&lt;h2 id="operational-reality-schemas-migrations-and-rollouts"&gt;Operational reality: schemas, migrations, and rollouts&lt;/h2&gt;
&lt;p&gt;It’s easy to get seduced by latency graphs. The real work is keeping the system healthy over time. With edge SQLite, operational concerns look different than with a single managed cluster, but they’re still manageable if you stay disciplined.&lt;/p&gt;
&lt;p&gt;Practical advice for running an edge SQLite setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treat migrations like releases.&lt;/strong&gt; Schema changes should be versioned and rolled out deliberately, not improvised during peak traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep edge queries simple.&lt;/strong&gt; Avoid overly complex joins that are expensive and hard to reason about across replicated data. Edge is about speed and predictability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design with propagation in mind.&lt;/strong&gt; If a user updates something and immediately reads it, decide what behavior you want:
&lt;ul&gt;
&lt;li&gt;Prefer the local edge replica to reflect the write quickly, or&lt;/li&gt;
&lt;li&gt;Accept a brief window where reads reflect the pre-update state.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instrument the propagation path.&lt;/strong&gt; Log replication lag and failures as first-class signals. If you ignore it, you’ll eventually debug it under pressure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The biggest mindset shift is accepting that “distributed” isn’t a theoretical label—it’s an operational domain. But the good news is you don’t have to build everything yourself. The value of Turso is that you’re standing on a battle-tested foundation while still getting the performance benefits of edge locality.&lt;/p&gt;
&lt;h2 id="conclusion-the-edge-database-pattern-is-finally-practical"&gt;Conclusion: the edge database pattern is finally practical&lt;/h2&gt;
&lt;p&gt;Turso and the rise of edge databases aren’t about chasing hype. They’re about aligning architecture with the constraints that always existed—latency, geography, and operational complexity—and choosing a database distribution model that actually matches how users interact with your app. SQLite at the edge gives you local reads where they matter, replication for global updates, and an approachable development experience that teams can adopt without reinventing their stack.&lt;/p&gt;
&lt;p&gt;Edge SQLite isn’t a universal replacement for PostgreSQL. It’s better framed as a new default for read-heavy, region-sensitive workloads. When your product’s performance hinges on proximity, Turso’s paradigm shift can feel less like innovation and more like the missing piece you should have had all along.&lt;/p&gt;</content></item><item><title>Why I Switched My Personal Projects to Fly.io</title><link>https://decastro.work/blog/switched-personal-projects-fly-io/</link><pubDate>Wed, 10 May 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/switched-personal-projects-fly-io/</guid><description>&lt;p&gt;I didn’t “discover” Fly.io so much as I got tired of duct-taping infrastructure together—again. Every personal project eventually turns into a mini platform: builds, deploys, databases, secrets, scaling, logs, rollbacks. After enough cycles, you stop asking “can I deploy this?” and start asking “why does deploying this feel like a punishment?” That’s the moment Fly.io clicked for me.&lt;/p&gt;
&lt;h2 id="the-real-problem-wasnt-hostingit-was-friction"&gt;The real problem wasn’t hosting—it was friction&lt;/h2&gt;
&lt;p&gt;Most hosting platforms make one thing easy (usually “getting a server running”) and then quietly add friction everywhere else. For personal projects, that friction compounds:&lt;/p&gt;</description><content>&lt;p&gt;I didn’t “discover” Fly.io so much as I got tired of duct-taping infrastructure together—again. Every personal project eventually turns into a mini platform: builds, deploys, databases, secrets, scaling, logs, rollbacks. After enough cycles, you stop asking “can I deploy this?” and start asking “why does deploying this feel like a punishment?” That’s the moment Fly.io clicked for me.&lt;/p&gt;
&lt;h2 id="the-real-problem-wasnt-hostingit-was-friction"&gt;The real problem wasn’t hosting—it was friction&lt;/h2&gt;
&lt;p&gt;Most hosting platforms make one thing easy (usually “getting a server running”) and then quietly add friction everywhere else. For personal projects, that friction compounds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deployment feels fragile.&lt;/strong&gt; Small changes trigger big ceremony: build steps, env wiring, migrations, rollbacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The database becomes a separate world.&lt;/strong&gt; You end up juggling connection strings, backups, failover, and version mismatches—on top of your app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling is either hand-wavy or expensive.&lt;/strong&gt; You either guess and over-provision or get surprised when traffic grows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling gets in your way.&lt;/strong&gt; The CLI should reduce cognitive load, not create a new set of commands you memorize once and regret forever.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fly.io attacked this as a systems problem. Instead of pretending everything is “just a button,” it gives you primitives that map cleanly to how apps actually run: containers, networks, storage, databases, and a control plane that doesn’t require you to be an infrastructure engineer.&lt;/p&gt;
&lt;h2 id="edge-deployment-that-doesnt-feel-like-a-gimmick"&gt;Edge deployment that doesn’t feel like a gimmick&lt;/h2&gt;
&lt;p&gt;Fly’s core idea is simple: run your workloads closer to users. The practical payoff is lower latency and a deployment model that scales beyond a single region without you building a whole geographic strategy from scratch.&lt;/p&gt;
&lt;p&gt;In my case, I was building a small web app with an API and a dashboard. Users weren’t concentrated in one place, and I didn’t want to over-engineer. On Fly, I could ship the same Docker image and rely on Fly’s global infrastructure to handle placement.&lt;/p&gt;
&lt;p&gt;Here’s the concrete difference I noticed when comparing approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On “traditional” setups, I’d deploy to one region and then spend time convincing myself that performance was “good enough.”&lt;/li&gt;
&lt;li&gt;On Fly, performance became a default rather than a compromise. If a user is in a different geography, the app doesn’t have to wait for the slowest link in my architecture decisions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is that Fly still uses &lt;strong&gt;Docker containers&lt;/strong&gt; as the deployment unit. That matters because it keeps your workflow consistent: build once, run anywhere &lt;em&gt;within the Fly model&lt;/em&gt;, and don’t rewrite how you ship just because the hosting provider is fashionable.&lt;/p&gt;
&lt;h2 id="deployments-become-boring-in-the-best-way"&gt;Deployments become boring (in the best way)&lt;/h2&gt;
&lt;p&gt;Let’s talk about Docker—because for once, Docker isn’t the chore people fear. The workflow is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build a Docker image for your app.&lt;/li&gt;
&lt;li&gt;Deploy it to Fly using the CLI.&lt;/li&gt;
&lt;li&gt;Configure environment variables, ports, and services.&lt;/li&gt;
&lt;li&gt;Let Fly run it where it makes sense.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The best part is the &lt;strong&gt;CLI experience&lt;/strong&gt;. I’m not easily impressed by tools, but Fly’s CLI genuinely reduced my number of “what now?” moments. Commands are predictable. Feedback is helpful. You can tell what’s happening without squinting at logs like you’re decoding a firmware update.&lt;/p&gt;
&lt;p&gt;A personal example: I had a small Rails app that I rebuilt and redeployed frequently while iterating. With Fly, the “inner loop” felt tight. I could deploy, check health, inspect logs, and iterate quickly without turning deployment into a recurring afternoon.&lt;/p&gt;
&lt;p&gt;And because everything is containerized, you can keep local and remote behavior aligned. Your Dockerfile becomes the contract. That’s how you stop “works on my machine” from turning into “works in production, except when it doesn’t.”&lt;/p&gt;
&lt;h2 id="built-in-postgres-redis-and-litefs-less-glue-code"&gt;Built-in Postgres, Redis, and LiteFS: less glue code&lt;/h2&gt;
&lt;p&gt;Databases are where projects go to die—not because databases are hard, but because the integration surface area is huge. You start worrying about provisioning, networking, migrations, backups, and failover, and suddenly your app is no longer the main character.&lt;/p&gt;
&lt;p&gt;Fly’s built-in services changed the tone. Instead of assembling a database stack from separate vendors and then babysitting connectivity, I could treat Postgres and Redis as first-class components of the app.&lt;/p&gt;
&lt;p&gt;Even more interesting is &lt;strong&gt;LiteFS&lt;/strong&gt;, which gives you SQLite replication via a mechanism designed to keep developer workflows sane. For projects that benefit from SQLite (common for personal tooling, small APIs, and “I want speed without operational drag” apps), LiteFS is a pragmatic bridge: you get the simplicity of SQLite while still aiming for multi-instance resilience.&lt;/p&gt;
&lt;p&gt;What I did in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Postgres for apps where the relational model is the point and concurrency matters.&lt;/li&gt;
&lt;li&gt;Use SQLite for early-stage or lightweight services where operational simplicity wins.&lt;/li&gt;
&lt;li&gt;Add LiteFS when I want SQLite’s ergonomics without pretending single-instance is a long-term strategy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point isn’t that one database is always best. It’s that Fly gives you options without forcing you to learn and manage an entire ecosystem just to run queries.&lt;/p&gt;
&lt;h2 id="scaling-to-zero-feels-possible-not-theoretical"&gt;Scaling to zero feels possible, not theoretical&lt;/h2&gt;
&lt;p&gt;One of the most underrated features for personal projects is the ability to &lt;strong&gt;scale to zero&lt;/strong&gt;. When your app isn’t receiving traffic, you shouldn’t have to pay for idle resources—or keep processes running “just because.”&lt;/p&gt;
&lt;p&gt;Fly’s &lt;strong&gt;Machines API&lt;/strong&gt; makes scaling explicit and controllable. You can model your service instances instead of treating scaling as an opaque side effect of someone else’s defaults.&lt;/p&gt;
&lt;p&gt;In other words: if you want your app to wake up on demand, you’re not stuck with the idea that it must always be on. For my projects, this translated into less worry about “did I just leave something running all weekend?” and more freedom to experiment.&lt;/p&gt;
&lt;p&gt;This is also where Fly’s container-based approach shines again. When your workload is a machine definition, scaling becomes a workflow action, not a reconfiguration wizard.&lt;/p&gt;
&lt;h2 id="the-pricing-reality-cheaper-than-you-expect-generous-where-it-matters"&gt;The pricing reality: cheaper than you expect, generous where it matters&lt;/h2&gt;
&lt;p&gt;Pricing is always a trade-off, but what I appreciated about Fly is that the &lt;strong&gt;free tier is usable&lt;/strong&gt; for real personal projects—not just a demo environment you outgrow in a week.&lt;/p&gt;
&lt;p&gt;On the paid side, Fly also felt like it was pricing for developers who don’t want to do cost math every time they deploy. While I won’t pretend every workload maps perfectly, the overall experience for small systems was what I cared about: I could run a meaningful setup without feeling like I was renting a production-grade machine just to host a hobby.&lt;/p&gt;
&lt;p&gt;The best comparison isn’t a spreadsheet anyway—it’s your actual monthly behavior. When your stack is frictionless, you deploy more confidently, and the platform stops being a “cost center” you avoid and starts being a tool you rely on.&lt;/p&gt;
&lt;h2 id="what-switching-actually-changed-in-my-workflow"&gt;What switching actually changed in my workflow&lt;/h2&gt;
&lt;p&gt;After moving personal projects to Fly.io, the biggest change wasn’t performance or cost first—it was confidence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I shipped more often.&lt;/strong&gt; Deployments were less intimidating.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I spent more time on the app.&lt;/strong&gt; Fewer evenings went into infrastructure cleanup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I stopped re-learning the basics.&lt;/strong&gt; The same Docker-first model repeated across projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I had fewer “mystery failures.”&lt;/strong&gt; Built-in services reduced configuration surface area.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scaling felt like a knob, not a gamble.&lt;/strong&gt; Machines made it concrete.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re still on Heroku-style patterns (or you’re stuck in the “deployments are a ritual” phase), Fly offers a modern alternative: infrastructure primitives that feel approachable, not overwhelming.&lt;/p&gt;
&lt;p&gt;And yes—Fly feels like what Heroku would look like if it were built today with better defaults and cleaner operational semantics. Not because it’s flashy, but because it respects the fact that developers want to build software, not babysit pipelines.&lt;/p&gt;
&lt;h2 id="conclusion-flyio-is-the-platform-i-reach-for-first-now"&gt;Conclusion: Fly.io is the platform I reach for first now&lt;/h2&gt;
&lt;p&gt;I switched my personal projects to Fly.io because it solved the problems that always show up: Docker-based deployments, built-in Postgres/Redis, LiteFS for SQLite replication, and a CLI that doesn’t drain my patience. Add edge deployment and Machines-based scaling (including to zero), and you get a platform that fits the way solo developers and small teams actually work.&lt;/p&gt;
&lt;p&gt;If you want your projects to feel lighter—technically and emotionally—Fly.io is one of the few choices that makes that promise real.&lt;/p&gt;</content></item><item><title>We Put Rust in Production for Six Months. Here's What Actually Happened.</title><link>https://decastro.work/blog/rust-in-production-six-months-what-happened/</link><pubDate>Thu, 04 May 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rust-in-production-six-months-what-happened/</guid><description>&lt;p&gt;Replacing a production service is easy to justify in a slide deck and brutally hard to live through. We swapped our highest-throughput Python microservice for Rust with the usual promise: faster, cheaper, more reliable. And yes—those benefits showed up. But the story isn’t “Rust fixes everything.” It’s “Rust fixed &lt;em&gt;some&lt;/em&gt; things, while making other problems newly visible.” Here’s what really happened over six months, beyond the hype.&lt;/p&gt;
&lt;h2 id="the-starting-point-why-we-touched-the-service-at-all"&gt;The starting point: why we touched the service at all&lt;/h2&gt;
&lt;p&gt;Our Python service sat at the center of a high-traffic workflow. It wasn’t “toy” code: it was a core dependency with steady load, strict latency goals, and a performance profile that made ops tired of refreshing dashboards.&lt;/p&gt;</description><content>&lt;p&gt;Replacing a production service is easy to justify in a slide deck and brutally hard to live through. We swapped our highest-throughput Python microservice for Rust with the usual promise: faster, cheaper, more reliable. And yes—those benefits showed up. But the story isn’t “Rust fixes everything.” It’s “Rust fixed &lt;em&gt;some&lt;/em&gt; things, while making other problems newly visible.” Here’s what really happened over six months, beyond the hype.&lt;/p&gt;
&lt;h2 id="the-starting-point-why-we-touched-the-service-at-all"&gt;The starting point: why we touched the service at all&lt;/h2&gt;
&lt;p&gt;Our Python service sat at the center of a high-traffic workflow. It wasn’t “toy” code: it was a core dependency with steady load, strict latency goals, and a performance profile that made ops tired of refreshing dashboards.&lt;/p&gt;
&lt;p&gt;The pain was recognizable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt;: tail latency (p95/p99) was consistently worse than we wanted, especially under bursts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource pressure&lt;/strong&gt;: memory usage climbed with traffic; GC churn and fragmentation-like symptoms showed up in metrics and flame graphs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: we were paying for scale and still not getting “smooth” performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The simplest pitch to stakeholders was: “Rust will help us run the same workload with fewer resources and better predictability.” We sold the rewrite as a targeted modernization, not a wholesale replatform.&lt;/p&gt;
&lt;p&gt;Then reality started writing its own requirements.&lt;/p&gt;
&lt;h2 id="the-plan-we-made-and-how-it-went-wrong"&gt;The plan we made (and how it went wrong)&lt;/h2&gt;
&lt;p&gt;We estimated the rewrite would take “a few months.” That assumption was optimistic in a way only experienced teams can manage: we treated code migration as mostly mechanical. We underestimated the amount of time required to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Understand the old service’s behavior deeply&lt;/strong&gt; (not just read the code, but understand its edge cases and implicit contracts).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recreate correctness under concurrency&lt;/strong&gt; (Python masked a lot of sins with fewer threads and more forgiving timing).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build missing safety rails&lt;/strong&gt; (tests, benchmarks, and observability weren’t as complete as we needed).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rewrite took roughly &lt;strong&gt;3× longer than estimated&lt;/strong&gt;. The cause wasn’t Rust “being slow.” It was that migrating a production service is less like translating syntax and more like re-learning the system.&lt;/p&gt;
&lt;h3 id="a-practical-example-the-it-works-locally-trap"&gt;A practical example: the “it works locally” trap&lt;/h3&gt;
&lt;p&gt;We had a path that behaved fine in dev, then failed under load with subtle timing issues. In Python, the bug was partly hidden by the service model and by how execution interleaved. In Rust, everything was faster—and the faster version exposed the order-of-operations problem more clearly.&lt;/p&gt;
&lt;p&gt;We ended up spending time building instrumentation that we should have had from day one: request tracing across internal calls, structured logs that included correlation IDs, and benchmark harnesses that reproduced the hot paths.&lt;/p&gt;
&lt;p&gt;If you’re considering a rewrite, treat test/bench/trace work as first-class deliverables—not as cleanup after the “real” coding.&lt;/p&gt;
&lt;h2 id="what-improved-in-production-and-what-didnt"&gt;What improved in production (and what didn’t)&lt;/h2&gt;
&lt;p&gt;After rollout, the benefits were real, measurable, and—importantly—&lt;em&gt;stable&lt;/em&gt; rather than accidental.&lt;/p&gt;
&lt;h3 id="latency-dropped-dramatically"&gt;Latency dropped dramatically&lt;/h3&gt;
&lt;p&gt;Our latency improved substantially—&lt;strong&gt;about a 94% drop&lt;/strong&gt; in the metric we cared about most. What mattered wasn’t just average speed. It was predictability under load. Rust helped us reduce overhead in the hot path and avoid the “performance cliff” behavior we saw in Python when traffic spiked.&lt;/p&gt;
&lt;h3 id="memory-usage-fell-hard"&gt;Memory usage fell hard&lt;/h3&gt;
&lt;p&gt;We also saw &lt;strong&gt;~80% lower memory usage&lt;/strong&gt; for the service. A big part of that was simply running fewer processes / smaller footprints to achieve the same throughput, which became possible once we stopped paying the cost of Python runtime overhead and GC dynamics.&lt;/p&gt;
&lt;h3 id="the-aws-bill-moved"&gt;The AWS bill moved&lt;/h3&gt;
&lt;p&gt;Finally, the service got cheaper: we saw &lt;strong&gt;~60% reduction&lt;/strong&gt; in the AWS spend attributable to that microservice.&lt;/p&gt;
&lt;p&gt;Those numbers weren’t magic. They were the sum of less runtime overhead, more efficient handling of hot data paths, and a capacity plan that matched reality rather than optimism.&lt;/p&gt;
&lt;h3 id="what-didnt-automatically-improve"&gt;What didn’t automatically improve&lt;/h3&gt;
&lt;p&gt;Rust didn’t magically make our architecture better. If the service was slow because of an external dependency or a chatty interface, rewriting it wouldn’t fix that. In fact, in a few places we had to admit that the biggest wins came from tightening up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request batching strategy&lt;/li&gt;
&lt;li&gt;data serialization format and size&lt;/li&gt;
&lt;li&gt;internal allocation patterns&lt;/li&gt;
&lt;li&gt;concurrency model and backpressure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust gave us the tools; we still had to use them like adults.&lt;/p&gt;
&lt;h2 id="the-hard-parts-nobody-tells-you-about"&gt;The “hard parts” nobody tells you about&lt;/h2&gt;
&lt;p&gt;This is the section I wish we’d read before we started. Rust can be amazing, but it also has sharp edges that show up in the real world.&lt;/p&gt;
&lt;h3 id="compile-times-will-test-your-patience"&gt;Compile times will test your patience&lt;/h3&gt;
&lt;p&gt;Compile times are not theoretical. During active development, iteration speed became a bottleneck. We mitigated it with practical tactics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Limit what rebuilds&lt;/strong&gt;: keep module boundaries clean and avoid broad dependency graphs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use incremental builds&lt;/strong&gt; where possible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate dev loops&lt;/strong&gt;: wire up “compile + run + smoke test” commands so you’re not manually doing five steps every time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even then, the first time you change a core module and watch a full rebuild churn through your machine, you feel it.&lt;/p&gt;
&lt;h3 id="hiring-rust-developers-is-nearly-impossible"&gt;Hiring Rust developers is nearly impossible&lt;/h3&gt;
&lt;p&gt;This was the biggest operational surprise. We assumed “there are plenty of Rust devs.” In practice, for our needs—production experience with async, networking, observability, and a willingness to write unsafe-free code unless you truly mean it—we had fewer options than we wanted.&lt;/p&gt;
&lt;p&gt;We had to do one or more of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hire slower than planned&lt;/li&gt;
&lt;li&gt;rely on internal training&lt;/li&gt;
&lt;li&gt;accept a temporary productivity dip while people ramped up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust is not just a language switch; it’s a hiring and onboarding switch. If your timeline depends on filling seats quickly, plan for friction.&lt;/p&gt;
&lt;h3 id="the-rewrite-took-longer-because-correctness-costs-time"&gt;The rewrite took longer because “correctness” costs time&lt;/h3&gt;
&lt;p&gt;Rust’s ownership model is a gift, but it also forces you to confront design problems that dynamic languages can sometimes paper over. During the rewrite, we spent time on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lifetime and ownership boundaries&lt;/li&gt;
&lt;li&gt;data structure choices that reduce copying without overcomplicating&lt;/li&gt;
&lt;li&gt;error handling paths that we previously ignored&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That work is worthwhile. But it isn’t “free.” It’s more like &lt;em&gt;prepaying&lt;/em&gt; bugs at compile time rather than discovering them at runtime.&lt;/p&gt;
&lt;h2 id="how-we-made-rust-work-a-production-playbook"&gt;How we made Rust work: a production playbook&lt;/h2&gt;
&lt;p&gt;If you want a rewrite to actually succeed, here’s what helped most for us.&lt;/p&gt;
&lt;h3 id="1-treat-performance-engineering-as-part-of-the-spec"&gt;1) Treat performance engineering as part of the spec&lt;/h3&gt;
&lt;p&gt;Before porting anything, we established a benchmark plan for hot paths:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;microbenchmarks for isolated functions&lt;/li&gt;
&lt;li&gt;load tests for end-to-end behavior&lt;/li&gt;
&lt;li&gt;profiling during both development and rollout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When performance claims are just vibes, you lose time later. With benchmarks in place, you can make tradeoffs intentionally—like whether to optimize allocation patterns or to change the concurrency strategy first.&lt;/p&gt;
&lt;h3 id="2-build-observability-early-not-after-the-rewrite"&gt;2) Build observability early, not after the rewrite&lt;/h3&gt;
&lt;p&gt;Rust services can be “correct” and still be opaque. You need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;structured logging with request IDs&lt;/li&gt;
&lt;li&gt;metrics for queue depth, processing time, and error categories&lt;/li&gt;
&lt;li&gt;tracing across internal boundaries if you have multiple services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We learned this the hard way: once the service was faster, problems moved from “obviously slow” to “fast but wrong.” Better visibility mattered.&lt;/p&gt;
&lt;h3 id="3-choose-a-concurrency-model-you-can-explain-on-a-whiteboard"&gt;3) Choose a concurrency model you can explain on a whiteboard&lt;/h3&gt;
&lt;p&gt;Rust’s async ecosystem is powerful, but teams can create chaos by mixing patterns. We standardized our approach early, especially around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how work is spawned&lt;/li&gt;
&lt;li&gt;where backpressure happens&lt;/li&gt;
&lt;li&gt;how cancellation and timeouts are handled&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reduced incident complexity and made code reviews faster.&lt;/p&gt;
&lt;h3 id="4-dont-rewrite-everythingidentify-the-hot-path"&gt;4) Don’t rewrite everything—identify the hot path&lt;/h3&gt;
&lt;p&gt;The most important strategic decision was scope. We targeted our highest-throughput microservice and avoided a “Rust everything” impulse. The reason evangelists oversell Rust is that they often assume the bottlenecks align with CPU/memory overhead alone.&lt;/p&gt;
&lt;p&gt;In our case, the hot path was where Rust paid off. Other parts of the system were limited by external calls and orchestration—not by Python’s runtime characteristics. Those areas didn’t justify the migration cost.&lt;/p&gt;
&lt;h2 id="the-verdict-worth-it-but-only-for-the-right-constraints"&gt;The verdict: worth it, but only for the right constraints&lt;/h2&gt;
&lt;p&gt;Rust in production is absolutely worth it for certain problems. When your service is performance-critical, allocation-heavy, latency-sensitive, and you can invest in engineering rigor, Rust can deliver measurable gains quickly after the dust settles.&lt;/p&gt;
&lt;p&gt;But we also learned that “the right use cases” are narrower than the hype implies. If your service is mostly bound by network latency, third-party dependencies, or system design flaws that Rust can’t fix, a rewrite becomes expensive theater.&lt;/p&gt;
&lt;p&gt;And if your organization can’t support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the time cost of correctness work&lt;/li&gt;
&lt;li&gt;compile-time iteration overhead&lt;/li&gt;
&lt;li&gt;hiring/training for Rust production expertise&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…then the rewrite may turn into a multi-quarter tax with uncertain ROI.&lt;/p&gt;
&lt;p&gt;After six months, we don’t regret switching. We’re just more realistic about what to expect. The rewrite wasn’t a magic wand. It was a disciplined trade: we spent time and complexity upfront to buy performance predictability, memory efficiency, and lower operational cost.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Rust gave us real production improvements—lower latency, lower memory usage, and a lower bill. But the true lesson isn’t “Rust is better than Python.” It’s that a rewrite is a systems project: scope, testing, benchmarking, observability, hiring, and iteration speed all determine whether you get value or just accumulate complexity.&lt;/p&gt;
&lt;p&gt;If you’re considering Rust, be selective. Pick the parts of your system where performance constraints are actually inside your control, and build the safety rails before you start typing. That’s the path from hype to payoff.&lt;/p&gt;</content></item><item><title>PostgreSQL Dethroned MySQL and the Implications Are Huge</title><link>https://decastro.work/blog/postgresql-dethroned-mysql-implications-huge/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgresql-dethroned-mysql-implications-huge/</guid><description>&lt;p&gt;For years, “just use MySQL” was treated like database common sense—especially if you wanted to ship fast and avoid the complexity of “that other SQL.” But when PostgreSQL became the default choice for more developers than MySQL, it wasn’t a cosmetic change. It signaled a worldview shift: developers are increasingly selecting databases based on real capabilities, not convenience.&lt;/p&gt;
&lt;h2 id="the-quiet-end-of-whatever-comes-with-hosting"&gt;The quiet end of “whatever comes with hosting”&lt;/h2&gt;
&lt;p&gt;The old logic behind MySQL’s dominance was simple: it’s everywhere. It ships with tons of hosting stacks, tutorials assume it, and most teams never had to ask whether they actually needed anything more.&lt;/p&gt;</description><content>&lt;p&gt;For years, “just use MySQL” was treated like database common sense—especially if you wanted to ship fast and avoid the complexity of “that other SQL.” But when PostgreSQL became the default choice for more developers than MySQL, it wasn’t a cosmetic change. It signaled a worldview shift: developers are increasingly selecting databases based on real capabilities, not convenience.&lt;/p&gt;
&lt;h2 id="the-quiet-end-of-whatever-comes-with-hosting"&gt;The quiet end of “whatever comes with hosting”&lt;/h2&gt;
&lt;p&gt;The old logic behind MySQL’s dominance was simple: it’s everywhere. It ships with tons of hosting stacks, tutorials assume it, and most teams never had to ask whether they actually needed anything more.&lt;/p&gt;
&lt;p&gt;But popularity isn’t the same thing as fit. MySQL became the default because it was easy to deploy, not because it was the strongest long-term foundation for modern application patterns. PostgreSQL’s rise reflects a more modern decision-making loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You start with the problem (search, analytics, event logs, geospatial queries, background jobs).&lt;/li&gt;
&lt;li&gt;You evaluate database capabilities against that problem.&lt;/li&gt;
&lt;li&gt;You pick the database that reduces architectural contortions later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, teams are moving from “pick a database that’s convenient” to “pick a database that’s strategically correct.”&lt;/p&gt;
&lt;h2 id="postgresqls-real-superpower-it-adapts-to-your-workload"&gt;PostgreSQL’s real superpower: it adapts to your workload&lt;/h2&gt;
&lt;p&gt;PostgreSQL isn’t “just a relational database,” and that’s the point. It can cover multiple roles in the same system, which means fewer bolt-on services, fewer synchronization problems, and fewer “write it twice” compromises.&lt;/p&gt;
&lt;p&gt;Here are a few concrete ways teams actually use that flexibility:&lt;/p&gt;
&lt;h3 id="document-style-data-without-abandoning-sql-discipline"&gt;Document-style data without abandoning SQL discipline&lt;/h3&gt;
&lt;p&gt;PostgreSQL’s &lt;code&gt;JSONB&lt;/code&gt; support lets you store semi-structured documents while retaining relational strengths like indexing and query capabilities. For example, you might store user preferences as JSON, but still enforce constraints elsewhere and join cleanly to normalized tables.&lt;/p&gt;
&lt;p&gt;Practical takeaway: use &lt;code&gt;JSONB&lt;/code&gt; for fields that change shape, not as a substitute for modeling when the domain is stable.&lt;/p&gt;
&lt;h3 id="time-series-patterns-without-duct-taping"&gt;Time-series patterns without duct-taping&lt;/h3&gt;
&lt;p&gt;If your app tracks events—prices, sensor readings, clicks—PostgreSQL can handle time-based queries efficiently with the right schema and indexes. In many teams, this avoids spinning up a separate time-series database just to run “show me the last 7 days” queries.&lt;/p&gt;
&lt;p&gt;Practical takeaway: model time-series data with clear partitioning strategies (or at least indexes) early; don’t wait until dashboards are slow.&lt;/p&gt;
&lt;h3 id="geospatial-and-location-aware-features-that-dont-feel-hacked"&gt;Geospatial and “location-aware” features that don’t feel hacked&lt;/h3&gt;
&lt;p&gt;PostgreSQL is strong for geospatial data. If your product is map-driven—delivery zones, user proximity, route filtering—PostGIS-style workflows can keep those queries close to the data they depend on.&lt;/p&gt;
&lt;p&gt;Practical takeaway: if your domain includes geography, treat it as a first-class requirement, not a later add-on.&lt;/p&gt;
&lt;h3 id="vector-search-without-treating-your-database-like-a-leaky-cache"&gt;Vector search without treating your database like a leaky cache&lt;/h3&gt;
&lt;p&gt;Vector support in PostgreSQL enables embedding storage and similarity queries in the same transactional system as your relational data. That matters because your “what” (entities, permissions, metadata) often lives in relational tables, while your “how similar” lives in vectors. When the DB can do both, the application logic gets simpler and consistency gets better.&lt;/p&gt;
&lt;p&gt;Practical takeaway: if your ranking depends on both vector similarity and relational constraints (e.g., “only show results the user is allowed to see”), co-locating vectors with relational data can reduce query gymnastics.&lt;/p&gt;
&lt;p&gt;The underlying message is blunt: PostgreSQL gives developers fewer reasons to offload parts of the workload into special-purpose databases just because the original database couldn’t handle it cleanly.&lt;/p&gt;
&lt;h2 id="the-extensibility-advantage-sql-that-doesnt-cap-your-ambitions"&gt;The extensibility advantage: SQL that doesn’t cap your ambitions&lt;/h2&gt;
&lt;p&gt;The phrase “extensibility” can sound like marketing fluff until you’ve lived through what happens when you hit a wall.&lt;/p&gt;
&lt;p&gt;When a database is extensible, you can add capabilities without abandoning your existing schema, deployment model, and operational reality. PostgreSQL’s extension ecosystem—plus its ability to support advanced features natively—means teams can evolve without starting over.&lt;/p&gt;
&lt;p&gt;A practical example: imagine you need to implement custom scoring for search results. With a database that supports server-side logic and rich indexing, you can push more work toward the data layer. You might end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL functions for deterministic scoring&lt;/li&gt;
&lt;li&gt;indexing strategies that make ranking queries fast enough&lt;/li&gt;
&lt;li&gt;consistent behavior across services because logic lives in the database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That reduces drift: no more “service A computes scoring a little differently than service B” because the logic isn’t replicated everywhere.&lt;/p&gt;
&lt;h2 id="developers-choosing-capability-over-default-convenience"&gt;Developers choosing capability over default convenience&lt;/h2&gt;
&lt;p&gt;This is the real implication behind “Postgres overtook MySQL”: the industry is learning to stop treating databases as plumbing.&lt;/p&gt;
&lt;p&gt;Modern development pushes for tight feedback loops. You iterate quickly. Your schema changes frequently. Your product grows into new use cases. The cost of choosing a database that’s merely “good enough at day one” becomes a tax you pay every time you need to do something slightly unusual.&lt;/p&gt;
&lt;p&gt;PostgreSQL, as a category, supports a wider range of patterns without requiring a new platform for each pattern. That affects more than architecture diagrams—it affects team velocity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fewer migrations between databases because one finally “can’t do it”&lt;/li&gt;
&lt;li&gt;fewer services to secure, monitor, and maintain&lt;/li&gt;
&lt;li&gt;fewer edge cases around consistency and replication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever spent weeks coordinating cross-service data correctness problems, you know why teams get enthusiastic about putting more capability inside the database that already owns the truth.&lt;/p&gt;
&lt;h2 id="what-this-shift-means-for-teams-right-now"&gt;What this shift means for teams right now&lt;/h2&gt;
&lt;p&gt;It’s easy to celebrate a “winner,” but what matters is the action you take next.&lt;/p&gt;
&lt;h3 id="if-youre-starting-a-new-project"&gt;If you’re starting a new project&lt;/h3&gt;
&lt;p&gt;Don’t pick PostgreSQL because it’s trending—pick it because its strengths match your roadmap. Ask questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you need semi-structured data with queryable fields?&lt;/li&gt;
&lt;li&gt;Are you likely to add search, geospatial, analytics, or vector retrieval?&lt;/li&gt;
&lt;li&gt;Do you want application logic to remain consistent when requirements change?&lt;/li&gt;
&lt;li&gt;Are you aiming to reduce the number of supporting data services?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you can answer “yes” to even a couple, PostgreSQL is often the safer bet.&lt;/p&gt;
&lt;h3 id="if-youre-on-mysql-today"&gt;If you’re on MySQL today&lt;/h3&gt;
&lt;p&gt;You’re not obligated to panic or rewrite everything. Instead, be tactical:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify the first pain point that MySQL creates (query complexity, indexing limits, evolving data shape, performance under mixed workloads).&lt;/li&gt;
&lt;li&gt;Evaluate whether PostgreSQL would remove that pain in a way that reduces system complexity—not just “improves performance on one query.”&lt;/li&gt;
&lt;li&gt;Pilot migration for a bounded workload (one service, one dataset subset) and measure not only latency, but operational overhead and developer effort.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best migrations are boring. They start with a real workload and a clear definition of “success.”&lt;/p&gt;
&lt;h3 id="if-youre-hiring-and-standardizing-stacks"&gt;If you’re hiring and standardizing stacks&lt;/h3&gt;
&lt;p&gt;Your database choice influences hiring patterns, onboarding time, and tooling. When PostgreSQL becomes the default, you benefit from a larger pool of developers who already know how to model data using constraints, indexes, and extensions—and who can troubleshoot issues without guessing.&lt;/p&gt;
&lt;p&gt;Standardization also affects observability. You’ll want consistent metrics for query performance, lock contention, slow query logs, and index usage across environments. PostgreSQL’s ecosystem and tools make this easier—if you treat it as part of your engineering practice, not a one-time setup.&lt;/p&gt;
&lt;h2 id="conclusion-postgresql-isnt-just-winningits-changing-the-decision-model"&gt;Conclusion: PostgreSQL isn’t just winning—it’s changing the decision model&lt;/h2&gt;
&lt;p&gt;PostgreSQL overtaking MySQL isn’t merely a shift in ranking. It’s a signal that developers are selecting databases as strategic platforms, not as pre-installed conveniences. The implications are huge because the database you choose shapes how easily you can evolve your product—whether that evolution involves semi-structured data, geospatial queries, time-based analytics, or vector search.&lt;/p&gt;
&lt;p&gt;The era of “just use MySQL because it’s easy” is over. The next era is about fit: picking the database that lets your application grow without making you rebuild your foundations every time your requirements get interesting.&lt;/p&gt;</content></item><item><title>Zod Changed How I Think About Validation</title><link>https://decastro.work/blog/zod-changed-how-i-think-about-validation/</link><pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/zod-changed-how-i-think-about-validation/</guid><description>&lt;p&gt;I used to treat validation like paperwork: something you do at the edges “just in case.” Then I learned what TypeScript really does when the program runs: it doesn’t validate anything. Your types evaporate, and the real world leaks in—API responses, form data, environment variables, file uploads—turning “type-safe” code into a polite fiction. Zod didn’t just improve my validation. It changed how I reason about correctness in TypeScript.&lt;/p&gt;
&lt;h2 id="the-moment-types-stop-mattering"&gt;The moment types stop mattering&lt;/h2&gt;
&lt;p&gt;TypeScript’s type system is brilliant—at compile time. But at runtime, it’s gone. That’s not a bug; it’s how the language works. Once your application is running, &lt;code&gt;any&lt;/code&gt; silently wins the argument.&lt;/p&gt;</description><content>&lt;p&gt;I used to treat validation like paperwork: something you do at the edges “just in case.” Then I learned what TypeScript really does when the program runs: it doesn’t validate anything. Your types evaporate, and the real world leaks in—API responses, form data, environment variables, file uploads—turning “type-safe” code into a polite fiction. Zod didn’t just improve my validation. It changed how I reason about correctness in TypeScript.&lt;/p&gt;
&lt;h2 id="the-moment-types-stop-mattering"&gt;The moment types stop mattering&lt;/h2&gt;
&lt;p&gt;TypeScript’s type system is brilliant—at compile time. But at runtime, it’s gone. That’s not a bug; it’s how the language works. Once your application is running, &lt;code&gt;any&lt;/code&gt; silently wins the argument.&lt;/p&gt;
&lt;p&gt;Consider a typical boundary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You call an API endpoint.&lt;/li&gt;
&lt;li&gt;You receive JSON.&lt;/li&gt;
&lt;li&gt;You trust that it matches your TypeScript types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If that JSON is wrong—because of a backend change, a bad deploy, a malicious client, or even a subtle bug—you won’t know until something breaks later. And by then, the stack trace is often downstream from the actual cause, which makes debugging slower and more expensive.&lt;/p&gt;
&lt;p&gt;This is why “types-as-suggestions” becomes a daily reality. TypeScript may help you &lt;em&gt;write&lt;/em&gt; correct code, but it can’t guarantee that your &lt;em&gt;inputs&lt;/em&gt; are correct.&lt;/p&gt;
&lt;h2 id="validation-isnt-optionaljust-misplaced"&gt;Validation isn’t optional—just misplaced&lt;/h2&gt;
&lt;p&gt;Before Zod, I’d sprinkle validation in a scattered way: ad-hoc checks, &lt;code&gt;if&lt;/code&gt; statements, a few runtime asserts, maybe a lightweight schema somewhere in the backend. The trouble is placement. Validation after you’ve already assumed a shape leads to the classic failure mode:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“The API returned something I didn’t expect,” but the error appears two layers later when you try to access a property.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Instead of validating at the boundary, many projects validate “somewhere near” the boundary, hoping it’s close enough. It usually isn’t.&lt;/p&gt;
&lt;p&gt;Here’s a concrete example. Let’s say you expect this from an API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#a6e22e"&gt;email?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You fetch it, then do:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;&lt;span style="color:#f92672"&gt;?&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toLowerCase&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the backend accidentally returns &lt;code&gt;{ id: 123, username: &amp;quot;...&amp;quot; }&lt;/code&gt;, TypeScript won’t help at runtime. You’ll get &lt;code&gt;user.email is undefined&lt;/code&gt; or—worse—an exception when you call &lt;code&gt;.toLowerCase()&lt;/code&gt; on a value that isn’t a string.&lt;/p&gt;
&lt;p&gt;Zod pushes validation to the correct location: right where untrusted data crosses into your typed world.&lt;/p&gt;
&lt;h2 id="zod-one-schema-two-guarantees"&gt;Zod: one schema, two guarantees&lt;/h2&gt;
&lt;p&gt;Zod changes the workflow by making your schema the source of truth. You define what you accept once, and you get:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Runtime validation&lt;/strong&gt;: real checks while the program runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type inference&lt;/strong&gt;: compile-time types derived from the schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That combination is the whole point. You stop writing “types” that look correct to the compiler but do nothing to protect the program when reality disagrees.&lt;/p&gt;
&lt;p&gt;A Zod schema for a &lt;code&gt;User&lt;/code&gt; might look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;z&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;zod&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserSchema&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;z&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;object&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.string&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.string&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.string&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;optional&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;z&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;infer&lt;/span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;typeof&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserSchema&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now your API boundary becomes explicit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;raw&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;getUser&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// unknown, from the real world
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserSchema&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;raw&lt;/span&gt;); &lt;span style="color:#75715e"&gt;// validates or throws
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If the payload is wrong, you fail immediately and locally, right where the assumption is made. That single shift—validating at the edge—eliminates an entire class of bugs.&lt;/p&gt;
&lt;h3 id="parsing-vs-just-checking"&gt;Parsing vs. “just checking”&lt;/h3&gt;
&lt;p&gt;Zod’s &lt;code&gt;parse()&lt;/code&gt; gives you a hard guarantee: either you get a valid typed value or you throw. There’s no pretending.&lt;/p&gt;
&lt;p&gt;If you want a non-throwing approach, Zod also provides &lt;code&gt;safeParse()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;result&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserSchema&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;safeParse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;raw&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;success&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// handle validation errors cleanly
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;result&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In practice, I prefer &lt;code&gt;parse()&lt;/code&gt; for internal invariants and &lt;code&gt;safeParse()&lt;/code&gt; when I’m building user-facing error handling (like form submission feedback).&lt;/p&gt;
&lt;h2 id="turning-validation-into-a-system-not-a-chore"&gt;Turning validation into a system (not a chore)&lt;/h2&gt;
&lt;p&gt;The best part of Zod isn’t the syntax—it’s the discipline it encourages. Your schemas become reusable assets instead of one-off checks.&lt;/p&gt;
&lt;h3 id="forms-validate-where-the-user-submits"&gt;Forms: validate where the user submits&lt;/h3&gt;
&lt;p&gt;When form data is involved, the input is almost always untrusted: everything starts as strings, numbers may arrive as malformed strings, optional fields might be missing, and browsers are inconsistent in edge cases.&lt;/p&gt;
&lt;p&gt;Zod makes it easy to model the exact input contract. For example, a login form:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoginSchema&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;z&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;object&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.string&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.string&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;min&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, on submit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;raw&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Object.&lt;span style="color:#a6e22e"&gt;fromEntries&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FormData&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;form&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;LoginSchema&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;raw&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;login&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;password&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll notice what’s happening: you’re not “validating fields somewhere.” You’re validating the entire payload &lt;em&gt;as a single unit&lt;/em&gt; right before you use it.&lt;/p&gt;
&lt;p&gt;That’s how you avoid the slow creep of partial checks that miss a field you didn’t think about.&lt;/p&gt;
&lt;h3 id="environment-variables-validate-at-startup"&gt;Environment variables: validate at startup&lt;/h3&gt;
&lt;p&gt;Environment variables are the silent source of runtime “unknown.” They’re strings. Sometimes empty. Sometimes missing. Sometimes set differently across deployments.&lt;/p&gt;
&lt;p&gt;With Zod, you can validate immediately on boot:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EnvSchema&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;z&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;object&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;PORT&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.coerce.number&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;int&lt;/span&gt;().&lt;span style="color:#a6e22e"&gt;min&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;max&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;65535&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;NODE_ENV&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;z.enum&lt;/span&gt;([&lt;span style="color:#e6db74"&gt;&amp;#34;development&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;production&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;test&amp;#34;&lt;/span&gt;]),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;env&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EnvSchema&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;parse&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;process&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;env&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now your application fails fast with a clear reason instead of stumbling later with strange behavior. You also get types for &lt;code&gt;env.PORT&lt;/code&gt; and &lt;code&gt;env.NODE_ENV&lt;/code&gt; without manual duplication.&lt;/p&gt;
&lt;h2 id="trpc--zod-end-to-end-type-safety-in-real-life"&gt;tRPC + Zod: end-to-end type safety in real life&lt;/h2&gt;
&lt;p&gt;Zod alone already changes your runtime story. But the real payoff shows up when you combine it with end-to-end frameworks like tRPC—where types flow across the network instead of resetting to “just JSON.”&lt;/p&gt;
&lt;p&gt;The pattern looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define a Zod schema for inputs/outputs.&lt;/li&gt;
&lt;li&gt;Use the inferred types in your TypeScript code.&lt;/li&gt;
&lt;li&gt;Ensure the client and server agree on the shape of data.&lt;/li&gt;
&lt;li&gt;Validate at the boundary so malformed data can’t masquerade as valid types.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever experienced the horror of a frontend and backend “agreeing” for weeks and then failing on deploy, you understand why this matters. A schema-driven approach makes contract changes intentional: if you update the contract, you update the schema, and the rest of the code follows.&lt;/p&gt;
&lt;p&gt;Even better, errors become actionable. Instead of chasing a downstream &lt;code&gt;Cannot read property 'x' of undefined&lt;/code&gt;, you get validation feedback that says the payload was wrong for the expected schema. That’s not just safer—it’s dramatically faster to debug.&lt;/p&gt;
&lt;h2 id="what-id-say-to-teams-still-without-zod"&gt;What I’d say to teams still without Zod&lt;/h2&gt;
&lt;p&gt;If you’re writing TypeScript without runtime validation, you’re not “being productive.” You’re deferring risk. The compiler will help you with internal correctness, but it cannot protect you from external inputs. Sooner or later, something outside your codebase disagrees with your types.&lt;/p&gt;
&lt;p&gt;My opinionated recommendation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treat every boundary as &lt;code&gt;unknown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Validate with Zod immediately on entry.&lt;/li&gt;
&lt;li&gt;Prefer schema-first designs where the schema drives both runtime behavior and TypeScript types.&lt;/li&gt;
&lt;li&gt;When possible, share schemas across client/server to keep the contract tight.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A small migration can deliver outsized returns. Start with your most critical boundaries: login/signup payloads, payment requests, environment config, and any endpoint that drives UI state. Those are where unexpected shapes turn into the most painful failures.&lt;/p&gt;
&lt;h2 id="conclusion-correctness-that-survives-contact-with-reality"&gt;Conclusion: correctness that survives contact with reality&lt;/h2&gt;
&lt;p&gt;Zod changed how I think about validation because it removed the split between “what TypeScript says” and “what actually happens.” Types are great—until runtime arrives. With Zod, your schemas enforce reality at the edges, and your TypeScript types stay in sync automatically. Add tRPC and you get end-to-end contracts that don’t crumble the moment JSON shows up.&lt;/p&gt;
&lt;p&gt;If you want fewer “API returned something weird” bugs, stop treating validation as optional. Put your guarantees where they belong: at the boundary.&lt;/p&gt;</content></item><item><title>Serverless Was Overhyped (But the Right Parts Won)</title><link>https://decastro.work/blog/serverless-overhyped-right-parts-won/</link><pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/serverless-overhyped-right-parts-won/</guid><description>&lt;p&gt;“Serverless” didn’t fail because the technology was bad—it failed because people tried to use it for the one thing it was never designed to be great at. In the 2018–2020 boom cycle, teams treated Lambda like a universal solvent for application architecture. The result was predictable: broken assumptions, latency surprises, and brittle designs that collapsed under real traffic and real state.&lt;/p&gt;
&lt;p&gt;But here’s the revisionist truth worth keeping: serverless didn’t replace servers. It carved out a few genuinely excellent niches. And if you use it for those niches—event-driven processing, scheduled jobs, webhook handlers, and the kind of pipelines that naturally flow through queues and storage—it’s not just acceptable. It’s often the best tool in the box.&lt;/p&gt;</description><content>&lt;p&gt;“Serverless” didn’t fail because the technology was bad—it failed because people tried to use it for the one thing it was never designed to be great at. In the 2018–2020 boom cycle, teams treated Lambda like a universal solvent for application architecture. The result was predictable: broken assumptions, latency surprises, and brittle designs that collapsed under real traffic and real state.&lt;/p&gt;
&lt;p&gt;But here’s the revisionist truth worth keeping: serverless didn’t replace servers. It carved out a few genuinely excellent niches. And if you use it for those niches—event-driven processing, scheduled jobs, webhook handlers, and the kind of pipelines that naturally flow through queues and storage—it’s not just acceptable. It’s often the best tool in the box.&lt;/p&gt;
&lt;h2 id="the-serverless-everything-mistake"&gt;The “serverless everything” mistake&lt;/h2&gt;
&lt;p&gt;The hype train sold a simple promise: stop managing servers, scale automatically, pay only for what you use. That’s all true—at least at the level of individual services. The problem was how quickly “no servers” became a design philosophy instead of an implementation detail.&lt;/p&gt;
&lt;p&gt;Most applications aren’t stateless monoliths you can decompose into independent functions with zero friction. They need at least some of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Persistent connections&lt;/strong&gt; (WebSockets, long-lived database sessions, streaming responses)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local state&lt;/strong&gt; that lives for the lifetime of a request flow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable latency&lt;/strong&gt; for interactive UX&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational habits&lt;/strong&gt; that assume warm process lifecycles and stable execution contexts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Serverless can sometimes mimic these patterns, but the cost is usually hidden complexity—clever caching strategies, fragile workarounds for state, and “best effort” latency management. Teams learned the hard way that architecture is about constraints, not slogans.&lt;/p&gt;
&lt;p&gt;My favorite example from that era: the “Lambda behind API Gateway for a REST API” pattern used as a default. For lightweight endpoints, it can work. But when you wrap every endpoint in a serverless façade without thinking through connection behavior, timeouts, request/response size, observability, and warm-up realities, you end up optimizing for convenience instead of performance and reliability. In many cases, the experience wasn’t dramatically better than running a small fleet of containers—and the failure modes were different enough to make troubleshooting more annoying.&lt;/p&gt;
&lt;h2 id="why-serverless-never-meant-no-state-just-no-babysitting"&gt;Why serverless never meant “no state,” just “no babysitting”&lt;/h2&gt;
&lt;p&gt;Serverless functions are ephemeral execution environments. That does &lt;em&gt;not&lt;/em&gt; mean you can’t build stateful systems. It means you must externalize state.&lt;/p&gt;
&lt;p&gt;That distinction is where teams either matured—or burned time. A healthy serverless architecture assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compute is disposable.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State lives outside the function.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You design for idempotency&lt;/strong&gt; because retries happen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You treat timeouts and concurrency as first-class constraints.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of trying to keep “session state” in memory, you store it in something like Redis or a database. Instead of assuming a function will always run once, you design for “run it again safely.” Instead of expecting a request to have unbounded duration, you set time budgets and push longer workflows into asynchronous pipelines.&lt;/p&gt;
&lt;p&gt;This framing turns “serverless limitations” into straightforward engineering constraints. The system stops being magical and starts being predictable.&lt;/p&gt;
&lt;h2 id="the-right-pattern-event-driven-processing-not-everything-behind-http"&gt;The right pattern: event-driven processing (not everything behind HTTP)&lt;/h2&gt;
&lt;p&gt;The most natural serverless use case is boring in the best way: &lt;strong&gt;events in, work out&lt;/strong&gt;. When your inputs come from systems that already speak in events—object storage uploads, message queues, webhook notifications—Lambda shines because it can scale compute to match incoming demand.&lt;/p&gt;
&lt;h3 id="s3-uploads--image-pipeline"&gt;S3 uploads → image pipeline&lt;/h3&gt;
&lt;p&gt;Take image processing. A user uploads an image. That’s an event. You don’t need an HTTP server sitting there for the rest of time; you need a pipeline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Upload to &lt;strong&gt;S3&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Trigger processing (e.g., resize, compress, generate thumbnails)&lt;/li&gt;
&lt;li&gt;Store results back to S3&lt;/li&gt;
&lt;li&gt;Optionally notify downstream services or update metadata&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In practice, you get a workflow like “S3 event → function → write outputs.” The user’s upload and the derived processing are decoupled. If the pipeline is slower, it doesn’t stall user-facing traffic. If you need to add another step—say, watermarking—you extend the pipeline without reshaping the entire application.&lt;/p&gt;
&lt;h3 id="sqs-messages--background-jobs"&gt;SQS messages → background jobs&lt;/h3&gt;
&lt;p&gt;Queues are another sweet spot because they’re fundamentally aligned with serverless execution models. You receive a message, do the work, acknowledge, repeat. If processing fails, retries happen. The important part is that you implement idempotency so “at least once delivery” doesn’t become “duplicate side effects.”&lt;/p&gt;
&lt;p&gt;A practical rule: make your work conditional on a stable identifier. For example, if a message represents “process order 123,” write results keyed by order ID, and safely no-op if the work already completed.&lt;/p&gt;
&lt;p&gt;This is also where operational sanity improves. Instead of scaling an application tier and managing job runners, you scale the event consumer and let the queue absorb bursts.&lt;/p&gt;
&lt;h3 id="webhooks--quick-durable-responses"&gt;Webhooks → quick, durable responses&lt;/h3&gt;
&lt;p&gt;Webhook handlers are another clean fit: receive an event from an external system, validate it, record it, trigger async follow-up work. The key is to keep the synchronous part short—ack quickly, then do heavier processing asynchronously.&lt;/p&gt;
&lt;p&gt;If you try to do “everything” in the webhook request, you’ll hit timeouts and create cascading retries. If you treat the webhook as a trigger and move the real work to a queue or pipeline, serverless becomes an advantage rather than a gamble.&lt;/p&gt;
&lt;h2 id="scheduled-jobs-let-time-be-the-trigger"&gt;Scheduled jobs: let time be the trigger&lt;/h2&gt;
&lt;p&gt;Not every useful workload starts with an external event. Some starts with time: nightly reports, cache refreshes, cleanup tasks, batch processing windows.&lt;/p&gt;
&lt;p&gt;Serverless scheduled invocations are ideal for these because you avoid building and maintaining the “cron runner” infrastructure. You also get clear boundaries: each job run is its own execution. That makes debugging and reruns easier.&lt;/p&gt;
&lt;p&gt;A practical pattern is “scheduled function → enqueue work.” Don’t have the scheduler do heavy lifting itself if the task can fan out. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheduled run at 02:00&lt;/li&gt;
&lt;li&gt;Query for “items needing refresh”&lt;/li&gt;
&lt;li&gt;Enqueue one message per item (or per shard)&lt;/li&gt;
&lt;li&gt;Worker functions process in parallel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reduces the blast radius of failures and prevents one long-running job from turning into a recurring outage.&lt;/p&gt;
&lt;h2 id="the-latency-and-connection-reality-and-how-to-work-with-it"&gt;The latency and connection reality (and how to work with it)&lt;/h2&gt;
&lt;p&gt;The hype glossed over the fact that serverless is not optimized for every interaction pattern. Execution time, cold starts, concurrency behavior, and integration timeouts are constraints you must respect.&lt;/p&gt;
&lt;p&gt;So how do you build well anyway?&lt;/p&gt;
&lt;h3 id="design-for-short-synchronous-work"&gt;Design for short synchronous work&lt;/h3&gt;
&lt;p&gt;Use serverless for the part of the request that benefits from elasticity and isolation. If a request needs slow external calls or heavy computation, push it into an async workflow. Then return something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Accepted” with a job ID&lt;/li&gt;
&lt;li&gt;Or update the UI via polling/websocket later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you’re using serverless behind HTTP for specific endpoints, you still want the synchronous path to be lean.&lt;/p&gt;
&lt;h3 id="externalize-dependencies-and-caches"&gt;Externalize dependencies and caches&lt;/h3&gt;
&lt;p&gt;If you’re repeatedly calling downstream services, cache aggressively &lt;em&gt;outside&lt;/em&gt; the function so you’re not depending on warm invocations. Treat caches as best-effort, not correctness.&lt;/p&gt;
&lt;h3 id="make-retries-boring"&gt;Make retries boring&lt;/h3&gt;
&lt;p&gt;Assume your function might run multiple times for the same logical event. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use unique keys for side effects&lt;/li&gt;
&lt;li&gt;Deduplicate where possible&lt;/li&gt;
&lt;li&gt;Write results in a way that can be replayed safely&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one of the most important “culture shifts” serverless demands. Once teams embrace it, the system becomes sturdier rather than fragile.&lt;/p&gt;
&lt;h2 id="when-you-should-avoid-lambda-and-what-to-use-instead"&gt;When you should avoid Lambda (and what to use instead)&lt;/h2&gt;
&lt;p&gt;The blunt take: if your workload needs stable, long-lived connections or you’re building a real-time system with tight latency expectations, serverless may fight you. Likewise, if you want to manage complex in-memory state across requests, you’re probably recreating a server—just with extra steps.&lt;/p&gt;
&lt;p&gt;In those cases, you’ll often be better off with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long-running containers or services&lt;/strong&gt; for streaming and persistent connections&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Managed databases and queues&lt;/strong&gt; paired with a service tier where appropriate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event-driven services&lt;/strong&gt; where serverless is a worker, not the entire application boundary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you’re thinking about doing “Lambda everywhere behind API Gateway,” do it selectively. Start with endpoints that are naturally request/response and lightweight—or endpoints that are easy to observe and easy to retry safely. Don’t make it your default architecture just because it’s trendy.&lt;/p&gt;
&lt;h2 id="conclusion-serverless-didnt-dieit-grew-up"&gt;Conclusion: serverless didn’t die—it grew up&lt;/h2&gt;
&lt;p&gt;The serverless everything era was overconfident, and it paid the price. But the core idea wasn’t wrong; it was the application of the idea that was. Lambda didn’t replace servers—it found a niche where it’s genuinely excellent: event-driven processing, scheduled jobs, webhook handling, and image/asset pipelines that naturally decompose into asynchronous steps.&lt;/p&gt;
&lt;p&gt;If you build with the grain of serverless—externalized state, idempotent work, short synchronous paths, and async fan-out—you get a system that’s easier to scale, easier to reason about, and often cheaper to operate. Serverless wasn’t the future of every workload. It was the future of the right workloads. And that future is still here.&lt;/p&gt;</content></item><item><title>htmx Is a Rebellion Against JavaScript Complexity (and I'm Joining)</title><link>https://decastro.work/blog/htmx-rebellion-against-javascript-complexity/</link><pubDate>Wed, 29 Mar 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/htmx-rebellion-against-javascript-complexity/</guid><description>&lt;p&gt;There’s a moment in every developer’s life when they realize they’re not building an app anymore—they’re building a build system to build an app. htmx is my loud, unapologetic answer to that feeling. It doesn’t “replace” JavaScript so much as it rejects the idea that every interactive UI needs a bespoke JavaScript universe. Instead: extend HTML, let the server do the heavy lifting, and ship.&lt;/p&gt;
&lt;p&gt;This is not nostalgia. It’s pragmatism—especially if your app looks like most real apps: CRUD screens, searchable lists, dashboards, comment threads, account pages, and all the workflows that don’t require a game engine in the browser.&lt;/p&gt;</description><content>&lt;p&gt;There’s a moment in every developer’s life when they realize they’re not building an app anymore—they’re building a build system to build an app. htmx is my loud, unapologetic answer to that feeling. It doesn’t “replace” JavaScript so much as it rejects the idea that every interactive UI needs a bespoke JavaScript universe. Instead: extend HTML, let the server do the heavy lifting, and ship.&lt;/p&gt;
&lt;p&gt;This is not nostalgia. It’s pragmatism—especially if your app looks like most real apps: CRUD screens, searchable lists, dashboards, comment threads, account pages, and all the workflows that don’t require a game engine in the browser.&lt;/p&gt;
&lt;h2 id="the-problem-isnt-javascriptits-the-complexity-tax"&gt;The problem isn’t JavaScript—it’s the complexity tax&lt;/h2&gt;
&lt;p&gt;Modern front-end tooling has trained us to believe that interactivity always comes with a specific stack shape: a client-side framework, a bundler, a state management library, a router, a data-fetch layer, a form abstraction, and then a pile of glue code to keep it all from falling apart.&lt;/p&gt;
&lt;p&gt;You can see the pattern in how teams talk:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“We’re rewriting the state management.”&lt;/li&gt;
&lt;li&gt;“We need a new data layer.”&lt;/li&gt;
&lt;li&gt;“The UI is hard because the data is hard.”&lt;/li&gt;
&lt;li&gt;“We’re fighting re-renders.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be clear: JavaScript is powerful. Frameworks are useful. But the default industry approach often charges an upfront complexity tax on every page, regardless of whether the page needs it.&lt;/p&gt;
&lt;p&gt;Most CRUD apps don’t need client-side routing gymnastics. They don’t need a virtual DOM diffing strategy. They need predictable behavior, fast iteration, and a server that’s already great at generating HTML.&lt;/p&gt;
&lt;p&gt;htmx asks the simple question we avoid: why are we replacing a perfectly good document with a second, synchronized document?&lt;/p&gt;
&lt;h2 id="the-htmx-idea-make-html-interactive-not-obsolete"&gt;The htmx idea: make HTML interactive, not obsolete&lt;/h2&gt;
&lt;p&gt;htmx takes HTML seriously. It turns “static markup plus a small enhancement layer” into a full application style—without insisting you abandon HTML or introduce a virtual DOM.&lt;/p&gt;
&lt;p&gt;The core move is this: use HTML attributes to declare behavior.&lt;/p&gt;
&lt;p&gt;Instead of writing a component that fetches data, renders UI, and updates state, you write markup that says things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When this button is clicked, make a request.&lt;/li&gt;
&lt;li&gt;When the response arrives, swap a portion of the page.&lt;/li&gt;
&lt;li&gt;When the user types, debounce and query.&lt;/li&gt;
&lt;li&gt;When a form is submitted, send it and replace the results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s it. The “state” lives where it should for these apps: on the server, reflected in HTML.&lt;/p&gt;
&lt;h3 id="a-tiny-example-delete-with-a-confirmation-and-a-swap"&gt;A tiny example: delete with a confirmation and a swap&lt;/h3&gt;
&lt;p&gt;Imagine a list of items:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;items&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;item-42&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;span&lt;/span&gt;&amp;gt;Item 42&amp;lt;/&lt;span style="color:#f92672"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-delete&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/items/42&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-confirm&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Delete this item?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-target&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;#item-42&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;hx-swap&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;outerHTML&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Delete
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No client-side store. No reducer. No rehydration drama. Your delete endpoint returns what the UI should become—often &lt;code&gt;204&lt;/code&gt; for “remove it,” or a small fragment to replace the row.&lt;/p&gt;
&lt;p&gt;This is not magic; it’s mechanical sympathy. The browser remains the browser: it loads a page, you update parts of it, and the DOM stays the DOM.&lt;/p&gt;
&lt;h2 id="no-build-step-doesnt-mean-no-discipline"&gt;“No build step” doesn’t mean “no discipline”&lt;/h2&gt;
&lt;p&gt;Let’s clear a common misconception: “no build step” doesn’t mean “no engineering.” You still need boundaries, good endpoints, and a clean mental model for what’s rendered where.&lt;/p&gt;
&lt;p&gt;What htmx changes is &lt;em&gt;where&lt;/em&gt; complexity belongs.&lt;/p&gt;
&lt;p&gt;With a React-style approach, complexity migrates into JavaScript architecture: component trees, client caching rules, state synchronization, optimistic updates, and a thicket of abstractions. With htmx, the complexity shifts back toward server design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your endpoints become the UI’s “program.”&lt;/li&gt;
&lt;li&gt;Your HTML fragments become the UI’s “render output.”&lt;/li&gt;
&lt;li&gt;Your controllers decide what state exists and what the user sees next.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: design endpoints that return partial HTML fragments with clear responsibility.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /items&lt;/code&gt; returns a full page for initial load.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /items/search?query=...&lt;/code&gt; returns a fragment for the results list.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /items&lt;/code&gt; returns the updated list fragment (or the created row).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /items/:id&lt;/code&gt; returns a fragment to remove or replace the row.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your UI composition becomes a series of intentional swaps.&lt;/p&gt;
&lt;h2 id="when-htmx-fits-like-a-glove-and-when-it-doesnt"&gt;When htmx fits like a glove (and when it doesn’t)&lt;/h2&gt;
&lt;p&gt;Let’s be honest: htmx isn’t a universal replacement for all front-end development. It’s a different trade.&lt;/p&gt;
&lt;h3 id="great-fits"&gt;Great fits&lt;/h3&gt;
&lt;p&gt;htmx shines when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The UI is mostly server-driven content.&lt;/li&gt;
&lt;li&gt;CRUD flows dominate.&lt;/li&gt;
&lt;li&gt;Users expect forms, tables, filtering, pagination, and actions that map cleanly to HTTP.&lt;/li&gt;
&lt;li&gt;You can tolerate navigation that is mostly “page loads + partial updates.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A job board. A blog admin. A ticketing system. A content CMS. An internal tool. Most of the software you or your company actually needs.&lt;/p&gt;
&lt;h3 id="not-ideal-by-default"&gt;Not ideal (by default)&lt;/h3&gt;
&lt;p&gt;htmx will fight you if you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Highly interactive, real-time canvas-like experiences.&lt;/li&gt;
&lt;li&gt;Complex client-side validation logic that depends on large local state.&lt;/li&gt;
&lt;li&gt;Heavy offline-first behavior where the server is only a later reconciliation step.&lt;/li&gt;
&lt;li&gt;Ultra-granular animations tied tightly to high-frequency UI state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those cases, you can still use htmx as a companion—enhancing the “boring parts” while reserving a dedicated front-end for the parts that truly need it.&lt;/p&gt;
&lt;p&gt;My opinionated stance: start with server-rendered HTML and let interactivity grow only where it earns its complexity.&lt;/p&gt;
&lt;h2 id="how-the-architecture-changes-your-day-to-day"&gt;How the architecture changes your day-to-day&lt;/h2&gt;
&lt;p&gt;Here’s why I’m “joining” the htmx camp: it changes how development feels.&lt;/p&gt;
&lt;h3 id="1-debugging-becomes-straightforward"&gt;1) Debugging becomes straightforward&lt;/h3&gt;
&lt;p&gt;When something renders wrong, you can inspect the HTML you received. When an interaction fails, you can inspect the network request and the returned fragment.&lt;/p&gt;
&lt;p&gt;No component introspection. No “why did the state update twice?” no phantom re-renders. You can reason about behavior using tools you already know: request/response, DOM updates, and HTML output.&lt;/p&gt;
&lt;h3 id="2-iteration-loops-get-tighter"&gt;2) Iteration loops get tighter&lt;/h3&gt;
&lt;p&gt;Design your app like you would design pages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a route that renders a view.&lt;/li&gt;
&lt;li&gt;Add a small fragment endpoint for updates.&lt;/li&gt;
&lt;li&gt;Wire an attribute in the markup to swap it in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The feedback cycle shrinks because you’re not constantly building an app shell just to test a small UI behavior.&lt;/p&gt;
&lt;h3 id="3-performance-becomes-natural"&gt;3) Performance becomes natural&lt;/h3&gt;
&lt;p&gt;Server-rendered pages start with meaningful HTML. That reduces the “blank screen until hydration” problem that haunts many client-heavy architectures. Even when you use JavaScript sparingly, the baseline experience is already there.&lt;/p&gt;
&lt;p&gt;And because updates are targeted swaps, you often avoid re-fetching and re-rendering entire application states for what should be local changes.&lt;/p&gt;
&lt;h2 id="a-pragmatic-migration-strategy-start-small-win-early"&gt;A pragmatic migration strategy: start small, win early&lt;/h2&gt;
&lt;p&gt;If you’re currently deep in a React-first setup, you don’t need to rip everything out to benefit from the underlying idea: fewer client-state responsibilities.&lt;/p&gt;
&lt;p&gt;A migration strategy that works in real organizations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pick one CRUD feature&lt;/strong&gt; (e.g., “edit profile” or “manage tags”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implement it server-rendered&lt;/strong&gt; with standard HTML routes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add htmx enhancements&lt;/strong&gt; only where it improves flow (search-as-you-type, inline delete, partial refresh of a table).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the rest untouched&lt;/strong&gt; until you’re confident you like the model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure engineering outcomes&lt;/strong&gt;, not just UI metrics: time-to-change, regression frequency, and how often you fight state bugs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal isn’t purity. It’s reducing the complexity tax. If htmx makes a single area of your product easier to maintain, that’s already a win.&lt;/p&gt;
&lt;h2 id="conclusion-the-server-isnt-oldits-the-solid-foundation"&gt;Conclusion: the server isn’t “old”—it’s the solid foundation&lt;/h2&gt;
&lt;p&gt;htmx isn’t a rejection of modern web development; it’s a rejection of modern web development’s habits. It brings us back to a principle that scales: build reliable behavior around HTML and HTTP, and use the browser as a platform rather than as a rendering target for a separate UI model.&lt;/p&gt;
&lt;p&gt;For the vast majority of apps—especially CRUD and content-driven workflows—htmx offers a simpler architecture that’s easier to understand, easier to debug, and faster to ship. I’m joining because it feels like building software again, not managing a framework’s ecosystem.&lt;/p&gt;</content></item><item><title>Phoenix LiveView Is the Framework the Industry Refuses to Notice</title><link>https://decastro.work/blog/phoenix-liveview-framework-industry-refuses-notice/</link><pubDate>Fri, 17 Mar 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/phoenix-liveview-framework-industry-refuses-notice/</guid><description>&lt;p&gt;For years, the web development conversation has been hijacked by a single obsession: push as much logic as possible into the browser. Phoenix LiveView quietly does the opposite—render server-driven HTML that feels real-time, reactive, and alive—without demanding a dedicated client-side framework to babysit state. The industry has plenty of reasons to ignore that. But those reasons are rarely about capability.&lt;/p&gt;
&lt;h2 id="the-promise-real-time-interactivity-without-the-javascript-tax"&gt;The promise: real-time interactivity without the JavaScript tax&lt;/h2&gt;
&lt;p&gt;LiveView’s core idea is disarmingly simple: you write Elixir on the server, and the UI updates over a persistent connection. Instead of a client-side app that owns the state, LiveView owns the interaction model on the server, and the browser becomes a “thin” endpoint that renders HTML updates as they arrive.&lt;/p&gt;</description><content>&lt;p&gt;For years, the web development conversation has been hijacked by a single obsession: push as much logic as possible into the browser. Phoenix LiveView quietly does the opposite—render server-driven HTML that feels real-time, reactive, and alive—without demanding a dedicated client-side framework to babysit state. The industry has plenty of reasons to ignore that. But those reasons are rarely about capability.&lt;/p&gt;
&lt;h2 id="the-promise-real-time-interactivity-without-the-javascript-tax"&gt;The promise: real-time interactivity without the JavaScript tax&lt;/h2&gt;
&lt;p&gt;LiveView’s core idea is disarmingly simple: you write Elixir on the server, and the UI updates over a persistent connection. Instead of a client-side app that owns the state, LiveView owns the interaction model on the server, and the browser becomes a “thin” endpoint that renders HTML updates as they arrive.&lt;/p&gt;
&lt;p&gt;That sounds like a throwback until you actually build something interactive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A live search box that updates results as you type.&lt;/li&gt;
&lt;li&gt;A dashboard with filters, tabs, and paginated tables that update instantly.&lt;/li&gt;
&lt;li&gt;A collaborative workflow where actions by one user trigger changes for others.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In LiveView, those behaviors aren’t bolted on with hacks. They’re part of the programming model. You don’t need to design a whole state management architecture before you can show a single component. You model events and render the result. That’s it.&lt;/p&gt;
&lt;p&gt;And crucially: you typically don’t need to introduce a large client-side JavaScript framework to get there. You can still use JavaScript where it’s genuinely needed (file uploads, complex third-party widgets, browser APIs), but you’re not forced into treating the front end as a separate application you must synchronize forever.&lt;/p&gt;
&lt;h2 id="why-react-feels-over-engineered-once-youve-shipped-liveview"&gt;Why React “feels” over-engineered once you’ve shipped LiveView&lt;/h2&gt;
&lt;p&gt;React is an extraordinary library. The problem is the ecosystem you end up building around it.&lt;/p&gt;
&lt;p&gt;React apps often become a collection of moving parts: a router, a data-fetching layer, a cache, a state manager, form handling utilities, global stores, local component state, and a growing set of conventions that exist largely to stop the app from collapsing under its own complexity. Even when you get it right, you’re still paying ongoing costs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You implement optimistic updates to keep the UI responsive.&lt;/li&gt;
&lt;li&gt;You fight edge cases around stale caches.&lt;/li&gt;
&lt;li&gt;You reconcile server state with local intent.&lt;/li&gt;
&lt;li&gt;You debug race conditions between concurrent effects and network latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LiveView attacks the problem at the root: it keeps the “truth” of interaction on the server. When the user clicks a button or submits a form, that event is handled by your Elixir code. The response is a render update—HTML patches that update what the user sees.&lt;/p&gt;
&lt;p&gt;Here’s the practical difference in mindset:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;React-style framing:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“What does the UI state become? Who owns it? How do we keep it consistent with the server?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;LiveView-style framing:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“What should the UI look like given the current state? What happens on each event?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once you’ve internalized that difference, React can start to feel like over-engineering for common product workflows—especially internal tools, admin dashboards, CRUD-heavy apps, and interactive pages where the business logic belongs on the backend anyway.&lt;/p&gt;
&lt;h2 id="the-developer-experience-write-elixir-not-a-second-job"&gt;The developer experience: write Elixir, not a second job&lt;/h2&gt;
&lt;p&gt;The strongest argument for LiveView isn’t theoretical; it’s ergonomic.&lt;/p&gt;
&lt;p&gt;In many web stacks, you end up doing “two jobs”:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build the server correctly.&lt;/li&gt;
&lt;li&gt;Then replicate parts of your server logic in the browser so the UI can stay responsive.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With LiveView, you usually don’t have to. You can keep business logic where it belongs—on the server—then render UI directly from that same logic. Forms, validations, and authorization checks happen in one place. That means fewer “mirrors” of logic, fewer mismatches, and fewer times you debug why the UI thinks something is allowed when the server denies it.&lt;/p&gt;
&lt;p&gt;Consider a common scenario: a settings page with dependent fields.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Changing “Region” changes available “Language” options.&lt;/li&gt;
&lt;li&gt;Certain combinations disable other fields.&lt;/li&gt;
&lt;li&gt;Submitting updates requires validating permissions and rejecting conflicting states.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a React app, you’ll often build a mini client-side system to manage this dependency graph. In LiveView, you handle the change event, update the server-side state, and re-render the affected section of the UI. The result is immediate, consistent, and—most importantly—maintainable by the same team that wrote the backend.&lt;/p&gt;
&lt;p&gt;If you want a litmus test: ask yourself how often your React UI logic duplicates server logic. If it’s frequent, LiveView will feel like relief rather than novelty.&lt;/p&gt;
&lt;h2 id="state-management-nightmare-is-usually-just-state-management-avoidance"&gt;“State management nightmare” is usually just state management avoidance&lt;/h2&gt;
&lt;p&gt;The phrase “state management nightmare” gets tossed around like a cliché, but it captures a real failure mode: many front-end architectures treat state as something you constantly patch rather than something you design.&lt;/p&gt;
&lt;p&gt;LiveView flips that. You structure your application around server-side events and renders. If you need to track UI-specific ephemeral state—like which tab is selected or whether a modal is open—LiveView lets you do it without turning your codebase into a distributed state experiment.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine a multi-step onboarding wizard.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 1 collects company details.&lt;/li&gt;
&lt;li&gt;Step 2 collects team preferences.&lt;/li&gt;
&lt;li&gt;Step 3 shows a summary and confirmation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In React, you’re likely to maintain form state in the browser, serialize it, validate it, and coordinate it across routes or components. You might keep it in a form library, a global store, or both.&lt;/p&gt;
&lt;p&gt;In LiveView, you can keep the “wizard state” server-side. Each step transition triggers an event; you validate and update state; you render the next step. You can still optimize user experience (e.g., debounce specific inputs), but you don’t have to build an entire client-side data layer to get correctness.&lt;/p&gt;
&lt;p&gt;This is where LiveView’s philosophy shines: it doesn’t deny interactivity—it centralizes responsibility.&lt;/p&gt;
&lt;h2 id="practical-advice-how-to-adopt-liveview-without-clinging-to-old-habits"&gt;Practical advice: how to adopt LiveView without clinging to old habits&lt;/h2&gt;
&lt;p&gt;If you’re coming from the React/Next.js world, the temptation will be to “port” your existing patterns instead of learning new ones. Don’t.&lt;/p&gt;
&lt;p&gt;Here are a few approaches that work well in practice:&lt;/p&gt;
&lt;h3 id="treat-liveview-as-your-ui-state-machine"&gt;Treat LiveView as your UI state machine&lt;/h3&gt;
&lt;p&gt;Model the app around events and render outcomes. When you name things—events, assigns, components—opt for business semantics, not UI mechanics. “OrderPlaced” beats “ClickedPrimaryButton.”&lt;/p&gt;
&lt;h3 id="keep-components-small-and-composable"&gt;Keep components small and composable&lt;/h3&gt;
&lt;p&gt;Break down your templates into LiveView components where it helps readability. Aim for components that own a clear set of responsibilities: a single form, a table with filters, a modal with its own logic.&lt;/p&gt;
&lt;h3 id="use-javascript-only-when-the-browser-genuinely-adds-value"&gt;Use JavaScript only when the browser genuinely adds value&lt;/h3&gt;
&lt;p&gt;If you’re building a date picker or integrating with a third-party widget, fine—use JavaScript. But don’t start every project by assuming the client must own the architecture. Let LiveView do what it does best: interactive HTML rendering driven by server events.&lt;/p&gt;
&lt;h3 id="dont-bolt-on-complexity"&gt;Don’t bolt on complexity&lt;/h3&gt;
&lt;p&gt;A common trap is trying to recreate SPA patterns: global stores everywhere, client-side caching strategies, “optimistic” behaviors that mirror server writes. LiveView encourages a more direct path: handle the event, update server state, re-render.&lt;/p&gt;
&lt;p&gt;Adopting LiveView successfully often means unlearning the reflex to “keep everything in sync on the client.” LiveView was built to make that sync problem smaller by design.&lt;/p&gt;
&lt;h2 id="why-the-industry-ignores-itand-why-thats-irrational"&gt;Why the industry ignores it—and why that’s irrational&lt;/h2&gt;
&lt;p&gt;Let’s be blunt: Phoenix LiveView is powerful, and it’s not new. Yet it doesn’t dominate mindshare the way JavaScript frameworks do. That gap has less to do with technical capability and more to do with how ecosystems grow.&lt;/p&gt;
&lt;p&gt;JavaScript is the default language of the web, so the ecosystem gravity is enormous. Teams hire for it. Tutorials lead with it. Tooling assumes it. And once a workflow becomes standard, it acquires a kind of institutional momentum that has nothing to do with whether it’s the best tool for the job.&lt;/p&gt;
&lt;p&gt;But “standard” is not the same as “effective.”&lt;/p&gt;
&lt;p&gt;LiveView offers a different trade: instead of treating the browser as the primary place where truth lives, it treats the server as the source of interaction truth. You get reactive behavior without the constant churn of front-end architectural overhead. The user experience feels modern because the UI updates immediately; the implementation feels sane because the state model stays coherent.&lt;/p&gt;
&lt;p&gt;If the industry wants to keep repeating the same patterns—global state, optimistic UI, reconciliation strategies, and the endless ceremony around correctness—React will keep looking like the safe choice. LiveView just quietly proves it isn’t the only one.&lt;/p&gt;
&lt;h2 id="conclusion-a-framework-that-treats-interactivity-like-engineering-not-theater"&gt;Conclusion: a framework that treats interactivity like engineering, not theater&lt;/h2&gt;
&lt;p&gt;Phoenix LiveView doesn’t ask you to surrender interactivity for simplicity. It gives you real-time UX through server-driven, reactive rendering—so your codebase remains unified and your UI stays correct without orchestrating a complicated client-side state universe. If your current stack feels like it’s always one refactor away from stability, LiveView is worth a serious look.&lt;/p&gt;
&lt;p&gt;Not because it’s trendy. Because it works—cleanly, confidently, and with far less drama than the industry’s JavaScript-first reflex will admit.&lt;/p&gt;</content></item><item><title>How I Use ChatGPT as a Senior Developer (Without Losing My Mind)</title><link>https://decastro.work/blog/how-i-use-chatgpt-senior-developer/</link><pubDate>Sun, 05 Mar 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/how-i-use-chatgpt-senior-developer/</guid><description>&lt;p&gt;I didn’t “learn ChatGPT” so much as I built a workflow around it—one that treats the model like a sharp junior teammate, not an oracle. After months of daily use, the pattern is clear: when I’m exploring, translating, or brainstorming, ChatGPT accelerates me. When I’m deciding, securing, or computing precisely, it can quietly steer me off a cliff. The difference is knowing which prompt category I’m in before I hit send.&lt;/p&gt;</description><content>&lt;p&gt;I didn’t “learn ChatGPT” so much as I built a workflow around it—one that treats the model like a sharp junior teammate, not an oracle. After months of daily use, the pattern is clear: when I’m exploring, translating, or brainstorming, ChatGPT accelerates me. When I’m deciding, securing, or computing precisely, it can quietly steer me off a cliff. The difference is knowing which prompt category I’m in before I hit send.&lt;/p&gt;
&lt;h2 id="my-rule-of-thumb-classify-the-task-before-you-prompt"&gt;My rule of thumb: classify the task before you prompt&lt;/h2&gt;
&lt;p&gt;Here’s the gating question I ask myself every time: &lt;em&gt;Is this something I can safely iterate on, or something I must get exactly right?&lt;/em&gt; That single decision determines what I paste, what I ask for, and how I verify the output.&lt;/p&gt;
&lt;p&gt;I split tasks into four buckets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Explaining and mapping&lt;/strong&gt; (safe): unfamiliar codebases, unclear docs, “walk me through this.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translating requirements&lt;/strong&gt; (mostly safe): turn a spec into test ideas, edge cases, or example scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging and reasoning support&lt;/strong&gt; (safe with discipline): rubber-duck style for complex logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deciding and delivering precision&lt;/strong&gt; (danger zone): architecture, security-sensitive code, exact numerical reasoning, or anything tied to specific API versions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do nothing else, do this: choose your bucket and tailor your prompt to match the risk. ChatGPT isn’t the risk—it’s the mismatch between the kind of certainty you need and the kind of certainty the model is designed to provide.&lt;/p&gt;
&lt;h2 id="the-sweet-spot-1-explaining-unfamiliar-codebases"&gt;The sweet spot #1: explaining unfamiliar codebases&lt;/h2&gt;
&lt;p&gt;The fastest way I get value is by treating ChatGPT like a code archaeologist. I paste a small slice—usually 50–150 lines around the behavior I don’t understand—and ask it to build a mental model.&lt;/p&gt;
&lt;p&gt;My go-to prompt pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Give context first:&lt;/strong&gt; language, framework, and what behavior you’re trying to understand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrain the scope:&lt;/strong&gt; “Explain only this file/function and its call chain.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask for a trace:&lt;/strong&gt; “What happens step-by-step when X occurs?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are reviewing a JavaScript/TypeScript service. Here’s a function &lt;code&gt;handleRequest(req)&lt;/code&gt; and the helper it calls.&lt;br&gt;
Explain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the data flow from &lt;code&gt;req&lt;/code&gt; to the final response,&lt;/li&gt;
&lt;li&gt;where validation happens, and&lt;/li&gt;
&lt;li&gt;which branch handles error conditions.&lt;br&gt;
Don’t suggest refactors yet—just narrate the current behavior.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;What I look for in the answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It should mention the real variables and branches I provided.&lt;/li&gt;
&lt;li&gt;It should identify what’s &lt;em&gt;missing&lt;/em&gt; (e.g., “This function assumes &lt;code&gt;userId&lt;/code&gt; is present but I don’t see the check here.”).&lt;/li&gt;
&lt;li&gt;It should surface questions I can answer with additional code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where this wins: the first pass is often enough to guide my next search in the repo. ChatGPT turns “Where do I even start?” into “Here are the three files and conditions I should inspect.”&lt;/p&gt;
&lt;h2 id="the-sweet-spot-2-generating-test-cases-from-requirements"&gt;The sweet spot #2: generating test cases from requirements&lt;/h2&gt;
&lt;p&gt;I use ChatGPT to turn vague product language into testable expectations. This is not “generate the tests and ship.” It’s “generate the &lt;em&gt;candidates&lt;/em&gt; and make them real.”&lt;/p&gt;
&lt;p&gt;My prompt pattern for test generation is brutally structured:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Paste the requirement (or user story).&lt;/li&gt;
&lt;li&gt;Provide known constraints (e.g., time zones, input formats, limits).&lt;/li&gt;
&lt;li&gt;Ask for &lt;strong&gt;equivalence classes + boundary cases + negative cases&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Request tests in a specific format (table-driven, Jest, pytest, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We have an endpoint &lt;code&gt;POST /v1/events&lt;/code&gt;. Requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eventType&lt;/code&gt; is required and must be one of: &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;view&lt;/code&gt;, &lt;code&gt;purchase&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userId&lt;/code&gt; is optional; if omitted, store as anonymous&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;/code&gt; must be ISO-8601 in UTC (e.g., &lt;code&gt;2026-03-19T12:34:56Z&lt;/code&gt;)&lt;br&gt;
Generate a test plan with:&lt;/li&gt;
&lt;li&gt;5 happy-path tests&lt;/li&gt;
&lt;li&gt;6 validation tests&lt;/li&gt;
&lt;li&gt;4 edge/boundary tests&lt;/li&gt;
&lt;li&gt;3 security-ish misuse tests (e.g., wrong content-type, oversized payload)&lt;br&gt;
Then output pseudocode tests in Jest-style.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;What makes this effective:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I don’t ask for “the correct behavior”—I ask for “a test plan.”&lt;/li&gt;
&lt;li&gt;I insist on format and constraints so the model can’t drift into generic advice.&lt;/li&gt;
&lt;li&gt;I always follow up with: “What assumptions did you make that I should verify in code?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My verification step is mechanical. I pick one or two tests to run immediately and use the rest to guide code reading. Test generation becomes a map, not the territory.&lt;/p&gt;
&lt;h2 id="the-sweet-spot-3-rubber-duck-debugging-complex-logic"&gt;The sweet spot #3: rubber-duck debugging complex logic&lt;/h2&gt;
&lt;p&gt;When logic gets gnarly—distributed edge cases, state machines, off-by-one territory—ChatGPT shines as a rubber duck &lt;em&gt;that can keep up&lt;/em&gt;. The trick is to force it to ask questions and reflect its reasoning back at you.&lt;/p&gt;
&lt;p&gt;My prompt pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide the problematic function (or simplified version).&lt;/li&gt;
&lt;li&gt;Tell it what you already believe is happening.&lt;/li&gt;
&lt;li&gt;Ask it to enumerate hypotheses and contradictions.&lt;/li&gt;
&lt;li&gt;Require it to point to what would falsify each hypothesis.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here’s a simplified function that calculates a rolling window score.&lt;br&gt;
Current behavior: when the window crosses midnight, scores sometimes reset unexpectedly.&lt;br&gt;
Assume I might be misunderstanding the timestamp conversions.&lt;br&gt;
Do this debugging:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Restate the algorithm in plain English&lt;/li&gt;
&lt;li&gt;List the branches that behave differently near midnight&lt;/li&gt;
&lt;li&gt;Identify 3 plausible bugs&lt;/li&gt;
&lt;li&gt;For each bug, tell me what input would disprove it and why&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;The output I want isn’t a verdict; it’s a menu of targeted experiments. Once I have that, I can instrument the code or write a focused failing test. ChatGPT accelerates the “what should I check next?” loop.&lt;/p&gt;
&lt;h2 id="the-sweet-spot-4-writing-regex-patterns-that-actually-match"&gt;The sweet spot #4: writing regex patterns that actually match&lt;/h2&gt;
&lt;p&gt;Regex is where I’m willing to delegate “construction,” because it’s easy to test. If you can run it against representative inputs, you can validate quickly.&lt;/p&gt;
&lt;p&gt;My prompt pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide the goal in examples (“matches these, rejects those”).&lt;/li&gt;
&lt;li&gt;Specify the regex flavor (JavaScript, PCRE, RE2, etc.).&lt;/li&gt;
&lt;li&gt;Ask for anchored vs unanchored variants depending on use case.&lt;/li&gt;
&lt;li&gt;Request explanation of groups and boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write a JavaScript regex that matches filenames like &lt;code&gt;invoice_2026-03-19_v3.pdf&lt;/code&gt;&lt;br&gt;
Requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Must start at beginning and end at &lt;code&gt;.pdf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Date is &lt;code&gt;YYYY-MM-DD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Version is &lt;code&gt;v&lt;/code&gt; followed by an integer (no decimals)&lt;br&gt;
Return:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;the regex,&lt;/li&gt;
&lt;li&gt;a list of example strings it matches, and&lt;/li&gt;
&lt;li&gt;a list it must reject.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;The “must reject” list is the real win—it exposes the model’s tendency to be overly permissive. I then run the regex against those examples, and only then do I integrate.&lt;/p&gt;
&lt;h2 id="the-failure-modes-i-refuse-to-delegate"&gt;The failure modes I refuse to delegate&lt;/h2&gt;
&lt;p&gt;Here’s where I draw hard lines. These are categories where ChatGPT can be persuasive in the wrong way—producing plausible code or explanations that fail under constraints you didn’t explicitly enforce.&lt;/p&gt;
&lt;h3 id="1-architecture-decisions"&gt;1) Architecture decisions&lt;/h3&gt;
&lt;p&gt;ChatGPT can propose elegant structures that don’t fit your system’s operational reality: deployment model, data ownership, migration strategy, team workflow. I use it for diagramming and options, not choosing.&lt;/p&gt;
&lt;p&gt;What I do instead: ask for &lt;strong&gt;trade-offs&lt;/strong&gt;, then make the decision myself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“List three architecture options and the operational implications of each.”&lt;/li&gt;
&lt;li&gt;“What questions should I ask my team to choose?”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-security-sensitive-code"&gt;2) Security-sensitive code&lt;/h3&gt;
&lt;p&gt;Anything involving auth, authorization, cryptography, input sanitization, permission boundaries, or threat modeling is not a “paste and hope” zone. Even if the code “works,” it can be subtly wrong.&lt;/p&gt;
&lt;p&gt;What I do instead: use ChatGPT to produce a checklist:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Give me a security review checklist for OAuth token validation in Node.js.”&lt;/li&gt;
&lt;li&gt;Then I implement with known, reviewed patterns and run the full test suite.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-precise-numerical-reasoning"&gt;3) Precise numerical reasoning&lt;/h3&gt;
&lt;p&gt;If the task hinges on exact math, floating-point behavior, financial rounding rules, or tricky time arithmetic, I treat the model as a brainstorming partner at best. “Looks right” isn’t good enough.&lt;/p&gt;
&lt;p&gt;What I do instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;derive formulas myself,&lt;/li&gt;
&lt;li&gt;write a few canonical tests with known inputs/outputs,&lt;/li&gt;
&lt;li&gt;and then use ChatGPT to help explain discrepancies rather than compute final truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-code-tied-to-specific-api-versions"&gt;4) Code tied to specific API versions&lt;/h3&gt;
&lt;p&gt;Models often generalize: an SDK method changed, a parameter was renamed, an error response structure differs. If your code must match a particular API version, you need deterministic references.&lt;/p&gt;
&lt;p&gt;What I do instead: include the exact version and relevant docs snippet in the prompt, and ask ChatGPT to &lt;strong&gt;only&lt;/strong&gt; transform what you gave it—never invent.&lt;/p&gt;
&lt;h2 id="red-flags-in-outputs-when-i-stop-trusting-it"&gt;Red flags in outputs: when I stop trusting it&lt;/h2&gt;
&lt;p&gt;Even in safe categories, I watch for signals that the model is making stuff up or smoothing over uncertainty:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Confident language with missing assumptions:&lt;/strong&gt; “It will work” without referencing your constraints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invented file names or symbols:&lt;/strong&gt; if it claims there’s a helper I didn’t paste, that’s a no-go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unverifiable leaps:&lt;/strong&gt; “This is the algorithm” when the reasoning depends on something not present in your code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Off-by-default behavior:&lt;/strong&gt; regex or parsing outputs that lack anchors, flags, or boundary conditions you didn’t request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Citations to nonexistent docs:&lt;/strong&gt; if it references a behavior “from the docs” but you didn’t provide the docs, treat it as suspect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When I see these, I don’t argue with it—I shrink the prompt. More context. Narrow scope. Ask it to point to exact lines. Or ask it to produce questions I can answer with additional code.&lt;/p&gt;
&lt;h2 id="conclusion-use-chatgpt-like-a-tool-with-guardrails"&gt;Conclusion: use ChatGPT like a tool with guardrails&lt;/h2&gt;
&lt;p&gt;ChatGPT became genuinely useful for me once I stopped trying to make it “smart” and instead made it &lt;em&gt;consistent&lt;/em&gt;. In the best cases—explaining unfamiliar code, generating test candidates, rubber-duck debugging, and crafting regex—it speeds up my feedback loops. In the worst cases—architecture, security-sensitive code, precise numerical reasoning, and version-specific integrations—it can sound right while being dangerously wrong.&lt;/p&gt;
&lt;p&gt;So my final advice is simple: classify the task, constrain the scope, and verify every output that must be exact. If you do that, you’ll get faster without losing your mind—and without shipping the model’s confidence as if it were proof.&lt;/p&gt;</content></item><item><title>Writing CLI Tools in 2023: Rust, Go, or TypeScript?</title><link>https://decastro.work/blog/writing-cli-tools-2023-rust-go-typescript/</link><pubDate>Tue, 21 Feb 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/writing-cli-tools-2023-rust-go-typescript/</guid><description>&lt;p&gt;If you’ve ever shipped a CLI and then watched it break because someone’s environment differed from yours, you already know the real problem isn’t the command—it’s the tooling choices that sit underneath it. In 2023, you don’t have to settle for brittle scripts anymore. You can build modern, ergonomic CLI tools that feel like first-class software—only now you get to choose the language that best fits the job.&lt;/p&gt;
&lt;p&gt;The “right” answer is less about what’s trendy and more about what your users will do with your tool: how fast it needs to be, how many dependencies it should carry, and whether your team lives in Node or prefers a compile-and-ship workflow.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever shipped a CLI and then watched it break because someone’s environment differed from yours, you already know the real problem isn’t the command—it’s the tooling choices that sit underneath it. In 2023, you don’t have to settle for brittle scripts anymore. You can build modern, ergonomic CLI tools that feel like first-class software—only now you get to choose the language that best fits the job.&lt;/p&gt;
&lt;p&gt;The “right” answer is less about what’s trendy and more about what your users will do with your tool: how fast it needs to be, how many dependencies it should carry, and whether your team lives in Node or prefers a compile-and-ship workflow.&lt;/p&gt;
&lt;h2 id="the-modern-cli-reality-youre-shipping-behavior-not-just-code"&gt;The modern CLI reality: you’re shipping behavior, not just code&lt;/h2&gt;
&lt;p&gt;A CLI tool is a contract. It promises:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predictable flags and help text&lt;/li&gt;
&lt;li&gt;Stable parsing across shells and environments&lt;/li&gt;
&lt;li&gt;Good error messages (with non-zero exit codes)&lt;/li&gt;
&lt;li&gt;A deployment story that won’t collapse on someone else’s machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In earlier years, “good enough” often meant Python with &lt;code&gt;argparse&lt;/code&gt;, or bash scripts that stitched together other programs. Those approaches can still work—but they tend to fail in the places teams care about most: dependency drift, version mismatches, packaging pain, and inconsistent argument parsing.&lt;/p&gt;
&lt;p&gt;So let’s talk about three languages that make those problems dramatically easier in 2023: Go (with Cobra), Rust (with Clap), and TypeScript (with Commander).&lt;/p&gt;
&lt;h2 id="go-pick-it-when-you-want-single-binary-simplicity-and-fast-iteration"&gt;Go: pick it when you want single-binary simplicity and fast iteration&lt;/h2&gt;
&lt;p&gt;Go’s superpower for CLI tools is straightforward: you can usually build a static-ish, single-binary executable quickly, with a deployment story that’s easy to explain. That matters when your users just want a command—no runtime setup, no “please install Node 18” footnotes, no container requirements.&lt;/p&gt;
&lt;h3 id="why-cobra-fits-gos-strengths"&gt;Why Cobra fits Go’s strengths&lt;/h3&gt;
&lt;p&gt;Cobra is a mature library with a familiar structure for defining commands and subcommands. It encourages you to model your CLI like a small app, not a pile of string parsing. Flags, help output, and command composition become “boring” in the best way.&lt;/p&gt;
&lt;h3 id="practical-example-a-workspace-cleanup-utility"&gt;Practical example: a workspace cleanup utility&lt;/h3&gt;
&lt;p&gt;Imagine a CLI like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tool purge --dry-run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool purge --days 30&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool purge --path ./dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tool config set region us-east-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Cobra, you can define subcommands cleanly and keep option wiring readable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One command file per major subcommand&lt;/li&gt;
&lt;li&gt;Central config handling&lt;/li&gt;
&lt;li&gt;Consistent exit codes and messaging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The real win is that Go lets you ship a utility that’s easy to install and fast to run—exactly what you want for operational scripts and developer productivity tools.&lt;/p&gt;
&lt;h3 id="when-go-is-the-wrong-choice"&gt;When Go is the wrong choice&lt;/h3&gt;
&lt;p&gt;Go isn’t ideal when your CLI is fundamentally a data-processing engine where you need tight control over memory layout or you’re squeezing every millisecond out of streaming. Go can handle that work, but if performance and predictable resource usage at scale are the whole point, Rust usually becomes the sharper tool.&lt;/p&gt;
&lt;h2 id="rust-pick-it-when-correctness-performance-and-large-data-are-the-product"&gt;Rust: pick it when correctness, performance, and large data are the product&lt;/h2&gt;
&lt;p&gt;Rust is what you reach for when “CLI tool” is really shorthand for “high-throughput system.” Think: parsing logs at scale, transforming large datasets, indexing files, running ETL-like pipelines, or any workload where speed and resource efficiency are non-negotiable.&lt;/p&gt;
&lt;h3 id="why-clap-fits-rusts-ergonomics"&gt;Why Clap fits Rust’s ergonomics&lt;/h3&gt;
&lt;p&gt;Clap is the de facto standard for CLI argument parsing in Rust. It gives you powerful declarative configuration for options, subcommands, and validation. You also get help output that feels polished without you spending hours polishing it.&lt;/p&gt;
&lt;p&gt;The bigger point: Rust makes it easier to write a CLI whose failure modes are clean. When things go wrong, you can surface precise errors instead of dumping stack traces or swallowing edge cases.&lt;/p&gt;
&lt;h3 id="practical-example-streaming-log-processing"&gt;Practical example: streaming log processing&lt;/h3&gt;
&lt;p&gt;Consider a tool like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;logx grep --pattern &amp;quot;ERROR&amp;quot; --input ./logs --output ./report.jsonl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;logx stats --group-by service --input ./logs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need to stream input efficiently, avoid loading everything into memory, and process files concurrently, Rust shines. You can build a pipeline where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input is read incrementally&lt;/li&gt;
&lt;li&gt;Parsing is robust (with clear errors for malformed lines)&lt;/li&gt;
&lt;li&gt;Output is written in a controlled format&lt;/li&gt;
&lt;li&gt;Concurrency is explicit and safe&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also where Rust tends to pay off in long-lived tools: memory safety and strong typing reduce “it worked on my machine” bugs that only surface under real workloads.&lt;/p&gt;
&lt;h3 id="when-rust-is-the-wrong-choice"&gt;When Rust is the wrong choice&lt;/h3&gt;
&lt;p&gt;Rust can be overkill when the CLI is mainly glue—wrapping a few subprocess calls, moving files around, or orchestrating existing tools. If your tool’s primary value is workflow convenience and you don’t need heavy lifting, Go will usually get you to a shippable result faster.&lt;/p&gt;
&lt;h2 id="typescript-pick-it-when-your-users-are-already-in-the-node-ecosystem"&gt;TypeScript: pick it when your users are already in the Node ecosystem&lt;/h2&gt;
&lt;p&gt;TypeScript makes sense when your CLI is part of a larger ecosystem that already runs on Node: dev tools, integrations, build tooling, plugin systems, and anything that benefits from npm’s distribution model.&lt;/p&gt;
&lt;h3 id="why-commander-fits-typescript-well"&gt;Why Commander fits TypeScript well&lt;/h3&gt;
&lt;p&gt;Commander is a popular choice for defining commands and options, with a pragmatic programming model that works well for JS/TS teams. You’ll find it especially natural to build a CLI that talks to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;npm packages&lt;/li&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;local project configuration files (like &lt;code&gt;package.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;JavaScript-based tooling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TypeScript also lets you share types between the CLI and the underlying libraries. That can be a big quality-of-life improvement: you don’t just parse flags—you generate strongly typed config objects that downstream code understands.&lt;/p&gt;
&lt;h3 id="practical-example-a-project-scaffolding-cli"&gt;Practical example: a project scaffolding CLI&lt;/h3&gt;
&lt;p&gt;Suppose you’re building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mygen init --template react --name acme-app&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mygen install --with eslint --with prettier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mygen doctor&lt;/code&gt; to validate the project setup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a Node-centric workflow, TypeScript shines because the CLI and the libraries live in the same universe. Installation might even be as simple as &lt;code&gt;npx mygen&lt;/code&gt; or &lt;code&gt;npm i -g mygen&lt;/code&gt; depending on your distribution goals.&lt;/p&gt;
&lt;h3 id="when-typescript-is-the-wrong-choice"&gt;When TypeScript is the wrong choice&lt;/h3&gt;
&lt;p&gt;TypeScript is less compelling when you want the simplest “drop a single binary on a server” experience or when your CLI must be standalone with minimal runtime concerns. If users expect &lt;code&gt;curl | bash&lt;/code&gt;-style installation and zero runtime assumptions, Rust or Go tends to be the smoother experience.&lt;/p&gt;
&lt;p&gt;Also, if your tool is heavy on raw data processing, TypeScript will likely feel frictional compared to Rust or even Go.&lt;/p&gt;
&lt;h2 id="what-about-python-dont-resurrect-the-original-pain"&gt;What about Python? Don’t resurrect the original pain&lt;/h2&gt;
&lt;p&gt;Python still exists—and some teams genuinely like it. But for a CLI that must be reliable across machines and time, Python scripts are where many projects go when they didn’t plan for operational longevity.&lt;/p&gt;
&lt;p&gt;The failure pattern is predictable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Someone has Python 3.10, someone else has 3.11 (or none)&lt;/li&gt;
&lt;li&gt;Dependencies behave differently across minor versions&lt;/li&gt;
&lt;li&gt;A packaging mistake turns “works locally” into “it fails on CI”&lt;/li&gt;
&lt;li&gt;Argument parsing logic becomes inconsistent and hard to test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Python is your starting point, you’ll want disciplined packaging and pinned dependencies. But if your goal is to ship a CLI that behaves like infrastructure—not a quick personal script—Rust, Go, or TypeScript are usually the more dependable choices.&lt;/p&gt;
&lt;h2 id="a-decision-framework-you-can-actually-use"&gt;A decision framework you can actually use&lt;/h2&gt;
&lt;p&gt;Here’s a blunt but useful way to choose:&lt;/p&gt;
&lt;h3 id="choose-go--cobra-if"&gt;Choose Go + Cobra if…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You want a single-binary developer utility&lt;/li&gt;
&lt;li&gt;Your CLI is primarily orchestration or lightweight processing&lt;/li&gt;
&lt;li&gt;You value fast compilation, straightforward deployment, and quick iteration&lt;/li&gt;
&lt;li&gt;Your team prefers minimal runtime dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if your users just need &lt;code&gt;tool do-thing&lt;/code&gt; to work everywhere, Go is your best bet.&lt;/p&gt;
&lt;h3 id="choose-rust--clap-if"&gt;Choose Rust + Clap if…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The tool must process large datasets efficiently&lt;/li&gt;
&lt;li&gt;Performance and predictable resource usage matter&lt;/li&gt;
&lt;li&gt;You care deeply about correctness, validation, and clean failure modes&lt;/li&gt;
&lt;li&gt;You’re building something closer to a real system than a wrapper&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if the CLI is the engine, Rust is usually the engine room.&lt;/p&gt;
&lt;h3 id="choose-typescript--commander-if"&gt;Choose TypeScript + Commander if…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Your users are already Node/TypeScript developers&lt;/li&gt;
&lt;li&gt;The CLI integrates with npm packages, project configs, or web APIs&lt;/li&gt;
&lt;li&gt;You want to share types and reuse libraries across CLI + runtime&lt;/li&gt;
&lt;li&gt;Distribution via npm is part of your product strategy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; if the CLI is part of a JavaScript workflow, TypeScript will feel native.&lt;/p&gt;
&lt;h2 id="make-your-cli-feel-professional-regardless-of-language"&gt;Make your CLI feel “professional” regardless of language&lt;/h2&gt;
&lt;p&gt;Language matters, but you can still raise the baseline quality of your CLI quickly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treat help output as product.&lt;/strong&gt; Add examples to &lt;code&gt;--help&lt;/code&gt; and ensure flags are consistent across subcommands.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate early.&lt;/strong&gt; Don’t wait until halfway through processing to discover a bad flag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be explicit in errors.&lt;/strong&gt; Print actionable messages and exit with non-zero codes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Support &lt;code&gt;--dry-run&lt;/code&gt; when the CLI modifies things.&lt;/strong&gt; It builds trust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep output stable.&lt;/strong&gt; If you offer machine-readable formats (JSON, NDJSON), document them and don’t casually break them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also: design your commands before your implementation. If your CLI architecture is clear, your library choice becomes an implementation detail rather than a regret.&lt;/p&gt;
&lt;h2 id="conclusion-the-best-language-is-the-one-that-matches-your-users"&gt;Conclusion: the best language is the one that matches your users&lt;/h2&gt;
&lt;p&gt;Go, Rust, and TypeScript are all excellent in 2023—they just optimize for different realities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Go + Cobra&lt;/strong&gt; for quick utilities and a simple install story.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rust + Clap&lt;/strong&gt; for performance-critical tooling and large-scale processing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript + Commander&lt;/strong&gt; for Node-first ecosystems and integration-heavy CLIs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose the one that matches how your users live. If you do that, you’ll spend less time firefighting environment mismatches and more time building a CLI people actually enjoy using.&lt;/p&gt;</content></item><item><title>AI Coding Assistants Are Making Senior Developers More Valuable, Not Less</title><link>https://decastro.work/blog/ai-coding-assistants-senior-developers-more-valuable/</link><pubDate>Thu, 09 Feb 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/ai-coding-assistants-senior-developers-more-valuable/</guid><description>&lt;p&gt;Every few months, a new wave of automation arrives with the same promise: “Don’t worry—your job is going away.” AI coding assistants are no exception. But the lived reality is harder to dismiss. When you put tools like Copilot and ChatGPT into the hands of real engineers, especially the senior ones, the story flips: expertise doesn’t get erased—it gets leveraged. And the disruption isn’t “AI replaces programmers.” It’s that the difference between good and mediocre developers is about to become unmistakable.&lt;/p&gt;</description><content>&lt;p&gt;Every few months, a new wave of automation arrives with the same promise: “Don’t worry—your job is going away.” AI coding assistants are no exception. But the lived reality is harder to dismiss. When you put tools like Copilot and ChatGPT into the hands of real engineers, especially the senior ones, the story flips: expertise doesn’t get erased—it gets leveraged. And the disruption isn’t “AI replaces programmers.” It’s that the difference between good and mediocre developers is about to become unmistakable.&lt;/p&gt;
&lt;h2 id="the-real-pattern-ai-speeds-up-judgment-not-just-typing"&gt;The real pattern: AI speeds up judgment, not just typing&lt;/h2&gt;
&lt;p&gt;Coding has always been more than generating lines of code. The work is figuring out what &lt;em&gt;should&lt;/em&gt; be written, how it fits into an existing system, and which tradeoffs matter—performance, safety, readability, cost, maintainability, and time-to-debug. Autocomplete can help you type faster. AI assistants help you think faster—&lt;em&gt;if&lt;/em&gt; you already know how to think.&lt;/p&gt;
&lt;p&gt;In practice, senior developers don’t treat AI output as gospel. They treat it like a suggestion engine for implementation details. They can review changes instantly, spot mismatches with architecture, and correct edge cases before those edge cases become incidents.&lt;/p&gt;
&lt;p&gt;That’s the key difference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Senior developers use AI to accelerate evaluation.&lt;/strong&gt; They review, test, refactor, and integrate quickly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Junior developers use AI to accelerate output.&lt;/strong&gt; They may implement quickly, but without the instincts to detect subtle wrongness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The outcome is not theoretical. It’s what you see after months of heavy usage: productivity gains skew higher for seniors because they can convert AI suggestions into correct, production-quality work at speed.&lt;/p&gt;
&lt;h2 id="why-seniors-get-the-biggest-lift-and-juniors-dont"&gt;Why seniors get the biggest lift (and juniors don’t)&lt;/h2&gt;
&lt;p&gt;Imagine you’re building a feature in a mature codebase: an API endpoint that writes to a relational database, triggers background work, and must conform to existing conventions. A junior developer might ask an assistant to “write the endpoint.” The assistant will likely produce something that &lt;em&gt;looks&lt;/em&gt; right: correct shapes, plausible validation, typical error handling.&lt;/p&gt;
&lt;p&gt;But senior developers also ask: “How does this endpoint need to behave under load?” “What are our domain invariants?” “How do we handle idempotency?” “Where does this belong in the layering?” Those questions are where senior judgment lives.&lt;/p&gt;
&lt;p&gt;A concrete example: suppose the assistant proposes a database query that works for small test data but misses an index and causes table scans in production. A junior might not notice, or might not understand the impact until it’s too late. A senior will notice immediately, because they’ve seen the pattern before and understand the query planner implications. They’ll adjust the query shape, add or leverage an index, and ensure the pagination strategy won’t grind the system down.&lt;/p&gt;
&lt;p&gt;That’s why the productivity boost often looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Senior developers: ~2–3× more productive&lt;/strong&gt; (because they can evaluate and correct quickly)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Junior developers: ~1.2× more productive&lt;/strong&gt; (because they can’t reliably separate good suggestions from dangerous ones yet)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notice what’s missing from that comparison: nobody becomes “twice as good” at design just because they can prompt. AI reduces friction, but it doesn’t install experience. Seniors already have it. Juniors are still building it—so they get smaller gains until their review instincts catch up.&lt;/p&gt;
&lt;h2 id="the-overlooked-advantage-faster-feedback-loops"&gt;The overlooked advantage: faster feedback loops&lt;/h2&gt;
&lt;p&gt;AI coding assistants also compress time between “idea” and “verified reality.” That matters because the best developers are the ones who run tight loops:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate a candidate implementation&lt;/li&gt;
&lt;li&gt;Understand it&lt;/li&gt;
&lt;li&gt;Validate it with tests and local reasoning&lt;/li&gt;
&lt;li&gt;Improve it until it meets real constraints&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For seniors, these loops stay intact. In fact, AI can strengthen them. For example, an engineer might use ChatGPT to draft a set of unit tests, then immediately verify them against the existing code patterns. Or they might use Copilot to implement a refactor and then rely on their own test suite and type system to confirm correctness.&lt;/p&gt;
&lt;p&gt;For juniors, the loops can degrade:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate code&lt;/li&gt;
&lt;li&gt;Believe the code&lt;/li&gt;
&lt;li&gt;Run it once&lt;/li&gt;
&lt;li&gt;Fix what breaks&lt;/li&gt;
&lt;li&gt;Ship what “seems” to work&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’ve mentored people, you recognize the failure mode: not malice—just an inability to anticipate what will break later. AI can make it easier to move fast, but without strong validation habits and system understanding, “fast” can become “fragile.”&lt;/p&gt;
&lt;p&gt;Practical advice for teams: don’t just measure output. Measure &lt;em&gt;time-to-correct&lt;/em&gt;. Encourage workflows that force verification. A simple rule like “every AI-generated change must include at least one targeted test or a code review checklist item” can dramatically raise the quality of junior work and reduce the risk of shipping subtle defects.&lt;/p&gt;
&lt;h2 id="how-ai-widens-the-canyon-between-good-and-mediocre"&gt;How AI widens the canyon between good and mediocre&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable truth: AI doesn’t make everyone equal. It makes speed cheaper. And when speed is cheaper, the bottleneck becomes correctness and decision-making.&lt;/p&gt;
&lt;p&gt;So the gap widens, because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mediocre developers lean harder on AI because it feels productive.&lt;/li&gt;
&lt;li&gt;They may accept plausible-but-wrong solutions.&lt;/li&gt;
&lt;li&gt;Their review standards don’t improve as fast as their ability to produce code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meanwhile, strong developers use AI as leverage. They ask better questions, structure work more cleanly, and refine AI output into something coherent with the system. They also know where to draw boundaries—what to generate, what to write by hand, and what to question.&lt;/p&gt;
&lt;p&gt;This is the “canyon” effect: differences that were once softened by slower iteration now become obvious. In code reviews, you’ll see it in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consistent architecture choices&lt;/li&gt;
&lt;li&gt;disciplined error handling&lt;/li&gt;
&lt;li&gt;thoughtful naming and documentation&lt;/li&gt;
&lt;li&gt;test coverage that matches risk&lt;/li&gt;
&lt;li&gt;PRs that require fewer follow-up fixes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best engineers won’t disappear. They’ll become the people whose reviews prevent costly mistakes and whose implementations align with the system’s reality.&lt;/p&gt;
&lt;h2 id="the-new-senior-skill-guiding-the-assistant-like-a-teammate"&gt;The new senior skill: guiding the assistant like a teammate&lt;/h2&gt;
&lt;p&gt;If you want to remain valuable in an AI-accelerated world, don’t try to “use AI more.” Use it smarter.&lt;/p&gt;
&lt;p&gt;A senior developer’s approach looks less like “prompting” and more like directing a working session:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Provide context early.&lt;/strong&gt; Link to the relevant modules, describe invariants, and mention constraints (“must be backwards compatible,” “avoid N+1 queries,” “follow existing logging conventions”).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ask for changes, not miracles.&lt;/strong&gt; “Refactor this function to…” beats “Rewrite everything to be better.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Require a review plan.&lt;/strong&gt; “List the edge cases you handled and how you’d test them.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confirm with tools you already trust.&lt;/strong&gt; Run linters, type checks, unit tests, and targeted integration tests—then interpret failures like a professional, not like a spectator.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, instead of asking, “Can you write the payment webhook handler?” try:&lt;br&gt;
“Given this event schema and these idempotency rules, implement the handler. Also propose a set of tests that verify retry behavior and duplicate event handling.”&lt;/p&gt;
&lt;p&gt;That single shift—explicit constraints plus verification—turns AI output into something a senior can validate quickly and juniors can learn from. It’s not just about correctness. It’s about teaching the habits that produce correct systems over time.&lt;/p&gt;
&lt;h2 id="what-companies-should-do-right-now"&gt;What companies should do right now&lt;/h2&gt;
&lt;p&gt;This is where leadership matters. If you treat AI coding assistants like a productivity hack, you’ll get patchy results. If you treat them like an experience amplifier, you’ll get measurable quality gains.&lt;/p&gt;
&lt;p&gt;Start with policies that encourage disciplined use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code review expectations:&lt;/strong&gt; AI-generated code still gets reviewed, but seniors should review for &lt;em&gt;reasoning&lt;/em&gt;, not just syntax.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test gates:&lt;/strong&gt; require tests for non-trivial changes, especially around data access, concurrency, authentication, and money-like domains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompting standards:&lt;/strong&gt; encourage developers to include constraints and to request test plans for risky code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mentorship pairing:&lt;/strong&gt; seniors can “drive” AI-assisted work while juniors learn by observing how decisions are made and validated.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, update evaluation criteria. If your hiring or performance reviews reward raw PR volume, AI will distort the metric. Reward the things that scale: fewer regressions, faster resolution of complex issues, better system design, and improved maintainability.&lt;/p&gt;
&lt;p&gt;The most successful teams will recognize that AI doesn’t replace senior talent. It concentrates it—because the people who can interpret, validate, and integrate AI output are the ones who prevent costly failures.&lt;/p&gt;
&lt;h2 id="conclusion-ai-doesnt-replace-expertiseit-concentrates-it"&gt;Conclusion: AI doesn’t replace expertise—it concentrates it&lt;/h2&gt;
&lt;p&gt;AI coding assistants are changing how software gets written, but they aren’t rewriting the fundamentals. Code still needs judgment. Systems still need context. Production still punishes assumptions.&lt;/p&gt;
&lt;p&gt;What’s emerging after widespread Copilot and ChatGPT adoption is a simple, opinionated takeaway: &lt;strong&gt;seniors get more valuable because they can turn AI suggestions into correct software immediately.&lt;/strong&gt; Juniors may get some speed, but the biggest gains come when they build the experience to evaluate output safely.&lt;/p&gt;
&lt;p&gt;So don’t panic. Invest in senior-level review culture, tighten validation loops, and teach junior developers the decision-making habits that AI can accelerate—but not replace. The future won’t be “AI programmers.” It will be “judgment-powered programmers,” and the ones who master that will lead.&lt;/p&gt;</content></item><item><title>WebGPU Is the Graphics API the Web Actually Deserves</title><link>https://decastro.work/blog/webgpu-graphics-api-web-deserves/</link><pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/webgpu-graphics-api-web-deserves/</guid><description>&lt;p&gt;For years, the browser’s graphics stack felt like a compromise: great for demos, limiting for serious work, and increasingly out of sync with what GPUs can actually do. WebGL helped the web learn to talk to hardware—but it was built on older assumptions. WebGPU is different. It’s the first graphics API designed with modern GPU programming in mind, and it finally gives browser developers the kind of control—and performance headroom—that desktop apps have enjoyed for years.&lt;/p&gt;</description><content>&lt;p&gt;For years, the browser’s graphics stack felt like a compromise: great for demos, limiting for serious work, and increasingly out of sync with what GPUs can actually do. WebGL helped the web learn to talk to hardware—but it was built on older assumptions. WebGPU is different. It’s the first graphics API designed with modern GPU programming in mind, and it finally gives browser developers the kind of control—and performance headroom—that desktop apps have enjoyed for years.&lt;/p&gt;
&lt;h2 id="webgls-hidden-tax-opengl-es-in-a-modern-world"&gt;WebGL’s hidden tax: OpenGL ES, in a modern world&lt;/h2&gt;
&lt;p&gt;WebGL did something crucial: it proved the browser could do real-time graphics without installing drivers or runtimes. But the “classic” path still shows its age. WebGL is effectively a web-friendly wrapper around an OpenGL ES-era model—stateful, abstraction-heavy, and not particularly aligned with how modern GPUs are pipelined and optimized.&lt;/p&gt;
&lt;p&gt;The practical consequences show up quickly as you push beyond simple rendering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You fight the API&lt;/strong&gt;: Many performance wins require batching, careful buffer updates, and predictable state changes—things WebGL can encourage, but it also makes awkward when your workload is modern and compute-heavy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute remains a second-class citizen&lt;/strong&gt;: WebGL’s main story is rendering. Yes, you can do GPU computations with fragment shaders and framebuffer tricks, but that’s a workaround—not an ergonomic model for general parallel compute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging and profiling are harder than they should be&lt;/strong&gt;: When your mental model is “draw calls and shader uniforms,” it’s tough to translate that to the kind of explicit scheduling and resource management that GPUs increasingly benefit from.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, WebGL made the web capable. But it didn’t make the web &lt;em&gt;native&lt;/em&gt; to GPU workflows.&lt;/p&gt;
&lt;h2 id="webgpus-big-idea-explicit-vulkan-inspired-control"&gt;WebGPU’s big idea: explicit, Vulkan-inspired control&lt;/h2&gt;
&lt;p&gt;WebGPU is the browser’s answer to a fundamental problem: GPUs don’t scale well with vague intent. They like explicitness—clear resource lifetimes, predictable pipeline states, and command buffers that can be optimized.&lt;/p&gt;
&lt;p&gt;WebGPU borrows architectural ideas from Vulkan: explicit resource binding, pipeline objects, and command-based submission. The difference is that it’s designed to feel approachable in JavaScript and safe by default.&lt;/p&gt;
&lt;p&gt;Concretely, you get a programming model that maps more cleanly to GPU reality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pipelines instead of “set random state and hope”&lt;/strong&gt;: You define how the GPU should run—vertex/fragment stages for rendering, compute pipelines for general GPU workloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffers and textures as first-class citizens&lt;/strong&gt;: You manage GPU memory objects explicitly, which makes performance tuning much more straightforward.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compute shaders are real&lt;/strong&gt;: No more treating fragments like compute threads by accident. If your problem is parallel, WebGPU treats it that way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command encoders and submission&lt;/strong&gt;: You build a plan for the GPU, then hand it off. That’s not just “clean”—it’s how you unlock consistent performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why WebGPU is more than a new renderer. It’s a general GPU API that lets web developers stop pretending the GPU is a glorified background effect.&lt;/p&gt;
&lt;h2 id="what-vulkan-level-performance-looks-like-in-practice"&gt;What “Vulkan-level performance” looks like in practice&lt;/h2&gt;
&lt;p&gt;Let’s translate the theory into something you can actually ship. High performance isn’t magic; it’s about avoiding avoidable overhead and keeping the GPU fed.&lt;/p&gt;
&lt;p&gt;WebGPU’s biggest practical wins tend to come from three areas:&lt;/p&gt;
&lt;h3 id="1-less-cpugpu-thrash-through-predictability"&gt;1) Less CPU/GPU thrash through predictability&lt;/h3&gt;
&lt;p&gt;Modern rendering performance often collapses when the CPU has to constantly reconfigure GPU state or when resource updates happen in unpredictable patterns. WebGPU encourages stable pipeline objects and explicit buffer management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Suppose you’re rendering a large scene with thousands of instances. With WebGL, you’ll often feel the pain of per-frame uniform churn and state changes. With WebGPU, you can structure data so instance transforms live in a single GPU buffer and you use appropriate binding strategies to keep draw calls consistent.&lt;/p&gt;
&lt;h3 id="2-gpu-compute-that-doesnt-feel-like-a-hack"&gt;2) GPU compute that doesn’t feel like a hack&lt;/h3&gt;
&lt;p&gt;If you’ve ever tried to run an ML inference step in the browser, you’ve likely hit the wall where your “GPU compute” approach feels like a shader trick instead of a compute pipeline. WebGPU’s compute support is designed for exactly this kind of workload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Implement a small neural network layer (e.g., matrix multiply + activation) with a compute shader. Instead of packing data into textures and writing “fragment compute,” you can dispatch work groups directly and control how buffers map to the GPU.&lt;/p&gt;
&lt;h3 id="3-better-scaling-for-parallel-workloads"&gt;3) Better scaling for parallel workloads&lt;/h3&gt;
&lt;p&gt;Performance isn’t only about frames per second. It’s also about throughput for workloads that can be parallelized—video processing, image transforms, simulation steps, scientific visualization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A web-based microscopy viewer can use compute passes to denoise frames or enhance contrast. Because these operations are uniform per pixel or per region, they map well to dispatchable compute work. The result is not just faster processing—it’s the difference between “interactive” and “wait for the results.”&lt;/p&gt;
&lt;p&gt;You don’t need to believe grand promises to see the value. If your workload is parallel and you want to avoid desktop-only pipelines, WebGPU gives you a serious path to get there.&lt;/p&gt;
&lt;h2 id="beyond-games-real-applications-that-benefit-from-webgpu"&gt;Beyond games: real applications that benefit from WebGPU&lt;/h2&gt;
&lt;p&gt;Games are the obvious headline because they stress the GPU. But the browser’s most valuable opportunities are broader: any time you need GPU acceleration in a place where your users already are.&lt;/p&gt;
&lt;p&gt;Here are the categories where WebGPU’s model is a strong fit:&lt;/p&gt;
&lt;h3 id="gpu-accelerated-machine-learning-inference"&gt;GPU-accelerated machine learning inference&lt;/h3&gt;
&lt;p&gt;On-device inference in the browser is compelling: fewer round trips, better privacy, and an easier “it just runs” story. WebGPU can accelerate tensor operations using compute shaders and carefully planned memory layouts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Start with a small subset of operations—like convolution or matrix multiplications—then expand. Don’t try to replicate an entire ML runtime on day one. Build a pipeline that keeps tensors in GPU buffers and minimizes host-device transfers.&lt;/p&gt;
&lt;h3 id="scientific-visualization-and-data-exploration"&gt;Scientific visualization and data exploration&lt;/h3&gt;
&lt;p&gt;Scientists and analysts increasingly expect interactive plots, volume rendering, and simulations. WebGPU’s explicit pipeline and compute support can power progressive rendering, smoothing filters, or on-GPU transformations of large datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Use compute passes to pre-process data into GPU-friendly formats. Then render using those outputs. Separating “prepare” from “draw” often leads to clearer code and more predictable performance.&lt;/p&gt;
&lt;h3 id="video-processing-and-real-time-image-pipelines"&gt;Video processing and real-time image pipelines&lt;/h3&gt;
&lt;p&gt;Filters, warps, denoising, stabilization, and effects are perfect for parallel processing. With WebGPU, you can build pipelines that operate on frames efficiently rather than relying on CPU-heavy transforms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Design your pipeline to reuse GPU buffers between frames. Allocate once, then update contents. Frequent reallocation is the silent performance killer.&lt;/p&gt;
&lt;h3 id="parallel-computation-for-web-native-tools"&gt;Parallel computation for web-native tools&lt;/h3&gt;
&lt;p&gt;Think simulations, procedural generation, particle systems, physics approximations, or interactive editing with GPU-accelerated preview steps. WebGPU lets you treat the browser as a real compute environment, not just a UI wrapper.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; Treat GPU work like a pipeline, not a function call. Batch operations when possible and structure your command submission to avoid stalling.&lt;/p&gt;
&lt;h2 id="how-to-think-about-development-building-pipelines-not-patches"&gt;How to think about development: building pipelines, not patches&lt;/h2&gt;
&lt;p&gt;WebGPU is powerful, but it rewards the right mindset. If you treat it like WebGL—incrementally patching state and hoping for the best—you’ll underutilize its strengths. If you treat it like a compute-and-render pipeline, you’ll get better results faster.&lt;/p&gt;
&lt;p&gt;Here’s a practical workflow that tends to work well:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with a narrow, measurable goal.&lt;/strong&gt;&lt;br&gt;
Example: “Apply a blur to an image at interactive latency.” Don’t start with “build a full engine.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Design buffer and data flow upfront.&lt;/strong&gt;&lt;br&gt;
Decide what lives on the GPU, what needs to move, and how often. Host-device transfers are where performance plans go to die.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create explicit pipelines for each stage.&lt;/strong&gt;&lt;br&gt;
Rendering stages belong in render pipelines; compute stages belong in compute pipelines. Mixing everything into one shader often makes optimization harder.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use predictable resource binding.&lt;/strong&gt;&lt;br&gt;
Favor stable bind groups and consistent layouts. The more your structure stays constant between frames, the easier it is for the runtime—and the GPU—to keep performance smooth.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Profile like you mean it.&lt;/strong&gt;&lt;br&gt;
You want to know where time goes: command encoding, buffer updates, shader execution, or synchronization. WebGPU gives you enough structure to investigate, but you still have to look.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you do this, WebGPU becomes less about learning a new API and more about adopting a better performance discipline.&lt;/p&gt;
&lt;h2 id="conclusion-the-web-is-finally-catching-up-to-gpus"&gt;Conclusion: the web is finally catching up to GPUs&lt;/h2&gt;
&lt;p&gt;WebGPU isn’t “WebGL, but newer.” It’s a different philosophy: explicit GPU control, compute-first capability, and a pipeline architecture inspired by the APIs that power serious desktop graphics and compute workloads.&lt;/p&gt;
&lt;p&gt;And that matters because the browser is no longer just a place to display content—it’s becoming a platform for real-time computation, from ML inference to scientific visualization to high-throughput media processing. WebGPU is the foundation that makes those experiences feel less like magic tricks and more like engineering.&lt;/p&gt;</content></item><item><title>Nix Is the Future of Reproducible Development Environments</title><link>https://decastro.work/blog/nix-future-reproducible-development-environments/</link><pubDate>Sat, 28 Jan 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/nix-future-reproducible-development-environments/</guid><description>&lt;p&gt;“Works on my machine” isn’t a bug—it’s a business model. It’s what happens when your developer environment is an artisanal snowflake, assembled from shell history, undocumented installs, and whatever your laptop decided to keep. Dev containers help, but they’re still a packaging strategy around an inherently mutable reality. Nix takes the opposite approach: define the world you need, lock it down with cryptographic hashes, and make it reproducible everywhere—from a teammate’s laptop to CI.&lt;/p&gt;</description><content>&lt;p&gt;“Works on my machine” isn’t a bug—it’s a business model. It’s what happens when your developer environment is an artisanal snowflake, assembled from shell history, undocumented installs, and whatever your laptop decided to keep. Dev containers help, but they’re still a packaging strategy around an inherently mutable reality. Nix takes the opposite approach: define the world you need, lock it down with cryptographic hashes, and make it reproducible everywhere—from a teammate’s laptop to CI.&lt;/p&gt;
&lt;p&gt;If you’ve spent more time debugging environment drift than debugging code, this is the pivot you’ve been looking for.&lt;/p&gt;
&lt;h2 id="why-developer-environments-keep-breaking"&gt;Why developer environments keep breaking&lt;/h2&gt;
&lt;p&gt;In most teams, “the dev environment” is a pile of assumptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The “right” Node or Python version is installed… somewhere.&lt;/li&gt;
&lt;li&gt;OS-level libraries exist… on some machines.&lt;/li&gt;
&lt;li&gt;Tooling plugins match… mostly.&lt;/li&gt;
&lt;li&gt;Your local &lt;code&gt;~/.config&lt;/code&gt; and &lt;code&gt;~/.cache&lt;/code&gt; don’t accidentally influence runtime behavior.&lt;/li&gt;
&lt;li&gt;Someone’s favorite workaround didn’t get written down, because it “only matters on this one distro.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even with Docker, the story doesn’t fully change. Containers give you a clean filesystem and a repeatable filesystem layout, but they often rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A base image that drifts over time (even when you “pin” vaguely).&lt;/li&gt;
&lt;li&gt;Build steps that can introduce nondeterminism (apt repositories, network timing, transitive dependency churn).&lt;/li&gt;
&lt;li&gt;A “container for dev” that still depends on the developer to pull the right tags, mount volumes correctly, and set env vars.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Net result: you trade one class of pain for another. The real issue is not packaging—it’s &lt;em&gt;declaration&lt;/em&gt; and &lt;em&gt;isolation&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="nix-flips-the-model-declare-everything-isolate-it"&gt;Nix flips the model: declare everything, isolate it&lt;/h2&gt;
&lt;p&gt;Nix is built on a simple, radical premise: the environment should be derived entirely from inputs you declare, not from what happens to exist on a machine.&lt;/p&gt;
&lt;p&gt;Instead of “install these things and hope,” Nix treats each dependency—system libraries, language runtimes, compilers, build tools, CLIs—as something that can be represented precisely. Those definitions are versioned and stored in a content-addressed way: when your inputs change, the resulting environment changes too, and Nix knows exactly how.&lt;/p&gt;
&lt;p&gt;The crucial part isn’t just that it installs dependencies. It’s that it produces an environment that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deterministic&lt;/strong&gt;: same declared inputs → same environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolated&lt;/strong&gt;: dependencies don’t leak into each other or rely on global state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable&lt;/strong&gt;: you can build new environments by combining precise pieces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s why Nix feels different. It doesn’t ask developers to follow instructions. It asks them to accept a specification.&lt;/p&gt;
&lt;h2 id="reproducibility-with-nix-flakes-the-it-works-in-ci-factor"&gt;Reproducibility with Nix flakes: the “it works in CI” factor&lt;/h2&gt;
&lt;p&gt;Where teams really feel the pain is the transition from “local works” to “CI passes.” Nix flakes address that by turning your environment definition into a first-class artifact you can reference across machines.&lt;/p&gt;
&lt;p&gt;A “flake” is essentially a reproducible bundle: you define inputs (like specific package sources) and outputs (like a dev shell). Then you let Nix resolve and build the exact dependency graph.&lt;/p&gt;
&lt;p&gt;Here’s the practical payoff: your CI pipeline can use the &lt;em&gt;same environment definition&lt;/em&gt; your developers use locally. That means the question stops being:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Why does CI fail when it works here?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…and becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Which input changed, and what exactly did it change?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also pin versions tightly. When you upgrade something, you upgrade intentionally, and the diff is real—because Nix computes the new closure from your new inputs.&lt;/p&gt;
&lt;p&gt;If you’ve used Docker with “latest” tags or a half-pinned base image, you already understand the temptation that reproducibility battles. Nix makes that temptation less productive.&lt;/p&gt;
&lt;h2 id="a-concrete-workflow-nix-develop-for-instant-parity"&gt;A concrete workflow: &lt;code&gt;nix develop&lt;/code&gt; for instant parity&lt;/h2&gt;
&lt;p&gt;Let’s say you’re building a service with a Rust backend and a Node-based toolchain (common in modern stacks). In a README-driven world, your setup might be spread across:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;language version managers (sometimes conflicting),&lt;/li&gt;
&lt;li&gt;OS package installs for shared libs,&lt;/li&gt;
&lt;li&gt;global npm installs,&lt;/li&gt;
&lt;li&gt;special environment variables that “you’ll see in the docs.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a Nix world, you specify those requirements once. Then the developer experience becomes boring—in the best way.&lt;/p&gt;
&lt;p&gt;A developer runs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nix develop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…and gets a shell with the right Rust toolchain, the Node version you declared, and any system libraries your builds require. No “install this, then reboot, then run this script.” No “wait, you’re on the other minor version.” The environment is built from a dependency graph rather than a set of vibes.&lt;/p&gt;
&lt;h3 id="practical-advice-for-making-this-stick"&gt;Practical advice for making this stick&lt;/h3&gt;
&lt;p&gt;If you’re adopting Nix, don’t try to boil the ocean on day one. Start with the highest-friction parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Put build tools under Nix first&lt;/strong&gt;: compiler, language runtime, and core build dependencies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then add OS libraries&lt;/strong&gt; that previously lived in “install on Ubuntu/Debian” instructions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Finally add dev conveniences&lt;/strong&gt; (formatters, linters, test runners, CLIs).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This incremental approach matters because teams often resist the learning curve when they don’t see immediate benefits. Your first win should be: “I can stop explaining setup.”&lt;/p&gt;
&lt;h2 id="dev-containers-arent-wrongbut-theyre-incomplete"&gt;Dev containers aren’t wrong—but they’re incomplete&lt;/h2&gt;
&lt;p&gt;Dev containers are a good idea: they make the environment portable and reduce host-specific differences. But they tend to be &lt;em&gt;procedural&lt;/em&gt;. You write steps; the result depends on the state of registries, network availability, and how carefully you pin versions.&lt;/p&gt;
&lt;p&gt;Even when you’re disciplined, you still get friction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rebuilding images for small changes can be slow.&lt;/li&gt;
&lt;li&gt;Updating base images can introduce subtle drift.&lt;/li&gt;
&lt;li&gt;You can end up with two sources of truth: the Dockerfile and the README, or the Dockerfile and your CI scripts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nix doesn’t eliminate every form of overhead—nothing eliminates all cost in software. But it targets the real defect: “the environment isn’t defined as a pure function of inputs.”&lt;/p&gt;
&lt;p&gt;In other words, Docker can help you ship a known filesystem. Nix aims to ship a known &lt;em&gt;world&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;And once you’ve experienced a &lt;code&gt;nix develop&lt;/code&gt; that consistently lands you in the right environment, the “works on my machine” genre starts to look like legacy software.&lt;/p&gt;
&lt;h2 id="the-learning-curve-steep-but-worth-designing-around"&gt;The learning curve: steep, but worth designing around&lt;/h2&gt;
&lt;p&gt;Nix has a reputation for being difficult, and that’s not entirely unfair. You’re stepping into a new way of thinking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of installing things globally, you define them.&lt;/li&gt;
&lt;li&gt;Instead of relying on mutable state, you embrace immutability and composition.&lt;/li&gt;
&lt;li&gt;Instead of a linear “do these commands,” you specify a graph.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the learning curve softens fast if you treat Nix like infrastructure, not like magic. A few strategies that work in real teams:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Template your flake outputs&lt;/strong&gt;: standardize a “dev shell” pattern and reuse it across repos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document the interface, not every internal detail&lt;/strong&gt;: developers shouldn’t need to memorize the whole Nix language to run the shell.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make upgrades explicit&lt;/strong&gt;: when you bump a dependency, it should be obvious which input changed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep your scope tight&lt;/strong&gt;: define exactly what the build and dev tooling needs, no more.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you do this right, developers mostly interact with a stable command set, not a novel programming language.&lt;/p&gt;
&lt;p&gt;And yes—initial setup will feel awkward. But awkwardness tends to fade when the payoff is immediate: fewer environment bugs, faster onboarding, and fewer CI “surprises.”&lt;/p&gt;
&lt;h2 id="conclusion-reproducibility-is-a-feature-not-a-ritual"&gt;Conclusion: reproducibility is a feature, not a ritual&lt;/h2&gt;
&lt;p&gt;Reproducible development environments aren’t a luxury for elite teams—they’re a foundation for velocity. Dev containers reduce drift, but Nix tackles the root cause: environments should be derived from declared inputs and isolated with cryptographic precision.&lt;/p&gt;
&lt;p&gt;Once your team stops treating setup as tribal knowledge and starts treating it as a versioned artifact, the entire development workflow tightens. &lt;code&gt;nix develop&lt;/code&gt; becomes less like a tool and more like a contract: the environment you run locally is the one you build, test, and deploy against.&lt;/p&gt;
&lt;p&gt;That’s the future—because it’s the only future where “works on my machine” stops being a recurring headline.&lt;/p&gt;</content></item><item><title>The Surprising Depth of Modern HTML</title><link>https://decastro.work/blog/surprising-depth-modern-html/</link><pubDate>Mon, 16 Jan 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/surprising-depth-modern-html/</guid><description>&lt;p&gt;Most developers treat HTML like a dumb container: a place to put &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; tags and hope JavaScript fixes everything later. That mindset is dated. Modern HTML has grown into a capable UI and data-validation layer—quietly, steadily, and without asking for attention. If you’ve stopped learning HTML since the “2015 era,” you’re almost certainly paying extra complexity to solve problems the browser already knows how to handle.&lt;/p&gt;
&lt;p&gt;Let’s talk about the overlooked depth: dialogs, accordions, lazy loading, form validation, and a few adjacent features that let you ship cleaner, faster, more maintainable front ends with less JavaScript.&lt;/p&gt;</description><content>&lt;p&gt;Most developers treat HTML like a dumb container: a place to put &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; tags and hope JavaScript fixes everything later. That mindset is dated. Modern HTML has grown into a capable UI and data-validation layer—quietly, steadily, and without asking for attention. If you’ve stopped learning HTML since the “2015 era,” you’re almost certainly paying extra complexity to solve problems the browser already knows how to handle.&lt;/p&gt;
&lt;p&gt;Let’s talk about the overlooked depth: dialogs, accordions, lazy loading, form validation, and a few adjacent features that let you ship cleaner, faster, more maintainable front ends with less JavaScript.&lt;/p&gt;
&lt;h2 id="html-isnt-just-markup-anymore-its-a-ui-contract"&gt;HTML Isn’t Just Markup Anymore (It’s a UI Contract)&lt;/h2&gt;
&lt;p&gt;HTML used to be “structure, then script.” That’s still mostly true—but the split has narrowed. Browsers now interpret HTML attributes and semantics in ways that used to require custom code.&lt;/p&gt;
&lt;p&gt;The practical implication is simple: before you reach for a JavaScript solution, ask whether HTML can do it declaratively. Not because JavaScript is “bad,” but because native browser behavior is usually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;More consistent&lt;/strong&gt; (works across pages and components without extra glue)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More accessible&lt;/strong&gt; by default when you use the right semantics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Less code&lt;/strong&gt; to maintain and less surface area for bugs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster to load&lt;/strong&gt; when you can reduce scripts and event handlers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need to make your app “HTML only.” You do need to stop treating HTML as powerless.&lt;/p&gt;
&lt;h2 id="dialogs-native-modals-without-a-library"&gt;Dialogs: Native Modals Without a Library&lt;/h2&gt;
&lt;p&gt;If you’ve built modal dialogs, you already know the usual pain: focus trapping, ESC-to-close, preventing background scroll, keyboard navigation, ARIA wiring, click-outside behavior, and lifecycle edge cases. Most teams solve this once with a library and then never question whether they could do better.&lt;/p&gt;
&lt;p&gt;The built-in &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element is the point where “HTML as structure” becomes “HTML as interaction.” It’s designed for exactly this job.&lt;/p&gt;
&lt;h3 id="example-a-real-modal-with-minimal-code"&gt;Example: A real modal with minimal code&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;open&amp;#34;&lt;/span&gt;&amp;gt;Open settings&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;dialog&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;settings&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;form&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;dialog&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;Settings&amp;lt;/&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Notifications
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;checkbox&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;notify&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;checked&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;menu&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;cancel&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;formmethod&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;dialog&amp;#34;&lt;/span&gt;&amp;gt;Close&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;save&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;saveBtn&amp;#34;&lt;/span&gt;&amp;gt;Save&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;menu&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;dialog&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dialog&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; document.&lt;span style="color:#a6e22e"&gt;getElementById&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;settings&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;getElementById&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;open&amp;#39;&lt;/span&gt;).&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;click&amp;#39;&lt;/span&gt;, () =&amp;gt; &lt;span style="color:#a6e22e"&gt;dialog&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;showModal&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;dialog&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;addEventListener&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;close&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;dialog&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;returnValue&lt;/span&gt; &lt;span style="color:#f92672"&gt;===&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;save&amp;#39;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// read form values if needed
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;console&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Save clicked&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You still need a little JavaScript to open it and handle results—but you avoid the entire “modal implementation” layer. The browser handles the semantics and default behavior; you handle the application logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated take:&lt;/strong&gt; if your current modal solution is a pile of focus and keyboard handling code, you’re probably over-engineering. Start with &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; and only customize behavior when you truly must.&lt;/p&gt;
&lt;h2 id="accordions-and-details-let-html-do-the-toggling"&gt;Accordions and Details: Let HTML Do the Toggling&lt;/h2&gt;
&lt;p&gt;Accordion components are everywhere: FAQs, filters, docs sections. Historically, teams built them by hand: ARIA attributes, &lt;code&gt;aria-expanded&lt;/code&gt;, click handlers, and CSS states.&lt;/p&gt;
&lt;p&gt;Modern HTML gives you a native pattern: the &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element and its sibling &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;. Together, they implement the “expand/collapse” behavior with semantics the browser understands.&lt;/p&gt;
&lt;h3 id="example-a-native-accordion"&gt;Example: A native accordion&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;details&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;summary&lt;/span&gt;&amp;gt;What’s included in the plan?&amp;lt;/&lt;span style="color:#f92672"&gt;summary&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;Everything you need to ship: templates, components, and support.&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;details&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;details&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;summary&lt;/span&gt;&amp;gt;Can I change my billing later?&amp;lt;/&lt;span style="color:#f92672"&gt;summary&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;Yes. Update your payment method and switch tiers anytime.&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;details&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Want one open at a time? That’s still possible with a tiny amount of JS, but you can often avoid it by accepting “multiple open” behavior. Users generally don’t need complex behavior for basic FAQs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; if you’re using an accordion library, check whether it’s adding value beyond styling. If the behavior is just expand/collapse, native HTML is a strong default.&lt;/p&gt;
&lt;h2 id="lazy-loading-loadinglazy-beats-custom-image-scripts"&gt;Lazy Loading: &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; Beats Custom Image Scripts&lt;/h2&gt;
&lt;p&gt;Image lazy loading is one of those features that used to be “required engineering.” You’d wire an IntersectionObserver, manage placeholders, handle edge cases, and hope it worked across browsers.&lt;/p&gt;
&lt;p&gt;Today, for the common case, HTML has the answer: the &lt;code&gt;loading&lt;/code&gt; attribute.&lt;/p&gt;
&lt;h3 id="example-native-lazy-loading"&gt;Example: Native lazy loading&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;img&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;/images/product-3.jpg&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alt&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Product photo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;lazy&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is intentionally modest: it won’t replace every advanced strategy (like responsive &lt;code&gt;srcset&lt;/code&gt; decisions, complex prefetching rules, or sophisticated viewport heuristics). But if your current code is basically “lazy load images as they approach the viewport,” you can almost certainly remove a chunk of JavaScript and let the browser handle it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated take:&lt;/strong&gt; treat &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; as the default. Add custom logic only when you’re solving a genuinely specific problem, not because “we always did it with JS.”&lt;/p&gt;
&lt;h2 id="form-validation-stop-writing-tiny-validators"&gt;Form Validation: Stop Writing Tiny Validators&lt;/h2&gt;
&lt;p&gt;One of the biggest areas where developers accidentally reinvent the wheel is validation. Many teams write JavaScript that checks “this field must be an email,” “this must be at least N characters,” or “this should match a format.” Then they display errors based on the result.&lt;/p&gt;
&lt;p&gt;Modern HTML supports validation attributes that perform checks natively and integrate with browser UX patterns.&lt;/p&gt;
&lt;h3 id="example-validate-with-pattern-minlength-and-more"&gt;Example: Validate with &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;minlength&lt;/code&gt;, and more&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Username
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;username&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;required&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;minlength&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;maxlength&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;20&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pattern&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;^[a-zA-Z0-9_]+$&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Use only letters, numbers, and underscore.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Zip code
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;zip&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;required&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;inputmode&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;numeric&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pattern&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;^\d{5}(-\d{4})?$&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;label&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&amp;gt;Create account&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built-in browser feedback behavior&lt;/li&gt;
&lt;li&gt;Reduced JS code&lt;/li&gt;
&lt;li&gt;Better accessibility because the browser knows what constraints exist&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you do need custom validation logic (cross-field checks, server-backed constraints), you can still use JavaScript—but the “basic constraints” should live in HTML whenever possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical advice:&lt;/strong&gt; When a validator is purely about &lt;em&gt;data shape&lt;/em&gt;, don’t hide that knowledge in JavaScript. Put it in HTML so it’s visible, declarative, and consistent.&lt;/p&gt;
&lt;h2 id="native-semantics-use-the-browsers-built-in-accessibility-and-navigation"&gt;Native Semantics: Use the Browser’s Built-In Accessibility and Navigation&lt;/h2&gt;
&lt;p&gt;HTML isn’t just about widgets. It’s also about semantics that the browser and assistive technologies understand.&lt;/p&gt;
&lt;p&gt;A few examples that commonly get overlooked because they’re “obvious” when you learn HTML but quietly disappear in real projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use real headings (&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;–&lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;) instead of styled text pretending to be headings.&lt;/li&gt;
&lt;li&gt;Use lists (&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt;) for collections instead of stacking &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; elements.&lt;/li&gt;
&lt;li&gt;Use buttons (&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;) for actions instead of clickable &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; tied to form fields so clicking text focuses inputs.&lt;/li&gt;
&lt;li&gt;Use meaningful link text and avoid “click here.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These choices reduce the need for custom scripting and increase usability. The browser will handle keyboard navigation and focus behavior in ways that are difficult to replicate manually without careful attention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinionated take:&lt;/strong&gt; if you find yourself adding JavaScript just to make interaction “work,” it’s often a sign that your underlying semantics aren’t communicating intent to the browser.&lt;/p&gt;
&lt;h2 id="a-better-workflow-decline-javascript-by-default"&gt;A Better Workflow: Decline JavaScript by Default&lt;/h2&gt;
&lt;p&gt;The most effective way to benefit from modern HTML isn’t to memorize every tag and attribute. It’s to adopt a workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with structure and semantics.&lt;/strong&gt; Can you express the UI with native elements?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for declarative capabilities.&lt;/strong&gt; Dialogs? Accordions? Validation? Lazy loading?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add JavaScript only for state and business logic.&lt;/strong&gt; Not for basic interaction mechanics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure tradeoffs.&lt;/strong&gt; If custom behavior is required, fine—but justify it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A concrete example: suppose you’re building a product page with&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an FAQ accordion,&lt;/li&gt;
&lt;li&gt;a “quick settings” modal,&lt;/li&gt;
&lt;li&gt;images that load as users scroll,&lt;/li&gt;
&lt;li&gt;and a sign-up form with constraints.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A strong approach is to use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;details&amp;gt;/&amp;lt;summary&amp;gt;&lt;/code&gt; for FAQs,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; for the modal,&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; for images,&lt;/li&gt;
&lt;li&gt;and HTML validation attributes for input constraints.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then use a small amount of JS to coordinate submission, update server state, or perform cross-field logic. You’ll end up with fewer event listeners, less UI code, and an interface that feels more “native” to the browser.&lt;/p&gt;
&lt;h2 id="conclusion-stop-paying-javascript-tax-for-things-html-already-solves"&gt;Conclusion: Stop Paying JavaScript Tax for Things HTML Already Solves&lt;/h2&gt;
&lt;p&gt;Modern HTML has matured into a practical toolkit for UI behavior, validation, and performance. The “HTML is dumb” assumption is no longer just outdated—it’s expensive. Use &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; for modals, &lt;code&gt;&amp;lt;details&amp;gt;/&amp;lt;summary&amp;gt;&lt;/code&gt; for accordions, &lt;code&gt;loading=&amp;quot;lazy&amp;quot;&lt;/code&gt; for images, and validation attributes for form constraints. When you combine those with sound semantics, you’ll write less JavaScript and ship interfaces that are easier to maintain, more accessible, and faster by default.&lt;/p&gt;</content></item><item><title>The Death of Heroku and What It Means for Developer PaaS</title><link>https://decastro.work/blog/death-of-heroku-developer-paas/</link><pubDate>Tue, 10 Jan 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/death-of-heroku-developer-paas/</guid><description>&lt;p&gt;For years, “deploy to Heroku” was the punchline and the strategy: spin up a real web app in minutes, keep costs near zero, and iterate without thinking too hard. When Salesforce removed the free tier in late 2022, the community didn’t just lose a feature—it lost a safety net. And the scramble that followed turned “platform as a service” from a convenience into a risk decision.&lt;/p&gt;
&lt;p&gt;This is the real lesson of Heroku’s fall: if your PaaS model depends on free hosting, you’re not building a product—you’re building your roadmap on someone else’s pricing page. The good news is that the alternatives aren’t just replacements. In many cases, they’re improvements.&lt;/p&gt;</description><content>&lt;p&gt;For years, “deploy to Heroku” was the punchline and the strategy: spin up a real web app in minutes, keep costs near zero, and iterate without thinking too hard. When Salesforce removed the free tier in late 2022, the community didn’t just lose a feature—it lost a safety net. And the scramble that followed turned “platform as a service” from a convenience into a risk decision.&lt;/p&gt;
&lt;p&gt;This is the real lesson of Heroku’s fall: if your PaaS model depends on free hosting, you’re not building a product—you’re building your roadmap on someone else’s pricing page. The good news is that the alternatives aren’t just replacements. In many cases, they’re improvements.&lt;/p&gt;
&lt;h2 id="what-herokus-free-tier-really-was-and-why-it-mattered"&gt;What “Heroku’s Free Tier” Really Was (and Why It Mattered)&lt;/h2&gt;
&lt;p&gt;Heroku’s free tier wasn’t simply cheaper hosting; it was an ecosystem trigger. It made it normal for developers to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;prototype in public,&lt;/li&gt;
&lt;li&gt;ship side projects that needed a real domain and SSL,&lt;/li&gt;
&lt;li&gt;test integrations end-to-end without fiddling with local tunnels,&lt;/li&gt;
&lt;li&gt;and share reproducible deployments with friends or teammates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you can reliably run your app in production-like conditions, your development process changes. You stop treating deployment as a separate discipline and start treating it as part of the build loop.&lt;/p&gt;
&lt;p&gt;So when the free tier ended, the impact was immediate and cultural. People had apps that were “free enough” to keep running. Those apps didn’t disappear magically—they just stopped working the way their authors planned. That forced a new conversation: &lt;em&gt;Where do we run small apps now, without waking up to surprise bills or downtime?&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="the-great-migration-render-railway-flyio-and-coolify"&gt;The Great Migration: Render, Railway, Fly.io, and Coolify&lt;/h2&gt;
&lt;p&gt;The post-Heroku scramble looked chaotic from the outside, but it was actually a series of clear design responses to the same anxiety: “Don’t make me fight infrastructure.”&lt;/p&gt;
&lt;h3 id="railway-monorepos-fast-iteration-and-sensible-defaults"&gt;Railway: Monorepos, fast iteration, and sensible defaults&lt;/h3&gt;
&lt;p&gt;Railway’s pitch is straightforward: keep deployments simple, and play well with real codebases. In practice, that often means better support for modern repo layouts than older “single app” mental models. If you’re working with a monorepo—say, a Next.js frontend plus a Node API plus background workers—Railway’s workflows can feel less like a contortion and more like a deployment system.&lt;/p&gt;
&lt;p&gt;A practical example: if you maintain a monorepo with a shared package and multiple services, you want a platform that won’t punish you for not fitting into a single-project mold. Railway’s approach tends to treat “application” as something broader than “one web dyno.”&lt;/p&gt;
&lt;h3 id="render-a-free-tier-with-real-world-expectations-including-ssl"&gt;Render: A free tier with “real-world” expectations (including SSL)&lt;/h3&gt;
&lt;p&gt;Render’s big differentiator in this era is how naturally it maps to production expectations. For many indie developers, “free hosting” that doesn’t come with SSL or a smooth domain story isn’t actually usable. Render helped make the free-tier conversation feel less like a compromise and more like a baseline.&lt;/p&gt;
&lt;p&gt;If you have a SaaS landing page plus a simple API, the ability to put SSL in front of everything without manual ceremony matters. You want to test payment webhooks, OAuth callbacks, and secure fetch calls without building your own deployment scaffolding.&lt;/p&gt;
&lt;h3 id="flyio-edge-deployment-and-the-promise-of-closer-to-users"&gt;Fly.io: Edge deployment and the promise of “closer to users”&lt;/h3&gt;
&lt;p&gt;Fly.io went hard on a different idea: compute near your users rather than funneling everyone through a single region. That can be more than marketing. If you’re serving a global audience—or your app latency is noticeable—you can architect deployments that reduce “distance tax.”&lt;/p&gt;
&lt;p&gt;A concrete use case: a chat app for users in different continents often feels different when the server is region-aware. Fly.io’s model encourages thinking in those terms, even if you start small.&lt;/p&gt;
&lt;h3 id="coolify-bring-paas-thinking-into-your-own-infrastructure"&gt;Coolify: Bring PaaS thinking into your own infrastructure&lt;/h3&gt;
&lt;p&gt;Coolify represents a more pragmatic mood shift: “If platforms are a business, their guarantees are business terms. Let’s reduce the blast radius.” Coolify lets you get PaaS-style workflows—apps, builds, environments—on your own server(s).&lt;/p&gt;
&lt;p&gt;That’s not for everyone, but it’s a critical counterpoint. Sometimes the best way to stop being surprised is to stop outsourcing the entire control plane.&lt;/p&gt;
&lt;h2 id="the-hidden-contract-you-were-ignoring-free-tiers-are-not-promises"&gt;The Hidden Contract You Were Ignoring: Free Tiers Are Not Promises&lt;/h2&gt;
&lt;p&gt;The Heroku chapter exposed a basic contract assumption many developers didn’t realize they were making:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Free” isn’t a right; it’s a lever.&lt;/li&gt;
&lt;li&gt;Pricing changes don’t require your permission.&lt;/li&gt;
&lt;li&gt;Ecosystems rebalance quickly when the economics shift.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when a provider doesn’t intend to break community trust, it only takes one internal decision—cost structure changes, growth strategy, or revenue pressure—to flip the game. In other words: the free tier is a convenience, not infrastructure.&lt;/p&gt;
&lt;p&gt;So the question becomes: &lt;strong&gt;How do you design your deployment strategy so pricing shocks don’t derail your project?&lt;/strong&gt; The answer is not “pick the best PaaS.” The answer is “build portability and reduce lock-in.”&lt;/p&gt;
&lt;h2 id="what-better-alternatives-teach-us-about-future-developer-paas"&gt;What Better Alternatives Teach Us About Future Developer PaaS&lt;/h2&gt;
&lt;p&gt;The new generation of developer PaaS isn’t just “Heroku but cheaper.” It reflects different philosophies about what matters for real builders.&lt;/p&gt;
&lt;h3 id="1-developer-experience-is-a-technical-feature"&gt;1) Developer experience is a technical feature&lt;/h3&gt;
&lt;p&gt;“Easy deployment” is not fluff. It directly affects iteration speed, debugging time, and how often you can test changes safely. Platforms that reduce steps—connect repository, set env vars, deploy, get logs—effectively shorten the time between idea and feedback.&lt;/p&gt;
&lt;p&gt;If your chosen PaaS makes every small change feel expensive or risky, you’ll develop workarounds. Those workarounds become technical debt.&lt;/p&gt;
&lt;h3 id="2-modern-repo-realities-are-no-longer-optional"&gt;2) Modern repo realities are no longer optional&lt;/h3&gt;
&lt;p&gt;Monorepos, background workers, preview environments, and multiple services aren’t edge cases. If a platform makes these awkward, you’ll pay later.&lt;/p&gt;
&lt;p&gt;Railway’s monorepo-friendly posture and Fly.io’s deployment flexibility are both ways of acknowledging this reality. The best platforms don’t ask you to contort your project into their favorite shapes.&lt;/p&gt;
&lt;h3 id="3-production-features-on-free-tiers-matter"&gt;3) “Production features” on free tiers matter&lt;/h3&gt;
&lt;p&gt;An SSL-enabled domain isn’t just a checkbox. It affects authentication flows, secure cookies, webhook verification, and even basic browser behavior. When providers offer these capabilities in their free tier (or at least make them painless), developers can keep projects alive in a way that feels legitimate, not improvised.&lt;/p&gt;
&lt;p&gt;Render’s emphasis here hits the practical nerve: if your app is public, users expect secure access without extra steps.&lt;/p&gt;
&lt;h3 id="4-edge-deployment-is-an-architectural-advantage-not-a-luxury"&gt;4) Edge deployment is an architectural advantage, not a luxury&lt;/h3&gt;
&lt;p&gt;Fly.io’s approach pushes you toward thinking about placement and latency. Even if you start with a single region, the mental model encourages better performance hygiene as your app grows.&lt;/p&gt;
&lt;p&gt;The win isn’t only speed; it’s a cleaner path to scaling responsibly.&lt;/p&gt;
&lt;h3 id="5-you-need-an-escape-hatch"&gt;5) You need an escape hatch&lt;/h3&gt;
&lt;p&gt;Whether your fallback is another PaaS or your own orchestrator, portability is insurance. Containerization and clear infrastructure boundaries are the antidote to “platform surprise.”&lt;/p&gt;
&lt;p&gt;In practice, this means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep build steps reproducible (Dockerfile or equivalent),&lt;/li&gt;
&lt;li&gt;avoid deep platform-specific APIs unless absolutely necessary,&lt;/li&gt;
&lt;li&gt;store secrets and configuration in a way you can rehydrate elsewhere,&lt;/li&gt;
&lt;li&gt;and document how you deploy.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-playbook-for-post-heroku-decisions-so-you-dont-get-bit-again"&gt;A Playbook for Post-Heroku Decisions (So You Don’t Get Bit Again)&lt;/h2&gt;
&lt;p&gt;If you’re picking a PaaS today, don’t start with “What’s the free tier?” Start with “How do I keep moving if the floor disappears?”&lt;/p&gt;
&lt;p&gt;Here’s a pragmatic checklist:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Define your portability level.&lt;/strong&gt;&lt;br&gt;
Are you okay re-deploying your app to another provider in a day? Or would you rather migrate in an hour? Your answer determines how much platform-specific wiring you should accept.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Choose the platform that matches your architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you want fewer deployment headaches for complex repos, lean toward providers that treat monorepos as normal (Railway-style thinking).&lt;/li&gt;
&lt;li&gt;If you want a clean path to public apps with domains and SSL, prioritize providers that make “public by default” easy (Render-style).&lt;/li&gt;
&lt;li&gt;If latency and global footprint matter, consider edge-first platforms (Fly.io’s model).&lt;/li&gt;
&lt;li&gt;If you want control and resilience, consider a self-hosted PaaS layer (Coolify-style).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Treat free hosting like a development environment, not a business guarantee.&lt;/strong&gt;&lt;br&gt;
You can absolutely run a side project on a free tier—but build the habit of having an upgrade path. Set budget alerts where possible. Even a small monthly cost is often cheaper than rebuilding your deployment pipeline under stress.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automate the boring parts.&lt;/strong&gt;&lt;br&gt;
If you can re-deploy with one command and one documented process, the platform choice matters less. Automation turns platform churn into a routine event.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Plan for state.&lt;/strong&gt;&lt;br&gt;
Databases, queues, and file storage are where migrations hurt. Keep state external to your web app when you can, and understand how each platform backs services.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="conclusion-the-paas-future-is-betterif-you-design-for-it"&gt;Conclusion: The PaaS Future Is Better—If You Design for It&lt;/h2&gt;
&lt;p&gt;Heroku’s free tier ending didn’t just change where developers deploy. It forced the community to confront a long-ignored truth: PaaS convenience is conditional on provider economics. But the aftermath also proved something encouraging: the ecosystem didn’t collapse—it evolved.&lt;/p&gt;
&lt;p&gt;Fly.io’s edge mindset, Railway’s monorepo-friendly workflows, and Render’s “actually usable” free tier with SSL collectively make post-Heroku life genuinely pleasant for many builders. Just don’t confuse “great alternatives” with “permanent assurances.” Build portability, keep deployment reproducible, and treat any free tier as a temporary advantage—not a foundation.&lt;/p&gt;</content></item><item><title>ChatGPT Is a Parlor Trick That Will Reshape Software Engineering</title><link>https://decastro.work/blog/chatgpt-parlor-trick-reshape-software-engineering/</link><pubDate>Wed, 04 Jan 2023 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/chatgpt-parlor-trick-reshape-software-engineering/</guid><description>&lt;p&gt;ChatGPT is the most theatrical thing to happen to software engineering in years: it talks like a genius, sometimes writes like a beginner, and occasionally manufactures facts out of pure confidence. If you’re unimpressed, you’re not wrong. If you’re dismissing it, you’re also probably wrong. The real story isn’t whether it’s “correct” today—it’s the direction it’s taking your job, your tools, and your expectations of how software gets built.&lt;/p&gt;
&lt;h2 id="the-parlor-trick-fluent-nonsense-that-feels-useful"&gt;The parlor trick: fluent nonsense that feels useful&lt;/h2&gt;
&lt;p&gt;Let’s start with the obvious. Large language models can hallucinate. They can produce code that fails tests. They can explain a concept accurately in a way that still doesn’t help you implement it. They can present uncertainty as certainty, which is a special kind of danger in engineering.&lt;/p&gt;</description><content>&lt;p&gt;ChatGPT is the most theatrical thing to happen to software engineering in years: it talks like a genius, sometimes writes like a beginner, and occasionally manufactures facts out of pure confidence. If you’re unimpressed, you’re not wrong. If you’re dismissing it, you’re also probably wrong. The real story isn’t whether it’s “correct” today—it’s the direction it’s taking your job, your tools, and your expectations of how software gets built.&lt;/p&gt;
&lt;h2 id="the-parlor-trick-fluent-nonsense-that-feels-useful"&gt;The parlor trick: fluent nonsense that feels useful&lt;/h2&gt;
&lt;p&gt;Let’s start with the obvious. Large language models can hallucinate. They can produce code that fails tests. They can explain a concept accurately in a way that still doesn’t help you implement it. They can present uncertainty as certainty, which is a special kind of danger in engineering.&lt;/p&gt;
&lt;p&gt;But here’s what’s easy to miss: a parlor trick still works if it moves attention. ChatGPT doesn’t just answer questions—it compresses an ocean of patterns into a conversational interface. That interface changes how developers &lt;em&gt;feel&lt;/em&gt; about asking for help.&lt;/p&gt;
&lt;p&gt;Consider a real workflow. You’re stuck debugging a flaky integration test. You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search docs for an hour and still come up empty.&lt;/li&gt;
&lt;li&gt;Or paste the error log, the relevant code snippet, and your hypothesis into ChatGPT and get back a structured set of suspects: race conditions, test isolation, network retries, timeouts, mocking mismatches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sometimes the model is wrong. But the first pass is often good enough to narrow the search space dramatically. That’s the trick: not magic correctness, but fast guidance.&lt;/p&gt;
&lt;p&gt;The “today” version matters less than the “tomorrow” direction—because the interface is becoming the new terminal prompt, the new ticket comment, the new design sketch.&lt;/p&gt;
&lt;h2 id="the-underestimation-problem-developers-are-judging-the-tool-not-the-trajectory"&gt;The underestimation problem: developers are judging the tool, not the trajectory&lt;/h2&gt;
&lt;p&gt;Most developers are still thinking in the wrong frame. They’re asking questions like: “Can it replace programmers?” or “How often does it hallucinate?” Those are understandable, but they’re also the wrong scoreboard.&lt;/p&gt;
&lt;p&gt;Software engineering is not a static craft; it’s a compounding system of feedback loops:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You write something.&lt;/li&gt;
&lt;li&gt;You test it.&lt;/li&gt;
&lt;li&gt;You observe the failure mode.&lt;/li&gt;
&lt;li&gt;You refine the design.&lt;/li&gt;
&lt;li&gt;You automate what you learned.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LLMs slot into that loop as a new kind of pre-processing layer for thinking. The models don’t have to be perfect to shift the system. They only have to be &lt;em&gt;useful enough&lt;/em&gt; to reduce the cost of iteration.&lt;/p&gt;
&lt;p&gt;And iteration is where software gets transformed. Even a modest improvement in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how quickly you draft a candidate solution,&lt;/li&gt;
&lt;li&gt;how quickly you translate requirements into concrete code,&lt;/li&gt;
&lt;li&gt;how quickly you locate likely root causes,
multiplies across every feature, every incident, and every onboarding cycle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The frightening part is not that the model is already “great.” It’s that engineering teams will normalize using it in the middle of the workflow—until they can’t imagine working without it.&lt;/p&gt;
&lt;h2 id="llms-arent-code-generatorstheyre-thinking-partners-or-they-fail-loudly"&gt;LLMs aren’t code generators—they’re thinking partners (or they fail loudly)&lt;/h2&gt;
&lt;p&gt;The biggest practical shift is this: stop treating ChatGPT like a machine that outputs working software. Treat it like a thinking partner with a strong autocomplete brain and a weak truth sense.&lt;/p&gt;
&lt;p&gt;That means you should engineer prompts the way you engineer systems: with constraints, inputs, and feedback.&lt;/p&gt;
&lt;h3 id="do-this-give-context--ask-for-plans-not-just-outputs"&gt;Do this: give context + ask for plans, not just outputs&lt;/h3&gt;
&lt;p&gt;Instead of “Write me an API client,” try:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Here are the constraints: authentication uses rotating tokens, requests must be idempotent, timeouts are 2s, and the server returns 429 with a Retry-After header. Propose an implementation plan first, then code.”&lt;/li&gt;
&lt;li&gt;“Given this TypeScript interface and this example response payload, generate parsing logic and explain edge cases that could break it.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re forcing the model to behave like a collaborator—planning before executing—reducing the odds of confident nonsense.&lt;/p&gt;
&lt;h3 id="do-this-demand-test-scaffolding-and-failure-modes"&gt;Do this: demand test scaffolding and failure modes&lt;/h3&gt;
&lt;p&gt;A productive question looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Write unit tests for the parsing logic, including malformed inputs and boundary cases. Then show how you’d validate the retry behavior under 429.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is that you’re not trying to get “correct code on the first try.” You’re trying to accelerate &lt;em&gt;the path to evidence&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="dont-do-this-paste-a-vague-task-and-accept-the-answer-unverified"&gt;Don’t do this: paste a vague task and accept the answer unverified&lt;/h3&gt;
&lt;p&gt;If you ask for “a caching layer,” you will get a story. If you ask for “a caching layer that avoids stampedes using request coalescing, supports TTL invalidation, and includes an integration test strategy,” you will get engineering artifacts you can actually evaluate.&lt;/p&gt;
&lt;p&gt;In practice: the model will still be wrong sometimes. But wrongness becomes manageable when you’re asking for structure, assumptions, and tests—not miracles.&lt;/p&gt;
&lt;h2 id="the-real-engineering-change-from-building-code-to-orchestrating-feedback"&gt;The real engineering change: from building code to orchestrating feedback&lt;/h2&gt;
&lt;p&gt;When LLMs become normal, the unit of work changes.&lt;/p&gt;
&lt;p&gt;Traditionally, you “build code.” With LLMs, you “orchestrate refinement.” The most valuable developers won’t be the ones who can prompt the slickest one-liner—they’ll be the ones who can set up evaluation quickly and relentlessly.&lt;/p&gt;
&lt;p&gt;Here’s how that looks in day-to-day engineering:&lt;/p&gt;
&lt;h3 id="1-faster-drafts-but-with-guardrails"&gt;1) Faster drafts, but with guardrails&lt;/h3&gt;
&lt;p&gt;LLMs can draft:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;endpoints,&lt;/li&gt;
&lt;li&gt;serializers,&lt;/li&gt;
&lt;li&gt;database queries,&lt;/li&gt;
&lt;li&gt;migration scripts,&lt;/li&gt;
&lt;li&gt;documentation,&lt;/li&gt;
&lt;li&gt;error-handling patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the winning teams wrap those drafts in guardrails: type checks, linting rules, unit tests, property-based tests where appropriate, and review checklists that explicitly look for common failure modes.&lt;/p&gt;
&lt;h3 id="2-review-becomes-prove-it-instead-of-read-it"&gt;2) Review becomes “prove it” instead of “read it”&lt;/h3&gt;
&lt;p&gt;Code review will shift from “does this look sensible?” to “show me the evidence.” Expect more pull requests that include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;test plans,&lt;/li&gt;
&lt;li&gt;rationale for tradeoffs,&lt;/li&gt;
&lt;li&gt;links between requirements and implementation,&lt;/li&gt;
&lt;li&gt;explicit handling of edge cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if the LLM wrote half the code, the human’s responsibility becomes sharper: validating behavior and aligning with system constraints.&lt;/p&gt;
&lt;h3 id="3-debugging-turns-into-interactive-diagnosis"&gt;3) Debugging turns into interactive diagnosis&lt;/h3&gt;
&lt;p&gt;In incidents, time is everything. Teams will start using LLMs to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;summarize logs and trace patterns,&lt;/li&gt;
&lt;li&gt;propose hypotheses in priority order,&lt;/li&gt;
&lt;li&gt;generate targeted reproduction steps,&lt;/li&gt;
&lt;li&gt;draft remediation PRs that include tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This won’t eliminate debugging. It will change how quickly you get to the right questions—and how often you stop flailing.&lt;/p&gt;
&lt;h2 id="who-will-thrive-the-engineers-who-treat-ai-like-a-new-layer-of-tooling"&gt;Who will thrive: the engineers who treat AI like a new layer of tooling&lt;/h2&gt;
&lt;p&gt;The developers who will thrive are not necessarily the most enthusiastic ones. They’re the ones with the right mental model and the right habits.&lt;/p&gt;
&lt;h3 id="thrivers-will"&gt;Thrivers will:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Demand plans and tests&lt;/strong&gt;, not just final answers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the model to accelerate exploration&lt;/strong&gt;, then validate via tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translate requirements into concrete constraints&lt;/strong&gt; (timeouts, retries, idempotency, security boundaries).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Know where hallucinations are likely&lt;/strong&gt; and design prompts to reduce that risk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build reusable prompt patterns&lt;/strong&gt; for their stack (e.g., “generate migrations with rollback,” “handle pagination with cursor semantics,” “never invent env vars—ask for them”).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="strugglers-will"&gt;Strugglers will:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dismiss AI entirely&lt;/strong&gt; and keep paying the “blank screen” cost when stuck.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trust blindly&lt;/strong&gt; and ship untested behavior because the output sounded plausible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat prompts like magic incantations&lt;/strong&gt; rather than engineering inputs that require iteration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The difference isn’t talent. It’s discipline.&lt;/p&gt;
&lt;p&gt;A concrete example: onboarding.
With LLMs, you can accelerate “how does this codebase do X?” But the thriving approach is to ask for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an explanation of architecture,&lt;/li&gt;
&lt;li&gt;a walkthrough of a representative module,&lt;/li&gt;
&lt;li&gt;a list of relevant entry points,&lt;/li&gt;
&lt;li&gt;and a suggestion for a small “first contribution” task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The failing approach is to ask for “an overview,” accept it, and then get blindsided by the real design constraints that the model never saw.&lt;/p&gt;
&lt;h2 id="the-uncomfortable-truth-hallucinations-force-better-engineering-not-weaker-it"&gt;The uncomfortable truth: hallucinations force better engineering, not weaker it&lt;/h2&gt;
&lt;p&gt;Yes, LLMs hallucinate. Yes, they can produce buggy code. And if you treat that as a reason to stop using them, you’ll miss the point.&lt;/p&gt;
&lt;p&gt;Hallucinations are a forcing function. They push you toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tighter feedback loops,&lt;/li&gt;
&lt;li&gt;stronger test culture,&lt;/li&gt;
&lt;li&gt;clearer requirements,&lt;/li&gt;
&lt;li&gt;explicit contracts between components,&lt;/li&gt;
&lt;li&gt;and better separation between “draft” and “verified.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, the risk can make the process better—if your team responds with engineering rigor rather than wishful thinking. The model’s confidence becomes a prompt for you to add structure: validations, tests, and operational checks.&lt;/p&gt;
&lt;p&gt;This is what makes the transformation “terrifying” in a productive way. Once engineers internalize that LLMs are unreliable narrators but powerful accelerators, software development becomes more experimental, more iterative, and less gated by how long you can stare at a problem.&lt;/p&gt;
&lt;p&gt;That’s a structural shift.&lt;/p&gt;
&lt;h2 id="conclusion-the-parlor-trick-becomes-the-toolchain"&gt;Conclusion: the parlor trick becomes the toolchain&lt;/h2&gt;
&lt;p&gt;ChatGPT can be a parlor trick—confident nonsense dressed in helpful language. But the parlor trick is exactly how it rewires engineering: it makes collaboration feel instantaneous, it shrinks the time between questions and candidate solutions, and it turns iteration into the default mode.&lt;/p&gt;
&lt;p&gt;The winners won’t be the ones who worship the model or ignore it. They’ll be the ones who treat LLMs as thinking partners inside a disciplined system of verification. If you build that muscle now—plans, constraints, tests, evidence—you won’t just adapt to the change. You’ll outpace the people still arguing about whether it’s “real.”&lt;/p&gt;</content></item><item><title>Five Developer Tools I Can't Live Without in 2022</title><link>https://decastro.work/blog/five-developer-tools-cant-live-without-2022/</link><pubDate>Thu, 22 Dec 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/five-developer-tools-cant-live-without-2022/</guid><description>&lt;p&gt;Most developer “productivity advice” sounds like it’s trying to sell you a new framework. I’ve stopped chasing shiny. The tools that actually make me faster are quieter—little interfaces that remove friction from work I do constantly. After a year of experimenting, these are the five developer tools I’d miss first. Not because they’re glamorous, but because they compound: shave seconds off every task, and you magically earn back hours by the end of the month.&lt;/p&gt;</description><content>&lt;p&gt;Most developer “productivity advice” sounds like it’s trying to sell you a new framework. I’ve stopped chasing shiny. The tools that actually make me faster are quieter—little interfaces that remove friction from work I do constantly. After a year of experimenting, these are the five developer tools I’d miss first. Not because they’re glamorous, but because they compound: shave seconds off every task, and you magically earn back hours by the end of the month.&lt;/p&gt;
&lt;h2 id="warp-terminal-ai-command-search-that-actually-feels-instant"&gt;Warp Terminal: AI command search that actually feels instant&lt;/h2&gt;
&lt;p&gt;I don’t use terminal menus. I use muscle memory—until I don’t. Warp Terminal fixed that gap with AI-powered command search, which is especially useful when you’re trying to remember &lt;em&gt;the exact incantation&lt;/em&gt; you ran yesterday.&lt;/p&gt;
&lt;p&gt;Here’s what this looks like in real life: I’ll be half-thinking about a task—“I need to tail logs for that service”—but I’m not sure which command I used last time. Instead of fumbling through history or scanning docs, I type a partial intent and Warp narrows down the candidates. It’s not magic; it’s fast retrieval. And fast retrieval beats slow recall.&lt;/p&gt;
&lt;p&gt;Practical ways to lean into it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Search by purpose, not by command.&lt;/strong&gt; If you’re working with Docker, Kubernetes, or a custom CLI, use natural language like “restart api container” or “stream logs web worker.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep your history clean enough to matter.&lt;/strong&gt; If you run scripts that generate noise, consider aliases or wrapper scripts so your “searchable” history remains useful.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use it as a bridge, not a crutch.&lt;/strong&gt; I still memorize frequent commands, but Warp handles the long tail—the ones you only run occasionally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Warp also improved my baseline terminal experience: speed, keyboard navigation, and a UI that encourages you to iterate rather than hesitate. In practice, it reduces the “I should look this up” moments that quietly steal focus.&lt;/p&gt;
&lt;h2 id="raycast-the-spotlight-replacement-that-understands-developers"&gt;Raycast: the Spotlight replacement that understands developers&lt;/h2&gt;
&lt;p&gt;If Warp makes the command line easier to navigate, Raycast makes the computer itself easier to navigate. For me, it effectively replaced macOS Spotlight as the default “search and launch” surface—except it’s faster, more configurable, and built for repeatable workflows.&lt;/p&gt;
&lt;p&gt;The difference is subtle: Spotlight is great for finding files and launching apps. Raycast is great for doing tasks. That means you don’t just open the thing—you get the right action immediately.&lt;/p&gt;
&lt;p&gt;Concrete examples that show why I kept it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Jumping between projects.&lt;/strong&gt; Instead of digging through folders, I hit Raycast and type the project name. It opens the right directory, sometimes with a command to start the dev server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clipboard and snippet workflows.&lt;/strong&gt; Copy something in one app and insert it into another without manually juggling context. If you do any kind of copy/paste-heavy work (migrations, config updates, API payloads), Raycast pays for itself quickly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Command palettes for dev chores.&lt;/strong&gt; You can build or install extensions that perform actions like “create branch,” “run test,” “open logs,” or “switch environments.” The win isn’t that it’s novel—it’s that it removes steps you repeat daily.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My rule: if I catch myself switching windows, hunting through menus, or retyping routine actions more than twice in a day, Raycast is where I fix it. I treat it like part of my workflow infrastructure, not a “nice-to-have launcher.”&lt;/p&gt;
&lt;h2 id="fig-shell-autocomplete-that-turns-typing-into-intention"&gt;Fig: shell autocomplete that turns typing into intention&lt;/h2&gt;
&lt;p&gt;Autocompletion is one of those benefits that’s easy to underestimate until you use it for a week. Fig extends your terminal experience with smarter autocomplete, which helps when your CLI ecosystem is large: multiple package managers, internal commands, and a web of scripts that never quite share the same flags.&lt;/p&gt;
&lt;p&gt;The best part is that autocomplete isn’t just about saving keystrokes. It’s about preventing mistakes. When you’re halfway through a command and you realize you forgot a flag—or you used a wrong argument—you can waste minutes rerunning. Fig reduces that by making the likely next tokens visible.&lt;/p&gt;
&lt;p&gt;How I use it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Autocomplete for flags and subcommands.&lt;/strong&gt; If I’m working with tools like git, docker, or a framework CLI, Fig makes it easy to discover valid options without memorizing syntax.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tab-completion for “where did this live again?” moments.&lt;/strong&gt; When you have scripts across repos, Fig helps you navigate the landscape faster than searching by filesystem paths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed during refactors.&lt;/strong&gt; Renaming commands, changing environments, or swapping parameters becomes less scary because the shell can guide you as you type.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you already have a great terminal workflow, you might think Fig is redundant. It isn’t. It’s the layer between “I know what I want” and “I can’t remember the exact text.” That layer saves time &lt;em&gt;and&lt;/em&gt; lowers mental load.&lt;/p&gt;
&lt;h2 id="tableplus-database-work-that-doesnt-feel-like-punishment"&gt;TablePlus: database work that doesn’t feel like punishment&lt;/h2&gt;
&lt;p&gt;Databases are where productivity goes to die—unless your tool gets out of the way. I use TablePlus for everything from quick inspections to more involved admin tasks. The key feature isn’t just that it works; it’s that it makes database work &lt;em&gt;comfortable&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When I’m debugging, I want two things immediately:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fast visibility&lt;/strong&gt; into what’s happening (tables, rows, indexes, schema).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-friction editing&lt;/strong&gt; (writing queries, running them, iterating, copying results).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;TablePlus nails the loop: open connection → inspect schema → write query → run → adjust → repeat. The UI matters because database work is iterative by nature. When the tool is clunky, iteration slows down. When it’s smooth, iteration becomes normal.&lt;/p&gt;
&lt;p&gt;A few practical workflows I rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inspect schema without tab-hopping.&lt;/strong&gt; I can look up columns and constraints without bouncing between a migration file, an admin UI, and documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query iteration with results in view.&lt;/strong&gt; I often build queries step-by-step. Being able to tweak and re-run with minimal friction reduces the “SQL downtime tax.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export and share results quickly.&lt;/strong&gt; When debugging with teammates, sending the right snippet matters. TablePlus makes it easier to extract exactly what someone needs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a surprising number of developer tasks, the database is the source of truth—and TablePlus turns the source of truth into something you can access without suffering.&lt;/p&gt;
&lt;h2 id="obsidian-the-documentation-system-that-stays-out-of-your-way"&gt;Obsidian: the documentation system that stays out of your way&lt;/h2&gt;
&lt;p&gt;Codebases change. Your memory won’t. Obsidian is my technical documentation “home base” because it’s flexible enough to capture messy reality: notes, snippets, decisions, postmortems, and links between them. It’s not a formal doc platform. It’s better than that—it’s a living workspace.&lt;/p&gt;
&lt;p&gt;I use it for exactly the kind of documentation teams avoid because it’s “not urgent.” That’s where Obsidian wins: it makes documentation feel lightweight and immediate, so you actually keep it current.&lt;/p&gt;
&lt;p&gt;Examples of notes I maintain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Runbooks for recurring problems.&lt;/strong&gt; “How to reset X” or “What to check when Y fails” in a format I can search in minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project decision logs.&lt;/strong&gt; When we choose a library, change an architecture, or decide not to adopt something, I record the reasoning. Months later, I can answer “why” without guessing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snippets with context.&lt;/strong&gt; Not just code blocks—include where it came from and when to use it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What makes it effective is the compound behavior: once you’ve connected related notes, you stop re-explaining the same concepts. Search becomes meaningful, not just a pile of text. And when you’re onboarding or debugging at 2 a.m., that “connected knowledge” matters more than any polished markdown page.&lt;/p&gt;
&lt;p&gt;My approach is simple: if I fix something that future-me will have to do again, I write down the fix while it’s fresh. Obsidian makes that habit practical.&lt;/p&gt;
&lt;h2 id="the-real-lesson-invest-in-the-local-dev-environment-before-your-stack"&gt;The real lesson: invest in the local dev environment before your stack&lt;/h2&gt;
&lt;p&gt;None of these tools are frameworks or languages. That’s the point. They’re productivity multipliers in the most boring place possible: your local workflow. They reduce friction around the activities you repeat constantly—typing commands, switching contexts, querying databases, and finding answers later.&lt;/p&gt;
&lt;p&gt;In my experience, “stack investing” is easy to justify because it’s visible: new framework, new library, new architecture. But the daily time sink is usually the environment around the stack—how quickly you can operate it, navigate it, and recover from mistakes.&lt;/p&gt;
&lt;p&gt;If you want a practical starting point, do this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pick one bottleneck you hit daily (command recall, app switching, DB debugging, or documentation retrieval).&lt;/li&gt;
&lt;li&gt;Trial one tool for a week.&lt;/li&gt;
&lt;li&gt;Measure the time you spend on the annoyance, not the time you spend admiring the feature.&lt;/li&gt;
&lt;li&gt;Keep the tool only if it becomes part of your default workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because the truth is, speed doesn’t come from doing more. It comes from doing less of what slows you down.&lt;/p&gt;
&lt;h2 id="conclusion-compound-speed-beats-occasional-heroics"&gt;Conclusion: compound speed beats occasional heroics&lt;/h2&gt;
&lt;p&gt;Warp, Raycast, Fig, TablePlus, and Obsidian aren’t flashy. They don’t rewrite your architecture. They just make routine work smoother—and that’s exactly what turns small improvements into hours over time. If you want to get faster in 2022, start by fixing your local environment. That’s where productivity quietly lives, and where it compounds.&lt;/p&gt;</content></item><item><title>The Year JavaScript Grew Up: 2022 in Review</title><link>https://decastro.work/blog/year-javascript-grew-up-2022-review/</link><pubDate>Sat, 10 Dec 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/year-javascript-grew-up-2022-review/</guid><description>&lt;p&gt;In most technology stories, “progress” looks like novelty sprinting ahead of reality. In 2022, the JavaScript ecosystem did something rarer: it started treating stability like a feature, not a tax. The result wasn’t just new tools—it was a shift in what people &lt;em&gt;expect&lt;/em&gt; their tools to do well.&lt;/p&gt;
&lt;h2 id="typescript-crossed-the-tipping-pointbecause-teams-got-tired-of-guessing"&gt;TypeScript crossed the tipping point—because teams got tired of guessing&lt;/h2&gt;
&lt;p&gt;TypeScript didn’t “arrive” in 2022; it matured. The meaningful change was cultural: type safety moved from “nice-to-have” to the default expectation for serious codebases.&lt;/p&gt;</description><content>&lt;p&gt;In most technology stories, “progress” looks like novelty sprinting ahead of reality. In 2022, the JavaScript ecosystem did something rarer: it started treating stability like a feature, not a tax. The result wasn’t just new tools—it was a shift in what people &lt;em&gt;expect&lt;/em&gt; their tools to do well.&lt;/p&gt;
&lt;h2 id="typescript-crossed-the-tipping-pointbecause-teams-got-tired-of-guessing"&gt;TypeScript crossed the tipping point—because teams got tired of guessing&lt;/h2&gt;
&lt;p&gt;TypeScript didn’t “arrive” in 2022; it matured. The meaningful change was cultural: type safety moved from “nice-to-have” to the default expectation for serious codebases.&lt;/p&gt;
&lt;p&gt;You can see it in the day-to-day decisions teams made:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontends stopped treating types as optional ceremony.&lt;/strong&gt; If a refactor could silently break production, teams increasingly demanded compile-time protection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIs became self-documenting.&lt;/strong&gt; When you ship a typed client or a typed backend boundary, you reduce the amount of tribal knowledge required to use the system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling improved enough that types stopped feeling punitive.&lt;/strong&gt; Editors started catching issues immediately; modern TypeScript workflows made incremental adoption less painful.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical example: imagine a React app with dozens of API calls. Without types, you eventually end up with patterns like “assume &lt;code&gt;response.data.user&lt;/code&gt; exists.” With TypeScript, you instead model the response shape—so missing fields become a build-time problem. That’s not theoretical. It changes how fast teams can refactor safely.&lt;/p&gt;
&lt;p&gt;My opinionated take: the “tipping point” wasn’t just people choosing TypeScript. It was people choosing &lt;strong&gt;predictability&lt;/strong&gt;. Once you’ve been burned by a runtime shape mismatch, the value of types becomes obvious.&lt;/p&gt;
&lt;h2 id="bun-showed-that-runtimes-are-still-design-spacenot-sacred-history"&gt;Bun showed that runtimes are still design space—not sacred history&lt;/h2&gt;
&lt;p&gt;Bun arriving in 2022 reminded everyone that JavaScript tooling isn’t frozen in time. A runtime isn’t merely an engine—it’s an integrated product: package management, bundling/transform behavior, developer experience, startup performance, and the ergonomics of everyday commands.&lt;/p&gt;
&lt;p&gt;The key idea wasn’t “Bun is faster” (speed is easy to market and hard to reason about). The key idea was that the runtime layer is still something developers can rethink:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Friction matters.&lt;/strong&gt; If your &lt;code&gt;node&lt;/code&gt;-based workflow requires extra steps or inconsistent behavior across tools, teams feel it daily.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A runtime can offer a more coherent toolchain.&lt;/strong&gt; When install, run, and script execution behave consistently, the environment stops being a source of bugs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practical terms, Bun’s biggest impact for many teams was exploratory. They started asking new questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can we standardize how scripts run across the monorepo?&lt;/li&gt;
&lt;li&gt;Can we simplify dev startup without sacrificing compatibility?&lt;/li&gt;
&lt;li&gt;Can we reduce the “glue code” between package manager, runtime, and build tool?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even when teams didn’t switch their production workloads immediately, Bun shifted the conversation. It made it easier to believe that the ecosystem could improve the foundations instead of only adding more wrappers around the same assumptions.&lt;/p&gt;
&lt;h2 id="vite-consolidated-the-build-tool-landscapeless-ceremony-fewer-moving-parts"&gt;Vite consolidated the build tool landscape—less ceremony, fewer moving parts&lt;/h2&gt;
&lt;p&gt;If TypeScript was about correctness and Bun was about reimagining runtime ergonomics, Vite was about something quieter: reducing the cognitive load of building frontend applications.&lt;/p&gt;
&lt;p&gt;Vite’s value wasn’t just that it was fast. It also pushed teams toward a build setup that feels closer to how modern development should work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Development and production behave consistently&lt;/strong&gt;, with fewer “works in dev, breaks in prod” mysteries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The default path became usable.&lt;/strong&gt; Teams weren’t required to assemble a complicated stack just to get started.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot reload and module handling felt more direct&lt;/strong&gt;, which reduces the number of times developers must mentally model the toolchain.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete scenario: consider a team migrating from a more complex bundler configuration. The old setup might involve many options, custom loaders, and bespoke performance hacks. With Vite, the migration becomes less about rewriting your entire mental model and more about mapping concepts—entry points, assets, environment variables—into a simpler structure.&lt;/p&gt;
&lt;p&gt;Opinionated truth: build tools aren’t where product ideas succeed. They’re where deadlines fail. Vite’s consolidation helped teams spend less time fighting configuration and more time shipping features.&lt;/p&gt;
&lt;h2 id="rust-entered-the-conversation-for-a-reason-teams-wanted-safer-systems-boundaries"&gt;Rust entered the conversation for a reason: teams wanted safer systems boundaries&lt;/h2&gt;
&lt;p&gt;2022 wasn’t a “Rust replaces everything” moment. That story is always too neat. But Rust entered the mainstream developer conversation because JavaScript teams kept running into the limits of the platform when they cared about performance and safety at the boundary.&lt;/p&gt;
&lt;p&gt;The pattern usually looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your Node/JS application needs a high-performance component—parsing, compression, crypto, file processing, or other CPU-heavy tasks.&lt;/li&gt;
&lt;li&gt;Rewriting the entire app in Rust is unrealistic.&lt;/li&gt;
&lt;li&gt;But depending on an unsafe native extension is also risky.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust offers a middle path: it’s suitable for writing reliable system-level components while keeping the surrounding ecosystem intact. Even if a team doesn’t adopt Rust directly, the pressure it adds to the ecosystem changes how people evaluate architectures.&lt;/p&gt;
&lt;p&gt;A practical way to think about it: if your product needs predictable memory behavior, robust concurrency, or hardened parsing, you don’t just want speed—you want &lt;em&gt;correctness under stress&lt;/em&gt;. Rust’s design choices make that conversation feel more grounded.&lt;/p&gt;
&lt;h2 id="docker-cemented-dominance-shipping-became-about-consistency-not-heroics"&gt;Docker cemented dominance: shipping became about consistency, not heroics&lt;/h2&gt;
&lt;p&gt;Docker didn’t become popular in 2022. It became even more essential. The reason is simple: containers made environments reproducible in a way that mattered when teams got larger and deployments got more complex.&lt;/p&gt;
&lt;p&gt;In 2022, the value proposition sharpened:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“Works on my machine” became increasingly indefensible.&lt;/strong&gt; Even small differences in OS libraries, Node versions, or dependency resolution can break builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD pipelines became more standardized.&lt;/strong&gt; Teams could run the same image locally and in production-like environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback and scaling got easier when the artifact is consistent.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical example: if your JavaScript app depends on a specific native module or system library, Docker becomes the difference between smooth onboarding and endless debugging. You bake the environment once, then treat it as an artifact you can reproduce.&lt;/p&gt;
&lt;p&gt;My take: Docker’s real victory is not containers—it’s the operational maturity they enabled. Stability in the environment reduces the surface area for failures that have nothing to do with your code.&lt;/p&gt;
&lt;h2 id="postgresql-kept-conquering-quietlybecause-reliability-is-a-feature"&gt;PostgreSQL kept conquering quietly—because reliability is a feature&lt;/h2&gt;
&lt;p&gt;Some technologies dominate headlines. PostgreSQL dominated production. In 2022, it continued its quiet conquest by being the default choice for teams who cared about correctness, operability, and long-term maintainability.&lt;/p&gt;
&lt;p&gt;Even when new data stores appeared, many teams stuck with PostgreSQL because it offered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A pragmatic balance of features and reliability&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solid tooling and migration patterns&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A mature ecosystem of extensions, integrations, and operational practices&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve run a database in the real world, you know this: the “right” database isn’t the one with the most futuristic options—it’s the one you can trust during incidents, migrations, and scaling events.&lt;/p&gt;
&lt;p&gt;A good rule of thumb for 2022 thinking: if your team is building a business, not a database platform, your data layer shouldn’t be a research project. PostgreSQL let teams focus on product work while still giving them enough power to evolve.&lt;/p&gt;
&lt;h2 id="web3-burned-through-billionsand-the-ecosystem-learned-some-painful-lessons"&gt;Web3 burned through billions—and the ecosystem learned some painful lessons&lt;/h2&gt;
&lt;p&gt;Web3 entered 2022 with momentum and left with skepticism. The industry’s most visible feature wasn’t technical capability—it was overconfidence. Billions spent on promises without proportional delivery trained developers to demand accountability.&lt;/p&gt;
&lt;p&gt;What changed in practice?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Teams became more cautious about timelines.&lt;/strong&gt; “Soon” started meaning “we don’t actually know.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk moved from theoretical to operational.&lt;/strong&gt; Custody, upgrades, smart contract safety, and integration complexity became harder to ignore.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The market for prototypes narrowed.&lt;/strong&gt; Investors and builders started demanding traction, not vibes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The lesson for JavaScript developers is broader than any single sector: novelty can attract attention, but stability earns trust. If a system can’t be operated safely, its theoretical advantages don’t matter during outages.&lt;/p&gt;
&lt;h2 id="conclusion-2022-wasnt-about-new-toysit-was-about-grown-up-expectations"&gt;Conclusion: 2022 wasn’t about new toys—it was about grown-up expectations&lt;/h2&gt;
&lt;p&gt;2022 marked a turning point for JavaScript: TypeScript made correctness mainstream, Bun proved runtimes could be redesigned, and Vite reduced build-time friction. Meanwhile, Rust, Docker, and PostgreSQL reinforced a larger theme—teams increasingly valued reliability at the boundaries: systems interfaces, deployment environments, and data storage.&lt;/p&gt;
&lt;p&gt;The ecosystem didn’t stop innovating. It just started prioritizing what lasts. And if you build for longevity, you don’t chase novelty—you design for stability.&lt;/p&gt;</content></item><item><title>The Architecture Decision Record Changed How Our Team Communicates</title><link>https://decastro.work/blog/architecture-decision-record-changed-communication/</link><pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/architecture-decision-record-changed-communication/</guid><description>&lt;p&gt;Every team has the same hidden cost: not the build time, not the cloud bills—communication drift. Six months after a big architecture decision, “why did we choose this?” turns into a scavenger hunt through Slack, half-remembered meeting notes, and tribal knowledge that lives in one person’s head. We fixed that with one small habit: Architecture Decision Records (ADRs). The change was less about documentation volume and more about documenting intent—so the team stops guessing.&lt;/p&gt;</description><content>&lt;p&gt;Every team has the same hidden cost: not the build time, not the cloud bills—communication drift. Six months after a big architecture decision, “why did we choose this?” turns into a scavenger hunt through Slack, half-remembered meeting notes, and tribal knowledge that lives in one person’s head. We fixed that with one small habit: Architecture Decision Records (ADRs). The change was less about documentation volume and more about documenting intent—so the team stops guessing.&lt;/p&gt;
&lt;h2 id="what-an-adr-is-and-what-it-isnt"&gt;What an ADR is (and what it isn’t)&lt;/h2&gt;
&lt;p&gt;An ADR is a short, lightweight document that records a significant architecture decision. The key is &lt;em&gt;significant&lt;/em&gt; and &lt;em&gt;architecture&lt;/em&gt;—not every code style preference, not every temporary workaround. We use ADRs to capture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context&lt;/strong&gt;: What problem are we solving, and what constraints exist?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Options considered&lt;/strong&gt;: What alternatives did we weigh?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decision&lt;/strong&gt;: What did we choose?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consequences&lt;/strong&gt;: What trade-offs did we accept, including known risks?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here’s what it isn’t: it’s not a design spec. It’s not a wiki page that someone will eventually complete. It’s not meant to be beautiful or exhaustive. In practice, an ADR is a tiny artifact with a job to do: preserve &lt;em&gt;the why&lt;/em&gt; so it’s still legible later.&lt;/p&gt;
&lt;p&gt;Our rule of thumb is brutally pragmatic: if we would need to answer “why” from scratch in a future incident, migration, or onboarding, that decision deserves an ADR. If the decision is reversible and trivial, it doesn’t.&lt;/p&gt;
&lt;h2 id="the-format-that-makes-adrs-actually-stick"&gt;The format that makes ADRs actually stick&lt;/h2&gt;
&lt;p&gt;You can’t rely on motivation. You need a format that reduces friction.&lt;/p&gt;
&lt;p&gt;We adopted a dead-simple structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ADRs live as &lt;strong&gt;numbered Markdown files&lt;/strong&gt; under a &lt;code&gt;/docs/adr&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Each ADR follows a consistent template so anyone can scan and trust it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example filenames:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/docs/adr/0012-move-to-postgres.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/docs/adr/0013-introduce-event-driven-ingestion.md&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A typical ADR document is compact—often 20–30 minutes to write, usually just a few sections with headings. The point isn’t to write a novel; it’s to make the decision findable and explainable.&lt;/p&gt;
&lt;p&gt;Our template looks like this (conceptually):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Status&lt;/strong&gt; (Proposed / Accepted / Deprecated)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decision&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Options&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consequences&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That template is doing more work than people think. It prevents the common failure mode where “documentation” becomes a blob of details with no recorded reasoning. When the team can read the same structure every time, the ADR log becomes a navigable timeline.&lt;/p&gt;
&lt;h2 id="the-real-benefit-fewer-why-conversations-later"&gt;The real benefit: fewer “why?” conversations later&lt;/h2&gt;
&lt;p&gt;The best argument for ADRs is the one you can feel: fewer repeat questions.&lt;/p&gt;
&lt;p&gt;Before ADRs, our communication had a predictable pattern. Early in a project, decisions were discussed in real time. Later, as the project scaled and people rotated, those same decisions became opaque. We didn’t lose knowledge because people were careless—we lost it because time happens.&lt;/p&gt;
&lt;p&gt;Six months after a migration, someone would ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Why are we using this queue instead of that one?”&lt;/li&gt;
&lt;li&gt;“Why did we avoid synchronous writes?”&lt;/li&gt;
&lt;li&gt;“Why did the service split happen the way it did?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those questions are not petty. They’re how engineers protect themselves from repeating mistakes. The problem is that the answers were locked in Slack threads, meeting minutes, or in the brain of a teammate who might not be available.&lt;/p&gt;
&lt;p&gt;ADRs changed the default. Instead of starting from scratch, we now point to a document:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Check ADR 0013—this was chosen because X, given the constraint Y, and we accepted Z.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The conversation shifts from archaeology to engineering. That’s a real productivity win, but more importantly, it improves quality: fewer misunderstandings, fewer half-accurate assumptions, and faster alignment on trade-offs.&lt;/p&gt;
&lt;h2 id="a-concrete-example-the-event-driven-ingestion-decision"&gt;A concrete example: the “event-driven ingestion” decision&lt;/h2&gt;
&lt;p&gt;One decision that ADRs made dramatically easier to revisit was our move to event-driven ingestion. At the time, we had a familiar list of options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Option A:&lt;/strong&gt; Keep a batch job that periodically fetches and writes to the database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option B:&lt;/strong&gt; Use synchronous requests from the source to the service&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Option C:&lt;/strong&gt; Publish domain events and consume them asynchronously&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the ADR, we captured the context plainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We needed fresher data without overloading the source system.&lt;/li&gt;
&lt;li&gt;We wanted resilience to transient failures.&lt;/li&gt;
&lt;li&gt;We were concerned about backpressure and retry behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then we recorded the decision and the why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We chose the event-driven approach because it decoupled producers from consumers and gave us controlled retry semantics.&lt;/li&gt;
&lt;li&gt;We called out consequences up front: increased operational complexity, the need for idempotency, and a more deliberate debugging story (because “what happened” is spread across logs and event history).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When we later hit a performance issue, we didn’t argue about whether event-driven was “right” in theory. We worked with the documented trade-offs. Even better: we could compare our current symptoms to the consequences section and verify whether we were facing a known risk or a new failure mode.&lt;/p&gt;
&lt;p&gt;That’s the difference between documentation that explains reasoning and documentation that only records outcomes.&lt;/p&gt;
&lt;h2 id="how-adrs-help-onboardingand-reduce-bus-factor-risk"&gt;How ADRs help onboarding—and reduce bus-factor risk&lt;/h2&gt;
&lt;p&gt;Onboarding is where communication debt becomes obvious. New teammates don’t just need to learn &lt;em&gt;what&lt;/em&gt; exists—they need to learn why the system looks the way it does.&lt;/p&gt;
&lt;p&gt;ADRs give new engineers a safe on-ramp. Instead of asking five separate people, they can read the ADR log and understand how the system evolved. That reduces “random questions” and shifts onboarding into a more confident, self-directed process.&lt;/p&gt;
&lt;p&gt;We also treat ADRs as a lightweight hedge against bus-factor risk. When someone leaves, their knowledge shouldn’t vanish with them. ADRs don’t preserve every implementation detail, but they preserve what matters most: the architecture decisions and the logic behind them.&lt;/p&gt;
&lt;p&gt;A practical tweak we made: we encourage engineers to link the ADR in the relevant PR description and in major design docs. That way, the decision isn’t isolated—it’s connected to code history and future context.&lt;/p&gt;
&lt;h2 id="keeping-adrs-useful-discipline-scope-and-updates"&gt;Keeping ADRs useful: discipline, scope, and updates&lt;/h2&gt;
&lt;p&gt;ADRs only work if they stay accurate and current. The fastest way to kill trust is to write an ADR and then pretend it’s still true after the world changes.&lt;/p&gt;
&lt;p&gt;So we practice three disciplines:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Write for future readers, not for the meeting&lt;/strong&gt;&lt;br&gt;
If an ADR can’t be understood by someone who wasn’t in the room, it’s too vague. Be explicit about constraints and trade-offs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Avoid “decision by committee” word salad&lt;/strong&gt;&lt;br&gt;
The ADR should clearly state what was decided. If there were disagreements, capture them as options and consequences—not as a transcript.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update with replacement ADRs&lt;/strong&gt;&lt;br&gt;
When a decision is superseded, we don’t edit the old ADR into a different truth. We mark it deprecated (or replaced) and create a new ADR that explains the new context and updated trade-offs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also, keep scope tight. If you’re starting to write something that feels like a full system proposal, split it. The ADR should capture the &lt;em&gt;decision&lt;/em&gt;, not document every parameter.&lt;/p&gt;
&lt;p&gt;Finally, treat ADRs as living operational tools. When we face a production failure tied to architectural trade-offs, we refer back to the relevant ADR and check what was expected. That turns documentation into a feedback loop, not a dead artifact.&lt;/p&gt;
&lt;h2 id="conclusion-documentation-that-behaves-like-engineering"&gt;Conclusion: documentation that behaves like engineering&lt;/h2&gt;
&lt;p&gt;ADRs didn’t make our team “better at writing.” They made our team better at thinking together.&lt;/p&gt;
&lt;p&gt;Instead of relying on memory, we preserved intent. Instead of replaying old debates, we referenced the recorded trade-offs. Instead of onboarding via Slack archaeology, we gave new engineers a clean timeline of decisions.&lt;/p&gt;
&lt;p&gt;If you want the smallest change with outsized communication impact, start here: create a &lt;code&gt;/docs/adr&lt;/code&gt; folder, adopt a consistent Markdown template, and commit to writing one ADR per meaningful architecture decision. You’ll feel the difference quickly—and you’ll thank yourself months later.&lt;/p&gt;</content></item><item><title>Astro Is the Static Site Framework That Actually Gets It Right</title><link>https://decastro.work/blog/astro-static-site-framework-gets-it-right/</link><pubDate>Tue, 29 Nov 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/astro-static-site-framework-gets-it-right/</guid><description>&lt;p&gt;Most static site generators brag about “fast” the way car companies brag about horsepower. Astro is different. It treats performance as a design constraint, not a marketing afterthought—and it makes the right defaults feel obvious. If you build content-heavy websites (blogs, docs, marketing sites, knowledge bases), Astro’s islands architecture and “zero-JS-by-default” philosophy give you a path to shipping less code without dumbing down the experience.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-static-websites-everything-becomes-javascript"&gt;The real problem with “static” websites: everything becomes JavaScript&lt;/h2&gt;
&lt;p&gt;For years, the ecosystem’s default answer to interactivity has been the same: JavaScript-heavy frameworks, client-side rendering, and a growing pile of hydrated components that wake up as soon as the page loads.&lt;/p&gt;</description><content>&lt;p&gt;Most static site generators brag about “fast” the way car companies brag about horsepower. Astro is different. It treats performance as a design constraint, not a marketing afterthought—and it makes the right defaults feel obvious. If you build content-heavy websites (blogs, docs, marketing sites, knowledge bases), Astro’s islands architecture and “zero-JS-by-default” philosophy give you a path to shipping less code without dumbing down the experience.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-static-websites-everything-becomes-javascript"&gt;The real problem with “static” websites: everything becomes JavaScript&lt;/h2&gt;
&lt;p&gt;For years, the ecosystem’s default answer to interactivity has been the same: JavaScript-heavy frameworks, client-side rendering, and a growing pile of hydrated components that wake up as soon as the page loads.&lt;/p&gt;
&lt;p&gt;Even when your homepage is mostly text and images, you often still pay for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bundling large framework runtime code,&lt;/li&gt;
&lt;li&gt;hydrating components you don’t need immediately,&lt;/li&gt;
&lt;li&gt;running expensive scripts on the main thread,&lt;/li&gt;
&lt;li&gt;and shipping JS that users may never execute.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The worst part is that the pain often hides behind smooth developer workflows. Your local dev server “feels fast,” your pages “work,” and production looks fine—until you test on slower devices or measure load and interactivity. Then the numbers stop being negotiable.&lt;/p&gt;
&lt;p&gt;Astro’s stance is refreshingly blunt: if a page is content, ship content. Only deliver JavaScript where it’s truly necessary.&lt;/p&gt;
&lt;h2 id="zero-js-by-default-the-default-you-should-want"&gt;Zero-JS-by-default: the default you should want&lt;/h2&gt;
&lt;p&gt;Astro’s most practical feature is also its most philosophical: it sends almost no JavaScript by default. The browser receives HTML for the page shell—the parts that matter for reading—rendered immediately.&lt;/p&gt;
&lt;p&gt;That doesn’t mean Astro has a “no JavaScript” rule. It means Astro treats JavaScript as an opt-in tool. If a component needs interactivity (a search box, a comment form, a toggle, a video player), Astro will bundle and hydrate that component. If it doesn’t, Astro doesn’t.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in a blog context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The blog post page loads as plain HTML: headings, paragraphs, images, code blocks, table of contents links.&lt;/li&gt;
&lt;li&gt;A “like” button hydrates only that button when needed.&lt;/li&gt;
&lt;li&gt;A comment widget loads its React component only when the user reaches it.&lt;/li&gt;
&lt;li&gt;If the user never scrolls to comments, the comment JS never runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This “pay for what you touch” approach aligns perfectly with how content is actually consumed. Most readers skim. They rarely engage with every interactive widget on the page. Your site shouldn’t punish them for that.&lt;/p&gt;
&lt;h2 id="islands-architecture-hydrate-where-interactivity-lives-not-everywhere"&gt;Islands architecture: hydrate where interactivity lives, not everywhere&lt;/h2&gt;
&lt;p&gt;Astro’s islands architecture is the mechanism that makes the philosophy work. Think of your page as a calm ocean of HTML, with small islands of interactivity rising where needed.&lt;/p&gt;
&lt;p&gt;A component-based mental model helps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static regions&lt;/strong&gt;: server-rendered HTML that the browser can display immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interactive islands&lt;/strong&gt;: UI components that require client-side behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lazy loading hooks&lt;/strong&gt;: hydration can be triggered based on visibility or user actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The comment widget example is the clearest. On a content-heavy article page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The comments section is typically below the fold.&lt;/li&gt;
&lt;li&gt;Your majority of readers never reach it in the first few seconds.&lt;/li&gt;
&lt;li&gt;Hydrating a full comment app early is a waste.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With islands, you can treat the comment section as an isolated island. Configure it to hydrate on visibility (or on scroll). The result is not just “faster,” but more honest: you only run what the user is about to use.&lt;/p&gt;
&lt;h2 id="a-framework-agnostic-workflow-react-svelte-vue-solid-in-one-project"&gt;A framework-agnostic workflow: React, Svelte, Vue, Solid in one project&lt;/h2&gt;
&lt;p&gt;One reason teams cling to Next.js or Gatsby is ecosystem gravity. But you shouldn’t have to rewrite your mental model or your component library to get good performance.&lt;/p&gt;
&lt;p&gt;Astro supports multiple UI component frameworks in the same project: React, Svelte, Vue, Solid—and you can mix and match based on what you already know.&lt;/p&gt;
&lt;p&gt;That enables a pragmatic strategy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Astro for page composition, routing, content rendering, and static output.&lt;/li&gt;
&lt;li&gt;Use React for an interactive “rich editor” or a complex widget.&lt;/li&gt;
&lt;li&gt;Use Svelte for lightweight UI bits where it’s a good fit.&lt;/li&gt;
&lt;li&gt;Use Solid for ultra-responsive components that benefit from its reactivity model.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not forced into a single framework’s worldview across your entire site. Instead, you choose the right tool for each island.&lt;/p&gt;
&lt;p&gt;In practice, this matters when you’re modernizing an existing content platform. You can migrate page by page and component by component instead of betting the farm on a full rewrite.&lt;/p&gt;
&lt;h2 id="lighthouse-scores-are-nicewhat-you-should-measure-instead"&gt;“Lighthouse scores” are nice—what you should measure instead&lt;/h2&gt;
&lt;p&gt;It’s tempting to celebrate Lighthouse improvements. And yes, when you remove unnecessary JavaScript, your metrics often get dramatically better.&lt;/p&gt;
&lt;p&gt;But the more important question is: what do the metrics represent in real life?&lt;/p&gt;
&lt;p&gt;For content-heavy sites, the outcomes that matter are usually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Immediate readability&lt;/strong&gt;: the page should render quickly enough that users start reading before they notice anything.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower main-thread work&lt;/strong&gt;: fewer scripts competing for time with rendering and user input.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Faster interaction readiness&lt;/strong&gt;: when a user hits an interactive element (like a subscribe form), it should work without lag.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a practical way to think about it when building with Astro:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Default every page to server-rendered HTML.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit interactive components&lt;/strong&gt;: ask, “Does this need to be hydrated immediately on load?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hydrate on demand&lt;/strong&gt;: visibility, user interaction, or even route-level conditions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep islands small&lt;/strong&gt;: don’t wrap an entire page in a single hydrated component just because it’s convenient.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re coming from React-only or Next.js defaults, the mental shift is: “interactivity is localized,” not “the app boots everywhere.”&lt;/p&gt;
&lt;h2 id="astro-vs-nextjs-vs-gatsby-not-a-winner-for-everyone-but-a-best-fit-for-content"&gt;Astro vs Next.js vs Gatsby: not a winner-for-everyone, but a best-fit for content&lt;/h2&gt;
&lt;p&gt;Comparing Astro directly to Next.js or Gatsby is fair, but the framing matters. Next.js is a powerful general-purpose framework. Gatsby is optimized for certain build-time patterns. Astro is optimized for a specific reality: most content sites don’t need a full client app.&lt;/p&gt;
&lt;p&gt;If your site is largely pages, articles, and documents—where most users want to read—Astro is the most natural fit because it treats interactivity as an exception, not the foundation.&lt;/p&gt;
&lt;p&gt;Next.js becomes attractive when you’re building a product-like UI with heavy, always-on interactivity. Gatsby can be great when your build pipeline and data layer match its strengths. Astro wins when you want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;static-friendly output,&lt;/li&gt;
&lt;li&gt;strong performance by default,&lt;/li&gt;
&lt;li&gt;and framework flexibility without framework sprawl.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For teams juggling content and interactivity, Astro feels like the first serious attempt to stop over-hydrating the web.&lt;/p&gt;
&lt;h2 id="build-it-the-smart-way-a-checklist-for-content-heavy-astro-projects"&gt;Build it the smart way: a checklist for content-heavy Astro projects&lt;/h2&gt;
&lt;p&gt;If you’re adopting Astro, don’t just “switch frameworks.” Adopt the performance discipline Astro encourages. Here’s a practical checklist I’d actually use on a real project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start with server-rendered pages&lt;/strong&gt;: let Astro handle layout, metadata, and content rendering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ship components, not apps&lt;/strong&gt;: make interactive widgets small and self-contained.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hydrate when the user is likely to care&lt;/strong&gt;: visibility-based hydration for widgets below the fold is a strong default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid “just because” interactivity&lt;/strong&gt;: if a control isn’t essential, render it as HTML first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use multiple frameworks only where it helps&lt;/strong&gt;: mixed stacks can be powerful, but complexity is real—keep it intentional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure the right moments&lt;/strong&gt;: test load, rendering, and interaction with real devices and throttled networks, not just fast desktops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example strategy: imagine a blog with a newsletter signup, a reading progress bar, and a comment system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reading progress bar: hydrate it as the user scrolls (or early if you consider it essential).&lt;/li&gt;
&lt;li&gt;Newsletter signup: hydrate only the form when it appears near the bottom or after a time delay.&lt;/li&gt;
&lt;li&gt;Comments: hydrate when the comments section becomes visible.&lt;/li&gt;
&lt;li&gt;Everything else: pure HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s islands architecture in the wild: small islands, fewer bytes, better user experience.&lt;/p&gt;
&lt;h2 id="conclusion-astro-makes-performance-the-default-not-the-exception"&gt;Conclusion: Astro makes performance the default, not the exception&lt;/h2&gt;
&lt;p&gt;Astro doesn’t ask you to choose between “modern developer experience” and “fast content.” It gives you a framework where the sane defaults are fast, and where interactivity is treated as an explicit, localized responsibility.&lt;/p&gt;
&lt;p&gt;If you build content-heavy websites and you’re tired of shipping unnecessary JavaScript everywhere, Astro is the kind of tooling that makes the right outcome feel like the easiest one to implement. The future of content isn’t more client apps—it’s less code, better defaults, and islands that show up only when users need them.&lt;/p&gt;</content></item><item><title>Technical Debt Is a Feature, Not a Bug</title><link>https://decastro.work/blog/technical-debt-feature-not-bug/</link><pubDate>Thu, 17 Nov 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/technical-debt-feature-not-bug/</guid><description>&lt;p&gt;Every engineering team wants “clean code.” The irony is that the cleanest codebase is usually the one that never shipped. In the real world, technical debt isn’t an accident you must eliminate—it’s an economic tool you’re always using. The problem isn’t that you accrue debt; the problem is that you accrue it unconsciously, fail to price it, and then act surprised when the interest compounds faster than your roadmap.&lt;/p&gt;
&lt;h2 id="debt-is-how-you-shipon-purpose"&gt;Debt is how you ship—on purpose&lt;/h2&gt;
&lt;p&gt;Let’s make the metaphor explicit: technical debt works like financial debt. You trade something valuable today—time, focus, or architectural purity—for the ability to move faster now. That trade can be rational, even strategically necessary.&lt;/p&gt;</description><content>&lt;p&gt;Every engineering team wants “clean code.” The irony is that the cleanest codebase is usually the one that never shipped. In the real world, technical debt isn’t an accident you must eliminate—it’s an economic tool you’re always using. The problem isn’t that you accrue debt; the problem is that you accrue it unconsciously, fail to price it, and then act surprised when the interest compounds faster than your roadmap.&lt;/p&gt;
&lt;h2 id="debt-is-how-you-shipon-purpose"&gt;Debt is how you ship—on purpose&lt;/h2&gt;
&lt;p&gt;Let’s make the metaphor explicit: technical debt works like financial debt. You trade something valuable today—time, focus, or architectural purity—for the ability to move faster now. That trade can be rational, even strategically necessary.&lt;/p&gt;
&lt;p&gt;Think about a common scenario: you’re building a new onboarding flow. You could spend two months perfecting a domain model, writing a full set of integration tests, and setting up a robust abstraction layer. Or you could ship a minimal version in four weeks, learn from real user behavior, and iterate. That faster launch is a win. It becomes “debt” only if you never plan to pay it back—or if you let the shortcuts quietly metastasize.&lt;/p&gt;
&lt;p&gt;The best teams treat debt as a consciously chosen option, not an embarrassing byproduct. They ask questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are we gaining by taking this shortcut?&lt;/li&gt;
&lt;li&gt;What will this cost us later?&lt;/li&gt;
&lt;li&gt;Can we isolate the impact so it doesn’t leak across the system?&lt;/li&gt;
&lt;li&gt;When, exactly, will we repay?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re answering those questions, you’re managing debt. If you’re pretending it doesn’t exist, you’re managing hope.&lt;/p&gt;
&lt;h2 id="the-real-bug-untracked-debt-and-unpriced-interest"&gt;The real bug: untracked debt and unpriced interest&lt;/h2&gt;
&lt;p&gt;Most organizations don’t have a “technical debt problem.” They have a visibility problem. Debt compounds invisibly when no one tracks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Where the debt lives&lt;/strong&gt; (which services, modules, or components)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What kind of debt it is&lt;/strong&gt; (tests missing, architecture skew, performance shortcuts, process issues)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How it affects delivery&lt;/strong&gt; (slower deployments, harder onboarding, frequent regressions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When it will block new work&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The interest rate isn’t a number on a dashboard—it&amp;rsquo;s your lived experience. It shows up as “small” annoyances that become permanent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every change takes longer because the system is fragile.&lt;/li&gt;
&lt;li&gt;Rollbacks become scary because behavior is unpredictable.&lt;/li&gt;
&lt;li&gt;New hires can’t contribute quickly because everything is entangled.&lt;/li&gt;
&lt;li&gt;Feature work gets rerouted into “fixing old stuff” with no planned budget.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the key: teams rarely notice interest rising until it’s already painful. The metaphor helps because financial debt has a predictable truth—if you only pay minimums, you may never escape. In software, minimum payments look like endless bugfixes without addressing root causes, or refactoring “whenever we have time,” which is a schedule that never arrives.&lt;/p&gt;
&lt;h2 id="a-practical-way-to-credit-card-your-debt"&gt;A practical way to “credit-card” your debt&lt;/h2&gt;
&lt;p&gt;If technical debt is credit card debt, then you need a ledger. Not bureaucracy—operational clarity.&lt;/p&gt;
&lt;h3 id="1-create-a-debt-backlog-with-real-fields"&gt;1) Create a Debt Backlog (with real fields)&lt;/h3&gt;
&lt;p&gt;You don’t need a fancy system. You need consistent entries. For each debt item, capture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context:&lt;/strong&gt; service/module/repo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description:&lt;/strong&gt; what shortcut was taken&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Impact:&lt;/strong&gt; what it slows or breaks (e.g., release frequency, test coverage, deployment time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Estimated repayment cost:&lt;/strong&gt; engineering effort to make it safer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Priority:&lt;/strong&gt; what’s blocking features or increasing risk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paydown plan:&lt;/strong&gt; the smallest complete step that reduces interest&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example entry:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“We skipped integration tests for payment webhook retries in &lt;code&gt;billing-service&lt;/code&gt;. Impact: every change requires manual verification; regressions appear in production.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Repayment: add contract tests + simulation harness; estimated: 2–3 days.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-estimate-repayment-like-you-mean-it"&gt;2) Estimate repayment like you mean it&lt;/h3&gt;
&lt;p&gt;“Someday” is not an estimate. Use ranges if you must, but provide enough structure that planning becomes real. Even a rough “1–2 days to reduce risk substantially” is better than a vague “we should refactor.”&lt;/p&gt;
&lt;p&gt;If you’re worried about accuracy, treat it like portfolio management: you’re not predicting the future perfectly; you’re making it possible to allocate resources intelligently.&lt;/p&gt;
&lt;h3 id="3-tie-debt-to-work-not-vibes"&gt;3) Tie debt to work, not vibes&lt;/h3&gt;
&lt;p&gt;A mature approach links debt items to feature initiatives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a team touches a brittle subsystem, require a debt check.&lt;/li&gt;
&lt;li&gt;When a feature is accepted, ensure the debt ledger reflects any new shortcuts.&lt;/li&gt;
&lt;li&gt;When a release is scheduled, include a debt repayment lane.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last part matters: you’re not just capturing debt—you’re booking repayment capacity.&lt;/p&gt;
&lt;h2 id="interest-shows-up-in-delivery-metrics-so-manage-those-too"&gt;Interest shows up in delivery metrics (so manage those too)&lt;/h2&gt;
&lt;p&gt;Financial debt is measured by payments and interest. Software debt is measured by delivery drag and risk. You can’t manage what you can’t observe, so use lightweight signals that reflect real cost.&lt;/p&gt;
&lt;p&gt;Look for patterns like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Change failure rate:&lt;/strong&gt; when tests are missing or architecture is fragile, failures cluster around modifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lead time creep:&lt;/strong&gt; small changes taking longer over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment friction:&lt;/strong&gt; longer pipelines, more manual steps, more rollbacks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding latency:&lt;/strong&gt; how long it takes new engineers to safely make changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rework loops:&lt;/strong&gt; the same area repeatedly generating bugs or “surprise regressions.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need a perfect metric. What you need is a consistent set of signals and the discipline to interpret them as interest.&lt;/p&gt;
&lt;p&gt;For example, if releases to a specific service increasingly require manual hotfixes, that’s a sign your “interest rate” is climbing. If you keep adding features to the same area while refusing to pay down the ledger, you’re essentially financing growth with credit—until the day you can’t.&lt;/p&gt;
&lt;h2 id="build-repayment-into-your-roadmap-sprints-that-actually-pay-down"&gt;Build repayment into your roadmap: sprints that actually pay down&lt;/h2&gt;
&lt;p&gt;Debt repayment fails when it competes with everything else and always loses. The solution is scheduling. Not as a moral commitment, but as a resource allocation policy.&lt;/p&gt;
&lt;h3 id="a-simple-repayment-cadence"&gt;A simple repayment cadence&lt;/h3&gt;
&lt;p&gt;Many teams adopt a recurring pattern like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Every sprint:&lt;/strong&gt; allocate a fixed percentage (e.g., 15–25%) to debt repayment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every quarter:&lt;/strong&gt; run a dedicated “paydown” initiative when a set of debt items becomes blocking.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The exact numbers don’t matter as much as the principle: repayment must be protected time, not leftover time.&lt;/p&gt;
&lt;h3 id="define-paid-down-clearly"&gt;Define “paid down” clearly&lt;/h3&gt;
&lt;p&gt;Debt items should have a “definition of done” that reduces interest, not just cleans up code aesthetics. Example definitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“We added automated tests that cover the risky behavior.”&lt;/li&gt;
&lt;li&gt;“We reduced deployment steps from N to 1 by improving the pipeline.”&lt;/li&gt;
&lt;li&gt;“We separated the module boundary so future changes don’t require touching unrelated components.”&lt;/li&gt;
&lt;li&gt;“We introduced a stable interface so downstream services stop breaking.”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="pay-the-highest-interest-debt-first"&gt;Pay the highest-interest debt first&lt;/h3&gt;
&lt;p&gt;Just like credit card strategy, don’t treat all debt equally. If one shortcut is causing repeated production incidents or blocking feature delivery, that’s high-interest. Pay it first, even if it’s not the most glamorous refactor.&lt;/p&gt;
&lt;p&gt;Conversely, low-interest cleanup can wait. Not because it’s unimportant—because the portfolio matters.&lt;/p&gt;
&lt;h2 id="when-you-ignore-debt-you-hit-bankruptcy"&gt;When you ignore debt, you hit “bankruptcy”&lt;/h2&gt;
&lt;p&gt;The word “bankruptcy” sounds dramatic, but software has its own versions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You stop shipping features because every release becomes a fire drill.&lt;/li&gt;
&lt;li&gt;Teams lose trust in the system, so changes require excessive ceremony.&lt;/li&gt;
&lt;li&gt;Bugs become so tightly coupled that fixing one thing breaks another.&lt;/li&gt;
&lt;li&gt;Engineers spend more time compensating for uncertainty than delivering value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Notice what’s common: the organization becomes unable to convert engineering effort into reliable outcomes. That’s the software equivalent of insolvency.&lt;/p&gt;
&lt;p&gt;And it’s often preventable. Bankruptcy doesn’t usually arrive from one catastrophic decision. It arrives from a pattern: no ledger, no repayment budget, and a steady stream of new “minimum payments” that keep you afloat just long enough to fall again.&lt;/p&gt;
&lt;h2 id="conclusion-manage-debt-like-engineering-not-like-embarrassment"&gt;Conclusion: Manage debt like engineering, not like embarrassment&lt;/h2&gt;
&lt;p&gt;Technical debt is not a moral failure. It’s a predictable consequence of shipping under constraints—and a tool you can use well. The difference between sustainable engineering and chronic pain is management: track your shortcuts, estimate the cost to repay, assign repayment time, and monitor the interest rate through delivery friction and risk.&lt;/p&gt;
&lt;p&gt;Treat your codebase like a credit portfolio. Pay down high-interest debt before it becomes unpayable. Keep shipping. Stay solvent.&lt;/p&gt;</content></item><item><title>Why Senior Engineers Should Write More Documentation</title><link>https://decastro.work/blog/senior-engineers-write-more-documentation/</link><pubDate>Tue, 08 Nov 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/senior-engineers-write-more-documentation/</guid><description>&lt;p&gt;Senior engineers don’t get promoted because they can compress ideas into clever code—they get promoted because other people can reliably build on those ideas. Documentation is how that reliability ships. The irony is that many experienced engineers treat writing as overhead, even though it’s the highest-leverage work they can do.&lt;/p&gt;
&lt;h2 id="the-real-reason-teams-need-documentation"&gt;The real reason teams “need” documentation&lt;/h2&gt;
&lt;p&gt;Teams don’t fall apart because junior engineers can’t read code. They fall apart because decisions evaporate.&lt;/p&gt;</description><content>&lt;p&gt;Senior engineers don’t get promoted because they can compress ideas into clever code—they get promoted because other people can reliably build on those ideas. Documentation is how that reliability ships. The irony is that many experienced engineers treat writing as overhead, even though it’s the highest-leverage work they can do.&lt;/p&gt;
&lt;h2 id="the-real-reason-teams-need-documentation"&gt;The real reason teams “need” documentation&lt;/h2&gt;
&lt;p&gt;Teams don’t fall apart because junior engineers can’t read code. They fall apart because decisions evaporate.&lt;/p&gt;
&lt;p&gt;Code is executable, but it’s not communicative in the way humans need. It answers &lt;em&gt;what&lt;/em&gt; the system does; it rarely answers &lt;em&gt;why&lt;/em&gt; the system does it, &lt;em&gt;when&lt;/em&gt; it should behave differently, or &lt;em&gt;what trade-off&lt;/em&gt; was accepted to get there. That’s where documentation wins: it preserves context, aligns expectations, and turns tribal knowledge into something transferable.&lt;/p&gt;
&lt;p&gt;If you’ve ever watched a senior engineer spend an hour explaining a system to a teammate (or to a future you), you’ve already seen what’s missing. Documentation isn’t a nice-to-have; it’s a mechanism for reducing repeated thinking and preventing expensive misunderstandings.&lt;/p&gt;
&lt;h2 id="clever-code-is-cheap-institutional-memory-is-scarce"&gt;Clever code is cheap; institutional memory is scarce&lt;/h2&gt;
&lt;p&gt;Experienced developers can always write more code. What’s hard is carrying forward the reasoning behind the code when people rotate, teams reorganize, or priorities change. Documentation is the closest thing engineering has to institutional memory.&lt;/p&gt;
&lt;p&gt;Consider a feature that “works on staging but not in production.” Sometimes the bug is real. Often it’s a gap in understanding: different configuration defaults, a missing dependency, an environment-specific constraint, or a deliberate behavior from an earlier incident. When the explanation lives only in someone’s head, the team is condemned to rediscover it the next time it breaks—usually during peak traffic, usually at the worst possible moment.&lt;/p&gt;
&lt;p&gt;Institutional memory also changes the shape of engineering work. When the why is written down, you can review decisions faster, onboard new engineers faster, and debug incidents without asking the same question in three different Slack threads.&lt;/p&gt;
&lt;h2 id="adrs-prevent-three-meetings-with-one-good-page"&gt;ADRs: prevent three meetings with one good page&lt;/h2&gt;
&lt;p&gt;The most practical documentation senior engineers can write is an Architecture Decision Record (ADR). Not because ADRs are fashionable—but because they directly attack the most common coordination cost in software: misalignment.&lt;/p&gt;
&lt;p&gt;A good ADR captures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Decision&lt;/strong&gt;: what you chose.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status&lt;/strong&gt;: accepted, rejected, superseded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context&lt;/strong&gt;: what problem you were solving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alternatives considered&lt;/strong&gt;: at least the major ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade-offs&lt;/strong&gt;: what you intentionally gave up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consequences&lt;/strong&gt;: how this affects future changes, operations, and risk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a concrete example. Imagine you decide to introduce a message queue for an order processing pipeline. Without an ADR, the team will eventually ask:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why not do it synchronously?&lt;/li&gt;
&lt;li&gt;Why this queue tech?&lt;/li&gt;
&lt;li&gt;What does it imply for retries and idempotency?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Those questions often turn into meetings because the “why” is trapped. With an ADR, they become reading exercises. The next time someone proposes switching away from the queue, they can refer to the trade-offs you already documented instead of re-litigating the same debate.&lt;/p&gt;
&lt;p&gt;Opinionated standard: if a decision affects correctness, reliability, security, or operational complexity, write it down. If it doesn’t—maybe it doesn’t deserve to be a decision at all.&lt;/p&gt;
&lt;h2 id="runbooks-the-difference-between-calm-and-chaos-at-2-am"&gt;Runbooks: the difference between calm and chaos at 2 AM&lt;/h2&gt;
&lt;p&gt;Code runs in production with or without documentation. But your on-call rotation lives and dies by how quickly humans can understand what’s happening.&lt;/p&gt;
&lt;p&gt;A runbook isn’t a screenshot collection or a pile of “try these steps” commands. It’s an explanation of system behavior under stress, plus a path to safe resolution.&lt;/p&gt;
&lt;p&gt;A useful runbook answers questions like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What does “normal” look like?&lt;/strong&gt; (Key metrics, expected patterns, alert triggers.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What changes during peak hours?&lt;/strong&gt; (Caching behavior, queue depth, throttling, autoscaling delays.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do dependencies behave?&lt;/strong&gt; (Database latency, third-party API limits, network issues.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Common failure modes&lt;/strong&gt; and what symptoms they produce.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Triage steps in order&lt;/strong&gt;: verify, isolate, mitigate, then resolve.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback and mitigation guidance&lt;/strong&gt;: what to do if things worsen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership and escalation&lt;/strong&gt;: who to page next and when.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Take the scenario from real life: the system behaves oddly during peak hours. Maybe timeouts spike because of downstream throttling, or a cache TTL causes stampedes right at the top of the hour. If that behavior is “mystifying,” it becomes an incident generator. If it’s documented—clearly, with reasoning—on-call becomes surgical instead of improvised.&lt;/p&gt;
&lt;p&gt;And yes, runbooks should be written by engineers who understand the system. Not just by whoever happens to have the least urgent backlog. If you wrote the subsystem, you’re uniquely qualified to explain what’s happening and why.&lt;/p&gt;
&lt;h2 id="documentation-reduces-the-real-cost-of-ownership"&gt;Documentation reduces the &lt;em&gt;real&lt;/em&gt; cost of ownership&lt;/h2&gt;
&lt;p&gt;The argument against documentation is usually framed like this: “We’re busy. The code is the source of truth.” That’s true, but incomplete. Documentation doesn’t replace code; it complements it by expressing the parts of software that code doesn’t naturally tell.&lt;/p&gt;
&lt;p&gt;Here’s the kicker: documentation is not extra work—it’s a form of engineering leverage.&lt;/p&gt;
&lt;p&gt;Without documentation, senior engineers pay a “re-explanation tax” repeatedly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;during onboarding,&lt;/li&gt;
&lt;li&gt;during code reviews,&lt;/li&gt;
&lt;li&gt;during incident retrospectives,&lt;/li&gt;
&lt;li&gt;during escalations,&lt;/li&gt;
&lt;li&gt;during dependency upgrades,&lt;/li&gt;
&lt;li&gt;during migrations and refactors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With documentation, you convert that tax into once-and-done writing. The system still changes, but your knowledge updates with it. A small doc that stays current saves hours across many future cycles. A stale doc is worse than no doc; the answer isn’t perfectionism—it’s ownership discipline (more on that below).&lt;/p&gt;
&lt;h2 id="how-to-write-docs-senior-engineers-actually-maintain"&gt;How to write docs senior engineers actually maintain&lt;/h2&gt;
&lt;p&gt;Documentation fails when it becomes a dumping ground. Good documentation isn’t long—it’s &lt;em&gt;usable&lt;/em&gt;. A useful doc has a clear audience and a clear job.&lt;/p&gt;
&lt;h3 id="adopt-a-simple-writing-contract"&gt;Adopt a simple writing contract&lt;/h3&gt;
&lt;p&gt;Every doc should answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Who is this for?&lt;/strong&gt; (On-call engineers, future maintainers, product engineers, security reviewers.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What decision or behavior does it cover?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What should the reader do with it?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="use-structured-formats"&gt;Use structured formats&lt;/h3&gt;
&lt;p&gt;Some formats are reliably effective:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ADRs&lt;/strong&gt; for decisions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runbooks&lt;/strong&gt; for operational response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“Design notes”&lt;/strong&gt; for complex subsystems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readme + docs pages&lt;/strong&gt; for entry points and mental models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commented examples&lt;/strong&gt; when the code behavior is non-obvious.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="keep-docs-close-to-the-code"&gt;Keep docs close to the code&lt;/h3&gt;
&lt;p&gt;A runbook in a random wiki link is effectively invisible at 2 AM. Put ADRs next to the systems they affect. Put runbooks in the repository that owns the service. Use links, but don’t bury essentials behind link stacks.&lt;/p&gt;
&lt;h3 id="treat-documentation-as-code-not-art"&gt;Treat documentation as code, not art&lt;/h3&gt;
&lt;p&gt;Version it. Review it. Refactor it. If you change a subsystem, update the documentation as part of the same work item. This is how you prevent documentation drift—the silent killer that turns good intentions into misleading guides.&lt;/p&gt;
&lt;p&gt;A practical workflow: add a “Docs updated?” checkbox in your PR template for changes that affect behavior, interfaces, or operational characteristics. It’s not bureaucracy; it’s a reminder that knowledge must travel with code.&lt;/p&gt;
&lt;h2 id="the-payoff-fewer-escalations-faster-change-and-better-engineers"&gt;The payoff: fewer escalations, faster change, and better engineers&lt;/h2&gt;
&lt;p&gt;When senior engineers write more documentation, the benefits are immediate and visible.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incidents improve&lt;/strong&gt; because triage is faster and less dependent on a single person’s memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reviews improve&lt;/strong&gt; because reviewers can evaluate trade-offs, not just diffs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding improves&lt;/strong&gt; because new engineers can form accurate mental models without interrupting experts constantly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teams become more resilient&lt;/strong&gt; to turnover because knowledge survives beyond the original author.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most importantly, documentation trains your organization to think like engineers. It forces clarity: what problem are we solving, what constraints exist, what risks did we accept, and what behavior should be expected under different conditions? That clarity improves both systems and people.&lt;/p&gt;
&lt;p&gt;And yes, it can feel like you’re writing “less code.” But the outcome is more successful code: code that others can safely change, operate, and trust.&lt;/p&gt;
&lt;h2 id="conclusion-document-like-your-future-self-is-your-teammate"&gt;Conclusion: document like your future self is your teammate&lt;/h2&gt;
&lt;p&gt;Senior engineers should write more documentation because it’s the highest-leverage activity in software. It prevents recurring debates, reduces operational chaos, and preserves the reasoning that code alone can’t carry. Write the ADRs. Write the runbooks. Keep them close to the systems they describe. Then watch your team become faster, calmer, and more capable—especially when the next incident hits and your future self is reading at 2 AM.&lt;/p&gt;</content></item><item><title>SolidJS Deserves Your Attention</title><link>https://decastro.work/blog/solidjs-deserves-your-attention/</link><pubDate>Sat, 05 Nov 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/solidjs-deserves-your-attention/</guid><description>&lt;p&gt;React taught the modern web how to think in components and re-render cycles. SolidJS—quietly, confidently—makes the case that your mental model doesn’t need to be “render, diff, patch” to get interactive UI performance. It swaps the usual framework tax for fine-grained reactivity: update only what changes, directly in the DOM, without virtual DOM diffing or rerendering components.&lt;/p&gt;
&lt;p&gt;If you like hooks, you’ll feel at home. If you care about performance, SolidJS is the rare framework that treats it as a first-class design constraint rather than an afterthought.&lt;/p&gt;</description><content>&lt;p&gt;React taught the modern web how to think in components and re-render cycles. SolidJS—quietly, confidently—makes the case that your mental model doesn’t need to be “render, diff, patch” to get interactive UI performance. It swaps the usual framework tax for fine-grained reactivity: update only what changes, directly in the DOM, without virtual DOM diffing or rerendering components.&lt;/p&gt;
&lt;p&gt;If you like hooks, you’ll feel at home. If you care about performance, SolidJS is the rare framework that treats it as a first-class design constraint rather than an afterthought.&lt;/p&gt;
&lt;h2 id="the-react-mental-model-and-why-solid-questions-it"&gt;The React Mental Model (and Why Solid Questions It)&lt;/h2&gt;
&lt;p&gt;React’s model is simple to explain: state changes, components re-render, React reconciles what should appear. That model is powerful, but it has a built-in assumption: the UI can be derived from state by repeatedly “recomputing” component trees.&lt;/p&gt;
&lt;p&gt;SolidJS agrees with the &lt;em&gt;goal&lt;/em&gt;—a declarative UI—but challenges the &lt;em&gt;mechanism&lt;/em&gt;. Instead of running components again and again, Solid compiles your reactive code into a graph of dependencies. When a signal changes, Solid updates only the DOM nodes that depend on it. No virtual DOM. No diffing. No rerendering to “figure it out” again.&lt;/p&gt;
&lt;p&gt;Here’s the practical difference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In React, a state update triggers a render pass for the affected component(s). Even if reconciliation avoids heavy work, the render phase still happens.&lt;/li&gt;
&lt;li&gt;In Solid, reactive primitives wire dependencies once. Updates propagate through that graph to precisely targeted DOM updates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t theoretical. It changes how predictable performance feels under real interaction: typing in an input, toggling classes, streaming data into a list, or driving high-frequency UI like charts and editors.&lt;/p&gt;
&lt;h2 id="fine-grained-reactivity-signals-that-hit-the-dom-directly"&gt;Fine-Grained Reactivity: Signals That Hit the DOM Directly&lt;/h2&gt;
&lt;p&gt;SolidJS uses &lt;strong&gt;signals&lt;/strong&gt; (the core primitive) to represent reactive state. When you read a signal during computation, Solid records the dependency. When you write to that signal later, Solid automatically re-runs only the computations that actually depend on it—and updates the specific DOM nodes bound to those computations.&lt;/p&gt;
&lt;p&gt;A simple example makes the point:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-jsx" data-lang="jsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;createSignal&lt;/span&gt; } &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;solid-js&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Counter&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;count&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;setCount&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createSignal&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;button&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;onClick&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{() =&amp;gt; &lt;span style="color:#a6e22e"&gt;setCount&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;c&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;c&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)}&amp;gt;&lt;span style="color:#f92672"&gt;+&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;span&lt;/span&gt;&amp;gt;{&lt;span style="color:#a6e22e"&gt;count&lt;/span&gt;()}&amp;lt;/&lt;span style="color:#f92672"&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice what’s not happening. The component function doesn’t need to re-run like it does in React’s render model. Instead, &lt;code&gt;count()&lt;/code&gt; is a reactive read. Solid connects that read to the &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; text node. When &lt;code&gt;setCount&lt;/code&gt; fires, Solid updates the span and leaves everything else alone.&lt;/p&gt;
&lt;p&gt;Now scale the idea:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you have ten reactive fields on a form, changing one field updates only the DOM parts that depend on that field.&lt;/li&gt;
&lt;li&gt;If a complex child component reads a signal, it updates only those computations that read that signal—not the entire component subtree via rerender.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Solid’s reactivity is &lt;em&gt;fine-grained&lt;/em&gt; because dependencies are tracked at the level of individual reads and computations, not at the component boundary.&lt;/p&gt;
&lt;h2 id="jsx-without-the-virtual-dom-overhead"&gt;JSX Without the Virtual DOM Overhead&lt;/h2&gt;
&lt;p&gt;Solid uses JSX, so your UI still looks like React. But JSX in Solid is not synonymous with “virtual DOM runtime work.” Solid’s compilation strategy produces efficient DOM bindings.&lt;/p&gt;
&lt;p&gt;In practice, you get a component authoring style that looks familiar while avoiding the reconciler loop that virtual DOM implementations rely on. That means fewer moving parts at runtime and less wasted work when state updates frequently.&lt;/p&gt;
&lt;p&gt;Think about this workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In React, you might rely on memoization (&lt;code&gt;React.memo&lt;/code&gt;, &lt;code&gt;useMemo&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;) to prevent unnecessary rerenders.&lt;/li&gt;
&lt;li&gt;In Solid, the dependency graph already ensures that only relevant computations update.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This doesn’t mean performance “just happens” in every case. If you create overly broad reactive dependencies, you can still cause more updates than needed. But Solid makes the common case—updating a single piece of UI—naturally efficient.&lt;/p&gt;
&lt;h2 id="hooks-like-ergonomics-with-a-different-execution-model"&gt;Hooks-Like Ergonomics, With a Different Execution Model&lt;/h2&gt;
&lt;p&gt;Solid’s API is intentionally approachable. You can write code that feels like React hooks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createSignal&lt;/code&gt; for state&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createEffect&lt;/code&gt; for side effects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createMemo&lt;/code&gt; for derived values&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onCleanup&lt;/code&gt; for teardown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the key is: the semantics are built around &lt;em&gt;reactive execution&lt;/em&gt;, not component rerender cycles.&lt;/p&gt;
&lt;p&gt;A derived computation is a good example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-jsx" data-lang="jsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;createSignal&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;createMemo&lt;/span&gt; } &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;solid-js&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Product&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;setPrice&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createSignal&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; [&lt;span style="color:#a6e22e"&gt;qty&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;setQty&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createSignal&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;total&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;createMemo&lt;/span&gt;(() =&amp;gt; &lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;() &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;qty&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;number&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;()} &lt;span style="color:#a6e22e"&gt;onInput&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{(&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;setPrice&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;currentTarget&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;valueAsNumber&lt;/span&gt;)} /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;number&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;qty&lt;/span&gt;()} &lt;span style="color:#a6e22e"&gt;onInput&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{(&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;) =&amp;gt; &lt;span style="color:#a6e22e"&gt;setQty&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;currentTarget&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;valueAsNumber&lt;/span&gt;)} /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;&lt;span style="color:#a6e22e"&gt;Total&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {&lt;span style="color:#a6e22e"&gt;total&lt;/span&gt;()}&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;total()&lt;/code&gt; updates only when &lt;code&gt;price()&lt;/code&gt; or &lt;code&gt;qty()&lt;/code&gt; changes, and only the parts depending on &lt;code&gt;total()&lt;/code&gt; re-evaluate. You’re not re-rendering the entire component tree to rediscover the relationship.&lt;/p&gt;
&lt;p&gt;This is where Solid feels like “React if it was designed today”: the API lowers the cognitive barrier, while the runtime behavior targets the performance bottleneck instead of papering over it with memoization.&lt;/p&gt;
&lt;h2 id="practical-guidance-how-to-think-in-signals-not-rerenders"&gt;Practical Guidance: How to Think in Signals, Not Rerenders&lt;/h2&gt;
&lt;p&gt;Solid rewards a particular way of structuring UI code. If you come from React, you can accidentally write “React-shaped” Solid code that creates unnecessary reactive reads. The good news: the fixes are straightforward.&lt;/p&gt;
&lt;h3 id="1-keep-reactive-reads-close-to-the-dom-that-needs-them"&gt;1) Keep reactive reads close to the DOM that needs them&lt;/h3&gt;
&lt;p&gt;If a computation reads a signal, that signal becomes a dependency for that computation. If you read it in a wide scope, you’ll broaden what updates. Prefer reading in the smallest scope that actually needs the value.&lt;/p&gt;
&lt;h3 id="2-use-derived-state-explicitly-with-creatememo"&gt;2) Use derived state explicitly with &lt;code&gt;createMemo&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you compute something from signals, treat it as a derived reactive value. That keeps dependency tracking honest and prevents accidental recomputation.&lt;/p&gt;
&lt;h3 id="3-be-intentional-about-effects"&gt;3) Be intentional about effects&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;createEffect&lt;/code&gt; runs when its dependencies change. If an effect reads multiple signals, it will rerun whenever any of them changes. That’s correct behavior—just make sure the effect’s dependency footprint is what you mean.&lt;/p&gt;
&lt;h3 id="4-model-lists-carefully"&gt;4) Model lists carefully&lt;/h3&gt;
&lt;p&gt;List rendering is where frameworks either shine or struggle. Solid’s fine-grained model is a strong fit for updating individual items. When you render an array, ensure you use Solid’s list primitives (like &lt;code&gt;For&lt;/code&gt;) and structure your data flow so updates don’t force wholesale recalculation.&lt;/p&gt;
&lt;p&gt;A common pattern: keep item state granular. If each row depends on its own signals, changes to one row won’t ripple through the entire list.&lt;/p&gt;
&lt;h2 id="when-solid-is-the-better-choice"&gt;When Solid Is the Better Choice&lt;/h2&gt;
&lt;p&gt;Solid is not a universal replacement for React. But it’s an excellent fit when you care about UI responsiveness and predictable performance—especially under frequent updates or complex interactive surfaces.&lt;/p&gt;
&lt;p&gt;Consider teams building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dashboards with fast-moving filters and live values&lt;/li&gt;
&lt;li&gt;editors with keystroke-level interactivity&lt;/li&gt;
&lt;li&gt;animation-heavy interfaces&lt;/li&gt;
&lt;li&gt;data-rich apps where “small changes” happen constantly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Solid’s model aligns with those realities. You don’t want rerenders to be the mechanism that “figures out” what changed. You want changes to propagate directly to the DOM nodes that actually care.&lt;/p&gt;
&lt;p&gt;And because Solid is JSX-based with hooks-like APIs, adoption is less intimidating than it sounds. Your biggest adjustment is internal: stop expecting component rerenders to be the engine of UI updates.&lt;/p&gt;
&lt;h2 id="conclusion-a-framework-that-respects-your-time"&gt;Conclusion: A Framework That Respects Your Time&lt;/h2&gt;
&lt;p&gt;SolidJS deserves attention because it offers the rare combination of familiar ergonomics and a radically more efficient update model. It replaces virtual DOM diffing and component rerender cycles with fine-grained reactivity that updates only what changed—often with less code in the places where React traditionally leans on memoization to stay fast.&lt;/p&gt;
&lt;p&gt;If you’ve ever thought, “This should be simpler, and it shouldn’t cost this much,” Solid is the kind of framework you reach for. React may still be your default, but Solid is a compelling answer to the question behind React’s greatest abstraction: what if the mental model was wrong all along?&lt;/p&gt;</content></item><item><title>Functional Programming Concepts Every OOP Developer Should Steal</title><link>https://decastro.work/blog/functional-programming-concepts-oop-developers-steal/</link><pubDate>Tue, 25 Oct 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/functional-programming-concepts-oop-developers-steal/</guid><description>&lt;p&gt;If you’ve ever shipped a bug caused by “somehow that state changed,” congratulations—you’ve already met the enemy. The good news: you don’t need to abandon OOP, learn Haskell, or rewrite your entire codebase to get most of the benefits of functional programming. You just need to steal a few ideas: immutability, pure functions, and composition.&lt;/p&gt;
&lt;p&gt;These aren’t academic curiosities. They’re pragmatic techniques that make code easier to reason about, simpler to test, and harder to break.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever shipped a bug caused by “somehow that state changed,” congratulations—you’ve already met the enemy. The good news: you don’t need to abandon OOP, learn Haskell, or rewrite your entire codebase to get most of the benefits of functional programming. You just need to steal a few ideas: immutability, pure functions, and composition.&lt;/p&gt;
&lt;p&gt;These aren’t academic curiosities. They’re pragmatic techniques that make code easier to reason about, simpler to test, and harder to break.&lt;/p&gt;
&lt;h2 id="1-stop-treating-state-like-it-cant-bite-you"&gt;1) Stop treating state like it can’t bite you&lt;/h2&gt;
&lt;p&gt;OOP is built around state: objects hold data, methods change it, and the system evolves. That’s not wrong—until it becomes unmanageable. The functional mindset challenges the default assumption that mutable state is the natural way to build software.&lt;/p&gt;
&lt;h3 id="what-immutability-actually-buys-you"&gt;What immutability actually buys you&lt;/h3&gt;
&lt;p&gt;When your data is immutable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You eliminate whole categories of “action at a distance” bugs (where a change in one place unexpectedly affects another).&lt;/li&gt;
&lt;li&gt;You can trust that a function’s inputs won’t be quietly mutated under your feet.&lt;/li&gt;
&lt;li&gt;Debugging gets less mystical. If something changes, you’ll see where a new value was created.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="practical-example-safer-updates"&gt;Practical example: safer updates&lt;/h3&gt;
&lt;p&gt;Imagine a typical OOP-style “update” flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;Cart&lt;/code&gt; object has a &lt;code&gt;List&amp;lt;Item&amp;gt;&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;addItem()&lt;/code&gt; method mutates that list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now compare a functional-ish approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cart&lt;/code&gt; is treated as immutable.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;addItem()&lt;/code&gt; returns a &lt;em&gt;new&lt;/em&gt; &lt;code&gt;Cart&lt;/code&gt; with the additional item.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In Java-like pseudocode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mutable style:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cart.addItem(item)&lt;/code&gt; (side effect)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Immutable style:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cart = cart.withAddedItem(item)&lt;/code&gt; (new value)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You didn’t remove the need for updates—you just made updates explicit and traceable. Most teams discover that this alone reduces bug volume dramatically, not because developers become smarter, but because the code becomes less ambiguous.&lt;/p&gt;
&lt;h3 id="how-to-start-without-rewriting-everything"&gt;How to start without rewriting everything&lt;/h3&gt;
&lt;p&gt;You don’t need persistent data structures tomorrow. Start small:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer &lt;code&gt;final&lt;/code&gt; (or equivalent) fields where feasible.&lt;/li&gt;
&lt;li&gt;Treat “DTOs” and “event objects” as immutable.&lt;/li&gt;
&lt;li&gt;In critical paths, return new values instead of mutating shared objects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re using TypeScript, you can also lean on &lt;code&gt;readonly&lt;/code&gt; types. If you’re in C#, favor records for value-like models. The goal is consistency: when objects represent facts, keep them stable.&lt;/p&gt;
&lt;h2 id="2-make-your-core-logic-pure-and-your-tests-will-thank-you"&gt;2) Make your core logic pure (and your tests will thank you)&lt;/h2&gt;
&lt;p&gt;Immutability prevents accidental chaos, but pure functions prevent intentional chaos. A pure function does one thing: it returns a result based solely on its inputs, without side effects.&lt;/p&gt;
&lt;p&gt;No network calls. No database writes. No hidden time bombs. Just input → output.&lt;/p&gt;
&lt;h3 id="why-pure-functions-are-the-ultimate-testability-upgrade"&gt;Why pure functions are the ultimate testability upgrade&lt;/h3&gt;
&lt;p&gt;Testing pure functions is almost comically easy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Given input X, you expect output Y.&lt;/li&gt;
&lt;li&gt;There’s no need to set up mocks for time, random numbers, or external services (unless those are &lt;em&gt;inputs&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t mean you can make your entire system pure. It means you should make the &lt;em&gt;business logic&lt;/em&gt; pure where it matters.&lt;/p&gt;
&lt;h3 id="practical-example-split-logic-from-effects"&gt;Practical example: split logic from effects&lt;/h3&gt;
&lt;p&gt;Consider a payment flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You fetch exchange rates (effect)&lt;/li&gt;
&lt;li&gt;You compute the final totals (logic)&lt;/li&gt;
&lt;li&gt;You store the transaction (effect)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A functional-friendly structure splits these responsibilities:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Effectful boundary&lt;/strong&gt;: “Get rate,” “read cart,” “write ledger.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure core&lt;/strong&gt;: “Compute totals,” “validate constraints,” “apply discounts.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Suppose you currently have a method like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;calculateTotal(cart)&lt;/code&gt; that calls a rate service internally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s hard to test and easy to break. Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;calculateTotal(cart, rate)&lt;/code&gt; becomes pure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now you can test the computation with plain data:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide cart and rate&lt;/li&gt;
&lt;li&gt;Assert the totals&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The service call still exists, but it moves to the edges of your application where side effects belong.&lt;/p&gt;
&lt;h3 id="what-about-randomness-and-time"&gt;What about randomness and time?&lt;/h3&gt;
&lt;p&gt;If you use &lt;code&gt;now()&lt;/code&gt; or &lt;code&gt;random()&lt;/code&gt;, you’re quietly importing hidden dependencies. The easiest fix is to pass them in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;computeDiscount(cart, currentTime)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;generateOrderId(seed)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps “environment” out of your core logic. It’s also a clean design signal: your logic depends on explicit inputs, not ambient conditions.&lt;/p&gt;
&lt;h2 id="3-compose-transformations-instead-of-stacking-control-flow"&gt;3) Compose transformations instead of stacking control flow&lt;/h2&gt;
&lt;p&gt;OOP code often leans on loops, conditionals, and stateful accumulation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“iterate through items”&lt;/li&gt;
&lt;li&gt;“if this, then mutate”&lt;/li&gt;
&lt;li&gt;“track totals”&lt;/li&gt;
&lt;li&gt;“handle edge cases inline”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Functional composition encourages a different style: define transformations and chain them. A pipeline is a story with steps.&lt;/p&gt;
&lt;h3 id="mapfilterreduce-as-the-data-movement-language"&gt;Map/filter/reduce as the “data movement” language&lt;/h3&gt;
&lt;p&gt;These three operators cover an astonishing amount of everyday work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;map&lt;/strong&gt;: transform each element&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;filter&lt;/strong&gt;: keep only elements that match&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;reduce&lt;/strong&gt;: combine elements into one result&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: generating order lines from raw cart items.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A for-loop that mutates an array&lt;/li&gt;
&lt;li&gt;Conditionals scattered inside the loop&lt;/li&gt;
&lt;li&gt;Manual accumulation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can express it as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;items.filter(isEligible)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.map(toOrderLine)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.reduce(sumTotal)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This reads like a specification. Also, it tends to make edge cases more obvious, because you isolate them in small, named functions.&lt;/p&gt;
&lt;h3 id="concrete-advice-name-your-steps"&gt;Concrete advice: name your steps&lt;/h3&gt;
&lt;p&gt;Don’t just cram anonymous lambdas into a single statement. Make it legible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;const eligible = items.filter(isEligibleItem)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const lines = eligible.map(toOrderLine)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const total = lines.reduce(addLineTotal, 0)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even in languages without first-class pipeline syntax, you can preserve the structure. The key is composition: each step should do one job.&lt;/p&gt;
&lt;h3 id="when-composition-helps-beyond-lists"&gt;When composition helps beyond lists&lt;/h3&gt;
&lt;p&gt;Composition isn’t limited to arrays. You can compose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;functions for validation&lt;/li&gt;
&lt;li&gt;mappers from domain models to view models&lt;/li&gt;
&lt;li&gt;parsers that normalize input&lt;/li&gt;
&lt;li&gt;reducers that build aggregates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The bigger your codebase, the more you’ll appreciate code that can be assembled like Lego rather than rewritten like a monolith.&lt;/p&gt;
&lt;h2 id="4-keep-your-objectsjust-stop-letting-them-control-everything"&gt;4) Keep your objects—just stop letting them control everything&lt;/h2&gt;
&lt;p&gt;Here’s the misconception to drop: functional programming doesn’t require abandoning OOP. You can absolutely keep classes, encapsulation, and polymorphism. The “steal” is about how you structure &lt;em&gt;logic&lt;/em&gt; and how you manage &lt;em&gt;data&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="a-hybrid-pattern-that-works-in-the-real-world"&gt;A hybrid pattern that works in the real world&lt;/h3&gt;
&lt;p&gt;A common, effective structure looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain objects&lt;/strong&gt;: represent core concepts (often immutable).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure functions&lt;/strong&gt;: implement business rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effectful services&lt;/strong&gt;: handle I/O (databases, HTTP, messaging).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composition layer&lt;/strong&gt;: orchestrates pure logic from effect boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of it as: objects own &lt;em&gt;meaning&lt;/em&gt;, pure functions own &lt;em&gt;reasoning&lt;/em&gt;, and services own &lt;em&gt;consequences&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="concrete-example-validation"&gt;Concrete example: validation&lt;/h3&gt;
&lt;p&gt;Bad approach (typical OOP trap):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;User&lt;/code&gt; method performs validation and also hits external systems to check something.&lt;/li&gt;
&lt;li&gt;The behavior becomes hard to test because it’s entangled with side effects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A pure function &lt;code&gt;validateUser(user)&lt;/code&gt; returns either errors or a validated result.&lt;/li&gt;
&lt;li&gt;Effectful calls happen elsewhere if needed (e.g., uniqueness checks against a database).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t weaken encapsulation; it makes responsibilities cleaner and testing cheaper.&lt;/p&gt;
&lt;h3 id="practical-heuristic-methods-that-call-the-network-arent-domain-logic"&gt;Practical heuristic: “Methods that call the network aren’t domain logic”&lt;/h3&gt;
&lt;p&gt;If a method performs I/O, it belongs near the edges. If a method only transforms data, it belongs in the core. When you apply that heuristic consistently, the functional gains arrive without the ideological baggage.&lt;/p&gt;
&lt;h2 id="5-you-dont-need-monads-to-get-the-benefits"&gt;5) You don’t need monads to get the benefits&lt;/h2&gt;
&lt;p&gt;You can hear the functional crowd and assume you must learn monads, higher-kinded types, or category theory to be “doing it right.” You don’t. Most practical functional improvements come from the basics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;avoid mutable shared state&lt;/li&gt;
&lt;li&gt;write pure functions for business rules&lt;/li&gt;
&lt;li&gt;compose transformations clearly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a small “gateway drug” concept beyond the basics, start with something as simple as representing computations that may fail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;Result&lt;/code&gt;/&lt;code&gt;Either&lt;/code&gt;-like types where available&lt;/li&gt;
&lt;li&gt;Or use explicit error-return objects&lt;/li&gt;
&lt;li&gt;Or, in mainstream languages, at least avoid exceptions for expected control flow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But even if you never touch monads, immutability + pure functions + composition already give you a large share of the real-world value.&lt;/p&gt;
&lt;h2 id="conclusion-steal-the-useful-partsand-your-code-will-get-calmer"&gt;Conclusion: steal the useful parts—and your code will get calmer&lt;/h2&gt;
&lt;p&gt;You don’t need to pick a side in a decades-old debate. The best modern code borrows ruthlessly: OOP for structure and abstraction, and functional ideas for predictability and clarity. Make data immutable where it represents facts. Write business logic as pure functions so tests become straightforward and bugs become rarer. Finally, express transformations as composition—so the “what” is obvious and the “how” stays tidy.&lt;/p&gt;
&lt;p&gt;Steal those three concepts, and you’ll feel the difference quickly: fewer state-related incidents, faster iteration, and code that reads like it was designed—not merely written.&lt;/p&gt;</content></item><item><title>GitHub Actions Ate Jenkins and Nobody Mourned</title><link>https://decastro.work/blog/github-actions-ate-jenkins-nobody-mourned/</link><pubDate>Thu, 13 Oct 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/github-actions-ate-jenkins-nobody-mourned/</guid><description>&lt;p&gt;For years, CI/CD felt like a rite of passage: set up Jenkins, wrestle with plugins, babysit servers, and pretend the build pipeline was “just another application.” Then GitHub Actions arrived with a simple pitch—commit a workflow, and stop treating automation like a pet. The punchline isn’t that Jenkins stopped working. It’s that the reasons people used Jenkins became optional overnight.&lt;/p&gt;
&lt;h2 id="the-jenkins-era-great-tools-heavy-maintenance"&gt;The Jenkins Era: Great Tools, Heavy Maintenance&lt;/h2&gt;
&lt;p&gt;Jenkins didn’t earn its reputation by accident. It was flexible, battle-tested, and extensible in a way that made teams feel in control. If you needed a custom build step, a weird environment, or an opinionated deployment flow, you could usually duct-tape it into Jenkins and keep shipping.&lt;/p&gt;</description><content>&lt;p&gt;For years, CI/CD felt like a rite of passage: set up Jenkins, wrestle with plugins, babysit servers, and pretend the build pipeline was “just another application.” Then GitHub Actions arrived with a simple pitch—commit a workflow, and stop treating automation like a pet. The punchline isn’t that Jenkins stopped working. It’s that the reasons people used Jenkins became optional overnight.&lt;/p&gt;
&lt;h2 id="the-jenkins-era-great-tools-heavy-maintenance"&gt;The Jenkins Era: Great Tools, Heavy Maintenance&lt;/h2&gt;
&lt;p&gt;Jenkins didn’t earn its reputation by accident. It was flexible, battle-tested, and extensible in a way that made teams feel in control. If you needed a custom build step, a weird environment, or an opinionated deployment flow, you could usually duct-tape it into Jenkins and keep shipping.&lt;/p&gt;
&lt;p&gt;But Jenkins has a cost structure that most developers quietly hate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You own the platform.&lt;/strong&gt; Patching Jenkins, upgrading Java, securing the controller, managing plugins, cleaning up agents—it’s not “set and forget.” It’s ongoing ops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plugins are a supply chain.&lt;/strong&gt; Every plugin is another moving part. Some stay healthy; others accumulate warnings, break with upgrades, or become incompatible with the rest of your setup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The pipeline becomes a second system to administer.&lt;/strong&gt; Even when the Jenkinsfile is clean, someone still has to explain why builds fail “only sometimes,” why a node is stuck, or why an agent label doesn’t match.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The uncomfortable reality: Jenkins made CI/CD &lt;em&gt;a job&lt;/em&gt;. It wasn’t just a workflow—it was an internal product you maintained. That’s a tax even mature teams resent, and a deal-breaker for smaller ones.&lt;/p&gt;
&lt;h2 id="what-changed-cicd-became-source-driven-again"&gt;What Changed: CI/CD Became Source-Driven Again&lt;/h2&gt;
&lt;p&gt;GitHub Actions flipped the mental model. Instead of “run your own CI server,” it became “describe your automation in your repo.”&lt;/p&gt;
&lt;p&gt;A workflow is just YAML living beside your code. That shift sounds small, but it changes everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Review is native.&lt;/strong&gt; When you modify how you build or test, your PR shows exactly what changed. No separate change process. No “trust the pipeline maintainers.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Version control becomes the system of record.&lt;/strong&gt; Roll back a broken pipeline by reverting a commit. That’s the most underrated feature of the GitHub Actions approach.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discovery is easier.&lt;/strong&gt; You don’t have to remember plugin names or hunt down internal docs written by someone who left the company. You search the repository, and the workflow is right there.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider a typical improvement scenario. Teams often start with “we’ll automate builds” and end up with a pipeline that nobody touches because it’s fragile. With GitHub Actions, the barrier drops: developers can iterate without filing infrastructure tickets.&lt;/p&gt;
&lt;p&gt;And yes—YAML can be intimidating. But “intimidating” is a one-time problem. “Nobody wants to babysit Jenkins” is a continuous one.&lt;/p&gt;
&lt;h2 id="marketplace-actions-and-matrix-builds-the-death-of-diy-ci"&gt;Marketplace Actions and Matrix Builds: The Death of DIY CI&lt;/h2&gt;
&lt;p&gt;A big reason Jenkins survived so long is that teams built everything themselves—or stitched together a plugin ecosystem. That approach worked, but it also encouraged bespoke solutions for common tasks: checking out code, setting up language runtimes, caching dependencies, building containers, publishing artifacts.&lt;/p&gt;
&lt;p&gt;GitHub Actions made the default path dramatically easier by combining two ideas:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reusable actions&lt;/strong&gt; for common tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Matrix builds&lt;/strong&gt; for systematic coverage (multiple versions, OSes, or configurations)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Instead of inventing your own “install dependencies” step or writing custom scripts to manage caching, you can use established actions and keep your workflow focused on the logic that’s unique to your application.&lt;/p&gt;
&lt;p&gt;For example, testing a Node.js project across Node 18 and 20 doesn’t have to be a tangle of branches and cron jobs. A matrix turns it into something readable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define the versions once&lt;/li&gt;
&lt;li&gt;Let the runner handle the repetition&lt;/li&gt;
&lt;li&gt;Keep failures scoped to the specific environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s the difference between CI/CD as a craft and CI/CD as a capability.&lt;/p&gt;
&lt;p&gt;And it matters culturally: the more your pipeline resembles declarative intent, the more developers feel ownership instead of fear.&lt;/p&gt;
&lt;h2 id="self-hosted-runners-enterprise-requirements-without-the-jenkins-burden"&gt;Self-Hosted Runners: Enterprise Requirements Without the Jenkins Burden&lt;/h2&gt;
&lt;p&gt;At some point, someone will say: “Sure, hosted runners are nice, but we need enterprise controls.” That’s fair. Some environments require access to private networks, internal artifact registries, compliance constraints, or custom tooling that can’t run on shared infrastructure.&lt;/p&gt;
&lt;p&gt;The good news is that you don’t have to rebuild the Jenkins world to meet those needs.&lt;/p&gt;
&lt;p&gt;GitHub Actions supports &lt;strong&gt;self-hosted runners&lt;/strong&gt;, which let you keep your requirements while removing the “Jenkins maintenance job” from your daily life. In practical terms, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install runners inside your network boundary&lt;/li&gt;
&lt;li&gt;Use labels to target where jobs can run&lt;/li&gt;
&lt;li&gt;Keep the workflow definition in the repo (so the orchestration is still source-driven)&lt;/li&gt;
&lt;li&gt;Scale runners as needed without managing a CI “controller” application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the comparison gets brutal. With Jenkins, “self-hosted” typically means you’re also hosting the orchestration layer—maintaining controllers, plugin compatibility, job configuration, and operational drift. With GitHub Actions, you can host only what must be hosted: the execution environment.&lt;/p&gt;
&lt;p&gt;You end up with enterprise capability without turning your CI/CD platform into a permanent internal engineering program.&lt;/p&gt;
&lt;h2 id="the-real-question-is-anyone-happy-running-jenkins"&gt;The Real Question: Is Anyone Happy Running Jenkins?&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable editorial stance: if your team still runs Jenkins, ask yourself whether Jenkins is solving a problem you genuinely have—not whether you can keep it running.&lt;/p&gt;
&lt;p&gt;A healthy CI/CD setup has one key property: it feels boring. Builds run, tests execute, artifacts publish, deployments follow policy. When something breaks, the failure is actionable.&lt;/p&gt;
&lt;p&gt;Jenkins often creates the opposite experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Builds fail because of infrastructure conditions, not code changes.&lt;/li&gt;
&lt;li&gt;Pipeline behavior is hard to reason about because it’s spread across plugins, scripts, and server state.&lt;/li&gt;
&lt;li&gt;Improvements require knowledge that only a few people have.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the only people who can safely modify the pipeline are “the Jenkins team,” then Jenkins has already achieved the worst possible outcome: CI/CD has become a specialty skill. That defeats the whole purpose of automation.&lt;/p&gt;
&lt;p&gt;And nobody wants to maintain a tool that makes everyday development feel harder. If your CI/CD pipeline requires a dedicated team to keep it alive, your platform isn’t supporting developers—it’s consuming them.&lt;/p&gt;
&lt;p&gt;Migration doesn’t need to be dramatic. Start by moving one workflow: say, “run unit tests on pull requests.” Port the logic, keep the build steps, and compare runtime and developer experience. Over time, you’ll find that replacing orchestration is less work than you feared—and less risky than you expect, precisely because workflows are versioned alongside the code.&lt;/p&gt;
&lt;h2 id="a-practical-path-off-jenkins-without-big-bang-rewrite"&gt;A Practical Path Off Jenkins (Without Big-Bang Rewrite)&lt;/h2&gt;
&lt;p&gt;Nobody should pretend migrations are effortless. But you can make the transition systematic and low drama. A good migration plan focuses on outcomes, not ideology.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Inventory your pipeline responsibilities&lt;/strong&gt;&lt;br&gt;
List what Jenkins does today: build, test, lint, package, publish artifacts, deploy environments, run scheduled jobs, manage credentials.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick a narrow first workflow&lt;/strong&gt;&lt;br&gt;
Choose something with clear inputs/outputs—like “compile + unit test” on every PR. Avoid the most custom deployment logic until you’ve established confidence.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Model stages as jobs and keep steps explicit&lt;/strong&gt;&lt;br&gt;
Treat your workflow like production code: readable naming, small steps, clear failure points. If you need scripts, check them into the repo.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Use caching and artifacts intentionally&lt;/strong&gt;&lt;br&gt;
Dependency caching and artifact passing are where pipeline speed and reliability improve. Do not reinvent caching mechanisms if a ready approach exists; do ensure caches are keyed sensibly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Move secrets and permissions carefully&lt;/strong&gt;&lt;br&gt;
Jenkins credential management and GitHub Actions secrets are different. Plan the migration of tokens, registry credentials, and deployment keys. Least privilege matters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Adopt self-hosted runners only when you must&lt;/strong&gt;&lt;br&gt;
Start on GitHub-hosted runners if possible. Bring in self-hosted runners when you hit real constraints (network access, internal tooling, compliance).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal isn’t to “win” a tool choice. The goal is to make CI/CD so maintainable that developers don’t notice it—except when it speeds them up.&lt;/p&gt;
&lt;h2 id="conclusion-cicd-shouldnt-be-a-separate-engineering-department"&gt;Conclusion: CI/CD Shouldn’t Be a Separate Engineering Department&lt;/h2&gt;
&lt;p&gt;Jenkins was a landmark product, and it earned a decade of loyalty. But modern CI/CD doesn’t need a dedicated maintenance team. GitHub Actions made automation source-driven, composable, and easier to reason about—while self-hosted runners cover legitimate enterprise needs without reintroducing the Jenkins maintenance tax.&lt;/p&gt;
&lt;p&gt;If your Jenkins setup requires constant babysitting, you don’t have CI/CD—you have a second system to operate. And if nobody’s happy about that, it’s time to stop pretending it’s normal.&lt;/p&gt;</content></item><item><title>Cloudflare Workers Are Underrated for Full Application Backends</title><link>https://decastro.work/blog/cloudflare-workers-underrated-full-backends/</link><pubDate>Sat, 08 Oct 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/cloudflare-workers-underrated-full-backends/</guid><description>&lt;p&gt;Most people still think of Cloudflare Workers as “serverless scripts for the edge.” That’s like calling a car a steering wheel. In practice, Workers has matured into a serious backend platform: request handling, global storage, durable coordination, and even a relational database—packaged around V8 isolates and deployed across Cloudflare’s footprint. If you’re building an app that doesn’t need a traditional server estate, Workers can be the cleanest, fastest, and most cost-effective way to run it.&lt;/p&gt;</description><content>&lt;p&gt;Most people still think of Cloudflare Workers as “serverless scripts for the edge.” That’s like calling a car a steering wheel. In practice, Workers has matured into a serious backend platform: request handling, global storage, durable coordination, and even a relational database—packaged around V8 isolates and deployed across Cloudflare’s footprint. If you’re building an app that doesn’t need a traditional server estate, Workers can be the cleanest, fastest, and most cost-effective way to run it.&lt;/p&gt;
&lt;h2 id="from-edge-scripts-to-full-backends-and-why-that-matters"&gt;From edge scripts to full backends (and why that matters)&lt;/h2&gt;
&lt;p&gt;The original appeal of Workers was simple: run code close to users, reduce latency, and avoid managing servers. But the “backend” part sneaks up on you once you stop treating Workers as a place to do small transformations and start building whole systems around it.&lt;/p&gt;
&lt;p&gt;A modern Workers backend typically includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Request logic in Workers&lt;/strong&gt; (routing, auth, rate limiting, rendering responses)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global storage with KV&lt;/strong&gt; (fast reads for configuration, feature flags, sessions-like keys where eventual consistency is acceptable)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Object storage with R2&lt;/strong&gt; (user uploads, generated artifacts, media, backups—without pulling data through a traditional cloud storage setup)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relational data with D1&lt;/strong&gt; (SQLite semantics at the edge for transactional needs)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateful coordination with Durable Objects&lt;/strong&gt; (when you need “one thing at a time” per entity: chat rooms, rate-limit counters, leaderboards, game sessions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async processing patterns&lt;/strong&gt; (queue-style workflows, webhooks, background tasks—without spinning up anything)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The underrated part? This isn’t a patchwork of random services. It’s one platform designed to work together, with consistent deployment ergonomics and an execution model that encourages building smaller, composable services without losing coherence.&lt;/p&gt;
&lt;h2 id="the-engine-v8-isolates-and-why-the-runtime-feels-different"&gt;The engine: V8 isolates and why the runtime feels different&lt;/h2&gt;
&lt;p&gt;Workers run your code on &lt;strong&gt;V8 isolates&lt;/strong&gt;. Practically, that affects performance and reliability in the way you’d expect from a properly sandboxed runtime: fast startup, tight memory usage, and predictable request handling.&lt;/p&gt;
&lt;p&gt;What you feel as a developer is this: you can build backends that respond instantly to traffic spikes without the “warm instance” ritual. Instead of thinking in terms of fleets and autoscaling delays, you think in terms of &lt;strong&gt;stateless request handlers plus explicit state where needed&lt;/strong&gt; (KV, D1, Durable Objects, R2).&lt;/p&gt;
&lt;p&gt;A good pattern is to separate your code into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stateless HTTP layer&lt;/strong&gt; (Workers): validate requests, read/update state, return responses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateful entities&lt;/strong&gt; (Durable Objects): own the coordination and invariants&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage-specific access&lt;/strong&gt; (KV / D1 / R2): use the right tool for the access pattern&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That separation keeps your logic crisp and prevents the common failure mode of “we’ll just store everything in KV,” followed by a debugging spiral when you discover consistency and query limitations.&lt;/p&gt;
&lt;h2 id="kv-and-r2-global-storage-without-the-usual-storage-anxiety"&gt;KV and R2: global storage without the usual storage anxiety&lt;/h2&gt;
&lt;p&gt;Let’s talk about the two storage pillars that many teams overlook because they’re not branded as “databases.”&lt;/p&gt;
&lt;h3 id="kv-for-global-configuration-and-fast-key-lookups"&gt;KV for global configuration and fast key lookups&lt;/h3&gt;
&lt;p&gt;KV shines when you want &lt;strong&gt;low-latency, global reads&lt;/strong&gt; keyed by something simple: &lt;code&gt;tenant:featureFlags&lt;/code&gt;, &lt;code&gt;user:preferences:theme&lt;/code&gt;, &lt;code&gt;promo:currentCampaign&lt;/code&gt;, and so on.&lt;/p&gt;
&lt;p&gt;Example: feature flags per tenant&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the Workers request path:
&lt;ul&gt;
&lt;li&gt;read &lt;code&gt;kv.get(&amp;quot;tenant:&amp;quot; + tenantId + &amp;quot;:flags&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;evaluate flags to choose routes or response behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On flag updates:
&lt;ul&gt;
&lt;li&gt;write new values to KV&lt;/li&gt;
&lt;li&gt;accept that propagation can be non-instant, depending on your design&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your UX can tolerate slight delays between toggling a flag and seeing the effect, KV is perfect. If you need strict read-after-write guarantees for user-critical decisions, you’re better off using D1 or Durable Objects.&lt;/p&gt;
&lt;h3 id="r2-for-application-objects-not-just-files"&gt;R2 for application objects, not just “files”&lt;/h3&gt;
&lt;p&gt;R2 is &lt;strong&gt;S3-compatible object storage&lt;/strong&gt;. The killer feature for many teams is that you avoid the typical “egress tax” mental model that shows up when you use traditional cloud storage as an afterthought.&lt;/p&gt;
&lt;p&gt;Examples where R2 fits naturally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User uploads&lt;/strong&gt; (profile images, documents)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generated artifacts&lt;/strong&gt; (PDF renders, thumbnails, exports)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Import/export pipelines&lt;/strong&gt; (store incoming files, process them, then store results)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups and snapshots&lt;/strong&gt; for non-database assets&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pragmatic workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Client uploads to your backend endpoint.&lt;/li&gt;
&lt;li&gt;Workers validates auth and content type.&lt;/li&gt;
&lt;li&gt;Workers writes the object to R2.&lt;/li&gt;
&lt;li&gt;Workers returns a reference URL or key.&lt;/li&gt;
&lt;li&gt;Optional: a Durable Object or a D1-backed job table tracks processing status.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Even if you later add a frontend CDN, R2 integration stays conceptually simple: store objects, reference them by key, and let the edge deliver.&lt;/p&gt;
&lt;h2 id="d1-sqlite-at-the-edge-for-real-backend-logic"&gt;D1: SQLite at the edge for real backend logic&lt;/h2&gt;
&lt;p&gt;KV is fast, but it’s not transactional. R2 is great, but it’s not queryable like a database. That’s where &lt;strong&gt;D1&lt;/strong&gt; comes in: a &lt;strong&gt;SQLite-based database&lt;/strong&gt; you can query from Workers.&lt;/p&gt;
&lt;p&gt;Use D1 when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Transactions&lt;/strong&gt; (e.g., “create order + create line items + update inventory”)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relational querying&lt;/strong&gt; (filter, sort, join-like behavior where applicable)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic integrity&lt;/strong&gt; (unique constraints and consistent updates)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: a minimal checkout state machine&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Table: &lt;code&gt;orders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;order_items&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Table: &lt;code&gt;payments&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Workers handles the HTTP request, then uses D1 to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create the order row&lt;/li&gt;
&lt;li&gt;record item rows&lt;/li&gt;
&lt;li&gt;update payment status on webhook callbacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This avoids the most common “backend tax” teams pay: shuttling state between services and storage systems until everything becomes glue code. With D1, the transactional core can stay close to the edge.&lt;/p&gt;
&lt;h2 id="durable-objects-the-missing-piece-for-stateful-backends"&gt;Durable Objects: the missing piece for stateful backends&lt;/h2&gt;
&lt;p&gt;Stateless is great—until it isn’t. Many applications have moments where you need coordination, ordering, or per-entity state that can’t be approximated with “eventual consistency and good luck.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Durable Objects&lt;/strong&gt; are the solution. Think of them as entities that live near the edge, encapsulating state and handling messages in a controlled way.&lt;/p&gt;
&lt;p&gt;Use Durable Objects for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chat rooms / group sessions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limiting per user or per API key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Game session state&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency keys&lt;/strong&gt; (prevent duplicate processing)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-step workflows&lt;/strong&gt; that require ordering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: per-user rate limiting with Durable Objects&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Workers receives a request.&lt;/li&gt;
&lt;li&gt;It routes the request to a Durable Object instance keyed by &lt;code&gt;userId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Durable Object increments a counter and enforces limits.&lt;/li&gt;
&lt;li&gt;Workers returns allow/deny based on the object’s authoritative decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is exactly the type of logic that teams traditionally implement with a centralized Redis cluster or a carefully tuned database approach. With Durable Objects, you can keep coordination near where requests arrive, while still making it deterministic.&lt;/p&gt;
&lt;p&gt;The important mental shift: &lt;strong&gt;Workers is your edge runtime; Durable Objects are your stateful runtime.&lt;/strong&gt; Put coordination where it belongs.&lt;/p&gt;
&lt;h2 id="practical-architecture-building-a-full-backend-without-a-server-fleet"&gt;Practical architecture: building a full backend without a server fleet&lt;/h2&gt;
&lt;p&gt;If you want a concrete blueprint, here’s a pattern that holds up well:&lt;/p&gt;
&lt;h3 id="1-http-api-in-workers"&gt;1) HTTP API in Workers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Authentication + request validation&lt;/li&gt;
&lt;li&gt;Routing to Durable Objects / D1 / KV / R2&lt;/li&gt;
&lt;li&gt;Response shaping and error handling&lt;/li&gt;
&lt;li&gt;Minimal business logic that requires no cross-request coordination&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-durable-objects-for-entities"&gt;2) Durable Objects for “entities”&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;One Durable Object per conceptual entity type (e.g., &lt;code&gt;UserSession&lt;/code&gt;, &lt;code&gt;ChatRoom&lt;/code&gt;, &lt;code&gt;ApiKeyLimiter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Methods that process events/messages&lt;/li&gt;
&lt;li&gt;Encapsulated invariants (ordering, dedupe, counters)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-d1-for-durable-transactional-state"&gt;3) D1 for durable transactional state&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Store durable records that your app must query and mutate reliably&lt;/li&gt;
&lt;li&gt;Use D1 when you need constraints, joins, or consistent updates&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-kv-for-read-heavy-configuration-and-flags"&gt;4) KV for read-heavy configuration and flags&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Store feature flags, static mappings, small config blobs&lt;/li&gt;
&lt;li&gt;Design for eventual propagation where appropriate&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="5-r2-for-everything-binary"&gt;5) R2 for everything binary&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Store uploads and generated files&lt;/li&gt;
&lt;li&gt;Keep the “source of truth” for objects in R2&lt;/li&gt;
&lt;li&gt;Reference by object key and let the edge handle delivery&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-sample-endpoint-design"&gt;A sample endpoint design&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /upload&lt;/code&gt;: validates user, writes to R2, stores metadata row in D1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /uploads/:id/process&lt;/code&gt;: triggers a Durable Object message to coordinate processing&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /uploads/:id&lt;/code&gt;: reads metadata from D1 and returns signed/accessible links to R2 objects&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /feature-flags&lt;/code&gt;: reads KV, applies logic in Workers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps your system cohesive. You’re not forcing a single datastore to do everything. You’re matching the tool to the access pattern.&lt;/p&gt;
&lt;h2 id="conclusion-stop-treating-workers-like-a-utilityuse-it-as-your-backend"&gt;Conclusion: stop treating Workers like a utility—use it as your backend&lt;/h2&gt;
&lt;p&gt;Cloudflare Workers is underrated because it doesn’t look like a “backend stack” at first glance. But once you lean into V8 isolates for runtime, KV for global lookups, R2 for objects, D1 for transactional data, and Durable Objects for stateful coordination, the platform behaves like a full application backend—deployed at the edge where latency matters.&lt;/p&gt;
&lt;p&gt;If your app can avoid a traditional server model, Workers can give you the performance benefits without the operational overhead. More importantly, it encourages better architecture: stateless request handling by default, explicit state when needed, and storage that matches how your app actually reads and writes. That’s not just cheaper. It’s cleaner.&lt;/p&gt;</content></item><item><title>Redis Is Not Just a Cache (And You're Underusing It)</title><link>https://decastro.work/blog/redis-not-just-cache-underusing-it/</link><pubDate>Sat, 01 Oct 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/redis-not-just-cache-underusing-it/</guid><description>&lt;p&gt;If your Redis experience is limited to &lt;code&gt;SET&lt;/code&gt; and &lt;code&gt;GET&lt;/code&gt;, you’re treating a Swiss Army knife like a single screwdriver. Redis has matured into a multi-model data structure server that can handle everything from rate limiting and leaderboards to pub/sub messaging and stream processing—often with simpler operational overhead than the alternatives.&lt;/p&gt;
&lt;p&gt;The good news: you don’t need to rewrite your whole system to get value. You just need to start using the right Redis primitives for the job.&lt;/p&gt;</description><content>&lt;p&gt;If your Redis experience is limited to &lt;code&gt;SET&lt;/code&gt; and &lt;code&gt;GET&lt;/code&gt;, you’re treating a Swiss Army knife like a single screwdriver. Redis has matured into a multi-model data structure server that can handle everything from rate limiting and leaderboards to pub/sub messaging and stream processing—often with simpler operational overhead than the alternatives.&lt;/p&gt;
&lt;p&gt;The good news: you don’t need to rewrite your whole system to get value. You just need to start using the right Redis primitives for the job.&lt;/p&gt;
&lt;h2 id="redis-as-a-multi-model-data-structure-server"&gt;Redis as a multi-model data structure server&lt;/h2&gt;
&lt;p&gt;The fastest way to appreciate Redis is to stop thinking in terms of “cache” and start thinking in terms of “data structures with commands designed for specific workflows.”&lt;/p&gt;
&lt;p&gt;Instead of storing one value per key, Redis gives you purpose-built tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lists, sets, and sorted sets&lt;/strong&gt; for different access patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streams&lt;/strong&gt; for event ingestion, retention, and consumer groups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pub/sub&lt;/strong&gt; for lightweight real-time messaging&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hashes&lt;/strong&gt; for structured objects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lua scripting&lt;/strong&gt; for atomic, multi-step logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transactions and server-side primitives&lt;/strong&gt; to coordinate distributed behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means Redis can sit at the center of real application logic—not just performance tuning.&lt;/p&gt;
&lt;h3 id="a-practical-mindset-shift"&gt;A practical mindset shift&lt;/h3&gt;
&lt;p&gt;Ask a better question: “What data structure does my problem resemble?”&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Need &lt;em&gt;ranking&lt;/em&gt;? Use &lt;strong&gt;sorted sets&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Need &lt;em&gt;ordered events&lt;/em&gt;? Use &lt;strong&gt;streams&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Need &lt;em&gt;real-time fan-out&lt;/em&gt;? Use &lt;strong&gt;pub/sub&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Need &lt;em&gt;atomic updates across keys&lt;/em&gt;? Use &lt;strong&gt;Lua&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="pubsub-real-time-notifications-without-the-overhead"&gt;Pub/sub: real-time notifications without the overhead&lt;/h2&gt;
&lt;p&gt;Pub/sub is Redis’s simplest messaging model: publishers send messages to channels; subscribers receive them. It’s a great fit when you want quick, transient distribution and don’t need durable replay.&lt;/p&gt;
&lt;h3 id="example-live-notifications"&gt;Example: live notifications&lt;/h3&gt;
&lt;p&gt;Suppose you have a web app where users get notifications when something changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When an event happens, publish to a channel like &lt;code&gt;notify:user:123&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Subscribers listen and push to WebSocket connections.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PUBLISH notify:user:123 &amp;#34;Your report is ready&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In practice, pub/sub shines for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Live UI updates&lt;/li&gt;
&lt;li&gt;Server-to-server “nudge” messaging&lt;/li&gt;
&lt;li&gt;Broadcasting lightweight state changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="the-gotcha-you-should-plan-for"&gt;The gotcha you should plan for&lt;/h3&gt;
&lt;p&gt;Pub/sub is not durable. If a subscriber is offline, it misses messages. That’s fine for “best effort” updates, but if you need reliability, you should pivot to &lt;strong&gt;Streams&lt;/strong&gt; (more on that next).&lt;/p&gt;
&lt;h2 id="streams-a-lightweight-kafka-alternative"&gt;Streams: a lightweight Kafka alternative&lt;/h2&gt;
&lt;p&gt;Redis Streams are designed for ordered event logs with consumer groups—meaning you can process events asynchronously, at scale, with less system complexity than a full Kafka deployment.&lt;/p&gt;
&lt;h3 id="example-background-jobs-from-events"&gt;Example: background jobs from events&lt;/h3&gt;
&lt;p&gt;Imagine you receive events from an API (e.g., user actions) and need a worker to process them. With Streams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your API appends events to a stream.&lt;/li&gt;
&lt;li&gt;Worker processes read from the stream using a consumer group.&lt;/li&gt;
&lt;li&gt;You can track progress with consumer offsets.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Writing to a stream looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;XADD events:activity * userId 42 action &amp;#34;clicked_button&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Workers then read using &lt;code&gt;XREADGROUP&lt;/code&gt;, claim pending messages when needed, and continue.&lt;/p&gt;
&lt;h3 id="why-this-beats-roll-your-own-queues"&gt;Why this beats “roll-your-own” queues&lt;/h3&gt;
&lt;p&gt;Many teams start by pushing jobs into lists or using ad-hoc keys. Streams are better because they are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ordered&lt;/strong&gt;: event sequencing is preserved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retainable&lt;/strong&gt;: you can keep messages for retries and reprocessing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operationally cohesive&lt;/strong&gt;: offsets and consumer groups help you avoid inventing your own coordination logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Kafka is overkill for your needs—or you want to avoid running a separate distributed log system—Streams are often the pragmatic answer.&lt;/p&gt;
&lt;h3 id="when-streams-arent-enough"&gt;When streams aren’t enough&lt;/h3&gt;
&lt;p&gt;If you need strict, enterprise-grade event log governance, heavy multi-datacenter replication, or deep Kafka ecosystem integrations, Kafka still has advantages. But for many product teams, Streams hit the sweet spot: durable enough, simple enough.&lt;/p&gt;
&lt;h2 id="sorted-sets-ranking-deduplication-and-time-windows-made-easy"&gt;Sorted sets: ranking, deduplication, and time windows made easy&lt;/h2&gt;
&lt;p&gt;Sorted sets (&lt;code&gt;ZSET&lt;/code&gt;) are Redis’s most underused “business logic” primitive. They store members with scores and keep them ordered. That single property unlocks a ton of features that are otherwise annoyingly complex.&lt;/p&gt;
&lt;h3 id="example-real-time-leaderboards"&gt;Example: real-time leaderboards&lt;/h3&gt;
&lt;p&gt;A classic ranking system: you award points and need to show the top users.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ZINCRBY leaderboard:points 10 user:42
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To get the top 10:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ZREVRANGE leaderboard:points 0 9
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What makes this elegant is that the sorted set is always ready to answer ranking queries—no separate database sorting step required.&lt;/p&gt;
&lt;h3 id="example-expiring-leaderboards-and-time-windows"&gt;Example: expiring leaderboards and time windows&lt;/h3&gt;
&lt;p&gt;For “leaderboards for the last 24 hours,” you can store timestamps as scores and periodically prune old entries. A common pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add score = timestamp&lt;/li&gt;
&lt;li&gt;Trim entries older than your window&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ZREMRANGEBYSCORE leaderboard:last24h 0 (currentTime - 86400)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This approach can work surprisingly well for time-bounded metrics without needing a heavyweight analytics pipeline.&lt;/p&gt;
&lt;h2 id="lua-scripting-atomic-multi-step-operations-you-can-actually-trust"&gt;Lua scripting: atomic multi-step operations you can actually trust&lt;/h2&gt;
&lt;p&gt;When distributed systems go wrong, it’s often because multi-step operations aren’t atomic. Redis Lua scripting lets you execute multiple commands server-side as a single atomic operation.&lt;/p&gt;
&lt;p&gt;If you’re doing anything like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;check then set&lt;/li&gt;
&lt;li&gt;decrement with guards&lt;/li&gt;
&lt;li&gt;update multiple keys consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…Lua is your tool of choice.&lt;/p&gt;
&lt;h3 id="example-atomic-rate-limiting-ish-logic"&gt;Example: atomic rate limiting-ish logic&lt;/h3&gt;
&lt;p&gt;Suppose you want to increment a counter only if it doesn’t exceed a threshold, and you want the read/update decision to be consistent.&lt;/p&gt;
&lt;p&gt;Lua runs the logic entirely on the Redis server, so you don’t get race conditions between separate client requests.&lt;/p&gt;
&lt;p&gt;A typical pattern is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Key holds a counter (or hash field)&lt;/li&gt;
&lt;li&gt;Lua reads current value&lt;/li&gt;
&lt;li&gt;If under threshold, it increments and sets expiry if needed&lt;/li&gt;
&lt;li&gt;Otherwise, it returns a denial&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you don’t copy an exact template, the key point is this: &lt;strong&gt;move the “decision” into Redis&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="practical-advice"&gt;Practical advice&lt;/h3&gt;
&lt;p&gt;Lua scripts are power tools—use them deliberately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep scripts small and focused.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;EVALSHA&lt;/code&gt; with cached script hashes for performance.&lt;/li&gt;
&lt;li&gt;Return simple values (&lt;code&gt;0/1&lt;/code&gt;, remaining tokens, current score) so your application logic stays readable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lua turns Redis from a storage layer into a coordination layer.&lt;/p&gt;
&lt;h2 id="putting-it-together-design-patterns-that-actually-work-in-production"&gt;Putting it together: design patterns that actually work in production&lt;/h2&gt;
&lt;p&gt;Once you stop treating Redis as a cache, you can build clean distributed patterns. Here are a few that consistently improve outcomes.&lt;/p&gt;
&lt;h3 id="pattern-1-streams-for-ingestion-setssorted-sets-for-state"&gt;Pattern 1: Streams for ingestion, Sets/Sorted sets for state&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Ingest events into a &lt;strong&gt;stream&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Update ranking or dedup state using &lt;strong&gt;sorted sets&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Use Lua to ensure multi-key consistency during updates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives you both durability (stream) and fast queryability (sorted sets).&lt;/p&gt;
&lt;h3 id="pattern-2-pubsub-for-real-time-ux-streams-for-reliability"&gt;Pattern 2: Pub/sub for real-time UX, Streams for reliability&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Pub/sub for “instant” UI nudges (best effort)&lt;/li&gt;
&lt;li&gt;Streams for everything that must not be lost (auditing, retries, workflows)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This hybrid approach keeps your system responsive without sacrificing correctness where it matters.&lt;/p&gt;
&lt;h3 id="pattern-3-rate-limiting-with-redis-data-structures--server-side-logic"&gt;Pattern 3: Rate limiting with Redis data structures + server-side logic&lt;/h3&gt;
&lt;p&gt;Instead of scattering rate-limit logic across the app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store counters in Redis&lt;/li&gt;
&lt;li&gt;Use Lua to enforce thresholds atomically&lt;/li&gt;
&lt;li&gt;Set expirations to keep memory usage bounded&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your API becomes simpler, and your guarantees become stronger.&lt;/p&gt;
&lt;h2 id="conclusion-redis-isnt-smaller-infrastructureits-smarter-architecture"&gt;Conclusion: Redis isn’t smaller infrastructure—it’s smarter architecture&lt;/h2&gt;
&lt;p&gt;Redis is not “just a cache.” It’s a multi-model data structure platform with messaging (pub/sub, streams), ranking (sorted sets), and atomic computation (Lua). If your current usage is &lt;code&gt;SET&lt;/code&gt; and &lt;code&gt;GET&lt;/code&gt;, you’re leaving reliability, speed, and architectural clarity on the table.&lt;/p&gt;
&lt;p&gt;Pick one feature you currently handle awkwardly—rate limiting, leaderboards, event processing, coordination—and implement it with the Redis primitive that matches the shape of the problem. You’ll be surprised how quickly your system becomes simpler and more robust.&lt;/p&gt;</content></item><item><title>Why I Switched from Jest to Vitest and Never Looked Back</title><link>https://decastro.work/blog/switched-jest-to-vitest-never-looked-back/</link><pubDate>Tue, 20 Sep 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/switched-jest-to-vitest-never-looked-back/</guid><description>&lt;p&gt;For years, I treated Jest as “the way things are done.” It was the default, the safe choice, the thing everyone knew how to configure. Then our codebase grew, our build times got better, and our test times… didn’t. At some point, I realized we weren’t just “waiting for tests”—we were building a second toolchain that fought our actual stack. Switching from Jest to Vitest wasn’t a theoretical upgrade. It was a quality-of-life change that made testing feel like part of development again.&lt;/p&gt;</description><content>&lt;p&gt;For years, I treated Jest as “the way things are done.” It was the default, the safe choice, the thing everyone knew how to configure. Then our codebase grew, our build times got better, and our test times… didn’t. At some point, I realized we weren’t just “waiting for tests”—we were building a second toolchain that fought our actual stack. Switching from Jest to Vitest wasn’t a theoretical upgrade. It was a quality-of-life change that made testing feel like part of development again.&lt;/p&gt;
&lt;h2 id="the-real-problem-with-jest-not-capabilityfriction"&gt;The real problem with Jest: not capability—friction&lt;/h2&gt;
&lt;p&gt;Jest still &lt;em&gt;works&lt;/em&gt;. That’s the trap: when a tool works, it becomes infrastructure, and infrastructure becomes inertia. But in practice, Jest’s pain points showed up in three places:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) Slow feedback loops.&lt;/strong&gt;&lt;br&gt;
Watch mode is supposed to be the thing that keeps you moving. When startup is heavy and transforms are expensive, “run tests on change” turns into “wait, and then maybe get an answer.” That’s not a developer experience; it’s a tax.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) ESM support that never quite felt native.&lt;/strong&gt;&lt;br&gt;
Modern JavaScript projects increasingly live in ESM land: &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt;, ESM-only dependencies, package &lt;code&gt;type: &amp;quot;module&amp;quot;&lt;/code&gt;, and the general desire not to juggle module systems. Jest has improved over time, but the experience can still feel like you’re fighting the boundary between tools rather than building on top of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) Configuration sprawl.&lt;/strong&gt;&lt;br&gt;
Jest’s configuration surface area can expand quickly—transform rules, environment choices, module name mapping, and all the little “just make it pass” settings. It starts small, then it becomes a second webpack-in-miniature. You end up maintaining testing logic that’s &lt;em&gt;unrelated&lt;/em&gt; to your app’s actual build logic.&lt;/p&gt;
&lt;p&gt;And once you’ve felt that friction long enough, you start to notice the irony: your production build is fast and modern, but your tests are stuck in the past.&lt;/p&gt;
&lt;h2 id="what-vitest-changes-it-shares-your-vite-brain"&gt;What Vitest changes: it shares your Vite brain&lt;/h2&gt;
&lt;p&gt;Vitest isn’t just a different test runner. The key idea is brutally simple: &lt;strong&gt;Vitest uses Vite’s transform pipeline&lt;/strong&gt;. That means the same ecosystem assumptions—how your code is parsed, how transforms happen, how your config is interpreted—carry directly into the test environment.&lt;/p&gt;
&lt;p&gt;If you’re already using Vite, this is the difference between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“We’ll test your code with our own interpretation rules.”&lt;/strong&gt; (Jest-style)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“We’ll test your code using the same rules you use to build it.”&lt;/strong&gt; (Vitest-style)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That shared pipeline matters. In real projects, the most annoying failures aren’t always logic bugs—they’re mismatches between how the app code is transformed for runtime and how it’s transformed for tests.&lt;/p&gt;
&lt;p&gt;Vitest also embraces the reality that &lt;strong&gt;ESM is the default mood&lt;/strong&gt;. If your project is ESM-first, you’re not trying to force Jest to pretend otherwise.&lt;/p&gt;
&lt;h2 id="fast-feedback-you-can-actually-feel"&gt;Fast feedback you can actually feel&lt;/h2&gt;
&lt;p&gt;The thing I didn’t expect—at least not emotionally—is how much faster watch mode makes you &lt;em&gt;test more&lt;/em&gt;. When tests run quickly, they stop being a “CI ceremony” and become a habit.&lt;/p&gt;
&lt;p&gt;Here’s a practical way to see the difference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start your dev server (Vite).&lt;/li&gt;
&lt;li&gt;In another terminal, run Vitest watch mode.&lt;/li&gt;
&lt;li&gt;Make a small change in a module and save.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With Vitest, the test runner starts quickly, reuses the Vite transform pipeline, and tends to keep the “save → results” loop tight. The impact isn’t just raw speed; it’s reduced interruption. You iterate in flow, not in pauses.&lt;/p&gt;
&lt;p&gt;Even if your test suite is moderate, the compound effect is real: fewer seconds waiting per iteration adds up to hours saved over a sprint. More importantly, you stop deferring tests until “later,” because later doesn’t arrive.&lt;/p&gt;
&lt;h2 id="migration-is-simpler-than-youre-imagining"&gt;Migration is simpler than you’re imagining&lt;/h2&gt;
&lt;p&gt;The other reason I switched—completely unglamorous, but important—is that &lt;strong&gt;Vitest intentionally offers a Jest-compatible API&lt;/strong&gt;. That means your existing test code usually doesn’t need a rewrite, just a new home.&lt;/p&gt;
&lt;p&gt;In most cases, you can move from Jest to Vitest without drama:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep your &lt;code&gt;describe&lt;/code&gt;, &lt;code&gt;it&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;expect&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keep your matchers and typical assertion flow&lt;/li&gt;
&lt;li&gt;Keep the same mental model of how tests are structured&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-quick-example-a-familiar-test-becomes-vitest"&gt;A quick example: a familiar test becomes Vitest&lt;/h3&gt;
&lt;p&gt;If your Jest test looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;sum&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;./sum&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;describe&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;sum&amp;#39;&lt;/span&gt;, () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;it&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;adds numbers&amp;#39;&lt;/span&gt;, () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;expect&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;sum&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;)).&lt;span style="color:#a6e22e"&gt;toBe&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That test can typically run in Vitest with minimal changes. The bigger shift is not rewriting test logic—it’s updating config and ensuring the test runner sees your files correctly.&lt;/p&gt;
&lt;h3 id="the-only-part-that-usually-needs-attention-setup-and-environment"&gt;The only part that usually needs attention: setup and environment&lt;/h3&gt;
&lt;p&gt;You may need to map over these categories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global setup files&lt;/strong&gt; (e.g., test utilities, polyfills)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DOM vs node environment&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mocking strategy&lt;/strong&gt; if you used Jest-specific helpers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But even then, you’re generally translating &lt;em&gt;configuration&lt;/em&gt;, not refactoring your suite.&lt;/p&gt;
&lt;p&gt;In my experience, the “hard parts” were less about Vitest and more about cleaning up what we’d let Jest-specific cruft accumulate over time.&lt;/p&gt;
&lt;h2 id="using-your-existing-vite-config-instead-of-reinventing-it"&gt;Using your existing Vite config instead of reinventing it&lt;/h2&gt;
&lt;p&gt;One of my favorite parts of the switch is that Vitest plays nicely with your current Vite setup. If you already have path aliases, plugin transforms, or module resolution rules, you can keep them.&lt;/p&gt;
&lt;p&gt;For example, if your Vite config includes aliases like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@/components&lt;/code&gt; → &lt;code&gt;src/components&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;your tests should ideally resolve those same aliases without additional Jest-specific &lt;code&gt;moduleNameMapper&lt;/code&gt; gymnastics.&lt;/p&gt;
&lt;p&gt;That’s not just convenience. It prevents a subtle but common failure mode: tests passing while the app fails (or vice versa) because one tool resolved imports differently.&lt;/p&gt;
&lt;p&gt;If your app uses Vite plugins, Vitest can benefit from the same assumptions. The result: fewer “why does this work in dev but not in tests?” conversations.&lt;/p&gt;
&lt;h2 id="a-practical-migration-plan-that-wont-derail-your-week"&gt;A practical migration plan (that won’t derail your week)&lt;/h2&gt;
&lt;p&gt;Switching a test runner can sound intimidating, so here’s the approach I recommend—incremental, boring, and effective.&lt;/p&gt;
&lt;h3 id="step-1-add-vitest-alongside-jest"&gt;Step 1: Add Vitest alongside Jest&lt;/h3&gt;
&lt;p&gt;Don’t try to “big bang” the entire suite. Start by installing Vitest and creating a Vitest config that fits your existing project.&lt;/p&gt;
&lt;h3 id="step-2-run-a-subset-of-tests"&gt;Step 2: Run a subset of tests&lt;/h3&gt;
&lt;p&gt;Pick a folder—say &lt;code&gt;src/utils&lt;/code&gt;—and migrate those tests first. Confirm that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;imports resolve correctly&lt;/li&gt;
&lt;li&gt;mocks behave as expected&lt;/li&gt;
&lt;li&gt;your environment is correct (DOM vs Node)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-3-port-shared-setup"&gt;Step 3: Port shared setup&lt;/h3&gt;
&lt;p&gt;If you have a Jest setup file (globals, custom matchers, test utilities), translate it to Vitest’s equivalent so your tests keep their expectations.&lt;/p&gt;
&lt;h3 id="step-4-adjust-only-what-breaks"&gt;Step 4: Adjust only what breaks&lt;/h3&gt;
&lt;p&gt;When something fails, resist the urge to reconfigure everything at once. Fix the specific mismatch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;module resolution&lt;/li&gt;
&lt;li&gt;test environment&lt;/li&gt;
&lt;li&gt;mocking differences&lt;/li&gt;
&lt;li&gt;any remaining transform edge cases&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-5-flip-the-default-once-the-suite-is-stable"&gt;Step 5: Flip the default once the suite is stable&lt;/h3&gt;
&lt;p&gt;Once the majority of tests run cleanly under Vitest, update your package scripts and make Vitest the standard.&lt;/p&gt;
&lt;p&gt;One more recommendation: keep CI behavior consistent. If Jest was running &lt;code&gt;--runInBand&lt;/code&gt; (or similar), you’ll want to understand Vitest’s concurrency defaults and tune only if you have a real reason. Speed is a feature—don’t accidentally eliminate it.&lt;/p&gt;
&lt;h2 id="the-conclusion-testing-should-reinforce-development-not-compete-with-it"&gt;The conclusion: testing should reinforce development, not compete with it&lt;/h2&gt;
&lt;p&gt;I didn’t switch from Jest to Vitest because Jest stopped being “good.” I switched because our workflow got better everywhere else—and testing lagged behind in a way that started to matter. Vitest feels like the test runner that finally understands the modern JavaScript toolchain: fast iteration, ESM-native sensibilities, and a configuration model that doesn’t ask you to maintain a second build system.&lt;/p&gt;
&lt;p&gt;If you’re already using Vite for your build, keeping Jest around is mostly symbolic inertia. Vitest doesn’t just run tests—it plugs into the same engine that already knows how your project works. And once you experience watch mode that feels instant, it’s hard to go back to waiting for your tools instead of your code.&lt;/p&gt;</content></item><item><title>GraphQL at Scale: The Complexity You Don't See Coming</title><link>https://decastro.work/blog/graphql-at-scale-complexity-coming/</link><pubDate>Wed, 14 Sep 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/graphql-at-scale-complexity-coming/</guid><description>&lt;p&gt;GraphQL is sold as simplicity: a single endpoint, a typed schema, and clients that ask for exactly what they need. In production, though, GraphQL quietly bills you for everything your team used to “get for free” with REST—until the bill shows up as slow pages, exploding query counts, tangled schemas, and authorization logic that’s impossible to reason about. The demos look elegant. The operations are where the real work starts.&lt;/p&gt;</description><content>&lt;p&gt;GraphQL is sold as simplicity: a single endpoint, a typed schema, and clients that ask for exactly what they need. In production, though, GraphQL quietly bills you for everything your team used to “get for free” with REST—until the bill shows up as slow pages, exploding query counts, tangled schemas, and authorization logic that’s impossible to reason about. The demos look elegant. The operations are where the real work starts.&lt;/p&gt;
&lt;h2 id="the-n1-query-problem-hides-behind-flexible-queries"&gt;The N+1 Query Problem Hides Behind “Flexible Queries”&lt;/h2&gt;
&lt;p&gt;If you’re new to GraphQL, the first time you see nested fields resolve “as expected,” it feels like magic. Then traffic hits, and your database logs turn into a horror movie.&lt;/p&gt;
&lt;p&gt;Here’s the typical failure mode: your resolver resolves a list, but each item triggers another query for nested fields. In GraphQL, that often means N+1 queries—even when your schema and resolvers look perfectly reasonable.&lt;/p&gt;
&lt;h3 id="a-concrete-example"&gt;A concrete example&lt;/h3&gt;
&lt;p&gt;Imagine a schema like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;users&lt;/code&gt; returns a list of users&lt;/li&gt;
&lt;li&gt;each &lt;code&gt;User&lt;/code&gt; has &lt;code&gt;projects&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;each &lt;code&gt;Project&lt;/code&gt; has &lt;code&gt;owner&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A naive resolver chain might do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query DB for users (1 query)&lt;/li&gt;
&lt;li&gt;For each user (N), query projects (N queries)&lt;/li&gt;
&lt;li&gt;For each project (M), query owner (M queries)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s not “one request.” It’s potentially hundreds or thousands of DB operations for a single GraphQL request—especially if the client requests a deep selection set.&lt;/p&gt;
&lt;h3 id="the-practical-fix-batching--caching-dataloader-done-correctly"&gt;The practical fix: batching + caching (DataLoader, done correctly)&lt;/h3&gt;
&lt;p&gt;The standard mitigation is batching at the resolver layer. DataLoader (or an equivalent) groups loads by key per request and executes them in bulk. But the operational gotcha is: you must apply it consistently.&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use one DataLoader instance per request (not global). Global caching can leak authorization context across users.&lt;/li&gt;
&lt;li&gt;Ensure your batch functions respect tenant boundaries and permission checks. A batch that returns too much is worse than N+1—because it’s a security incident waiting to happen.&lt;/li&gt;
&lt;li&gt;Watch for “partial batching.” If only some resolvers use DataLoader, you still pay most of the cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not eliminating database work—you’re preventing it from ballooning with query shape. That’s the difference between “works on my machine” and “still works at 9am on Monday.”&lt;/p&gt;
&lt;h2 id="schema-sprawl-when-typed-turns-into-unmanageable"&gt;Schema Sprawl: When “Typed” Turns into “Unmanageable”&lt;/h2&gt;
&lt;p&gt;GraphQL schemas look tidy in early builds. Then you add more teams, more features, more service ownership, and suddenly the schema is a living map of historical decisions. The result is schema sprawl: types proliferate, responsibilities blur, and changes become risky because everything is connected.&lt;/p&gt;
&lt;p&gt;There are two common reasons this happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Schema becomes a product of convenience&lt;/strong&gt;, not architecture. Teams reuse types across domains because it’s faster than modeling correctly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schema stitching or federation creates a distributed monolith&lt;/strong&gt;—the schema is centralized, but ownership and semantics are spread across systems.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="distributed-monolith-centralized-contract"&gt;Distributed monolith, centralized contract&lt;/h3&gt;
&lt;p&gt;When you “compose” multiple services into a single GraphQL schema, you create a contract that spans teams. That’s good—until you realize that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one service change can break queries in unrelated screens,&lt;/li&gt;
&lt;li&gt;versioning becomes fuzzy because clients only see the schema,&lt;/li&gt;
&lt;li&gt;performance issues are hard to trace because the request crosses boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice that actually helps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Treat schema changes like API changes with explicit ownership. Every type should have a clear “steward.”&lt;/li&gt;
&lt;li&gt;Define boundaries in the schema, not just in code. For example: group types by domain and avoid cross-domain reuse without a shared contract.&lt;/li&gt;
&lt;li&gt;Build automated checks for schema evolution. At minimum: backwards-compatibility tests and “query replay” against production-like data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you let schema sprawl happen, it will eventually demand a cleanup project. The painful part? Clients don’t wait politely for refactors.&lt;/p&gt;
&lt;h2 id="authorization-nightmares-the-field-level-problem-you-must-solve"&gt;Authorization Nightmares: The Field-Level Problem You Must Solve&lt;/h2&gt;
&lt;p&gt;REST authorization often maps cleanly to endpoints: “user can’t access this resource.” GraphQL authorization is nastier because fields are requested selectively. A single query might ask for 20 fields from multiple domains—and each field might require a different permission model.&lt;/p&gt;
&lt;p&gt;That means you end up implementing authorization at a granularity your team didn’t plan for.&lt;/p&gt;
&lt;h3 id="the-trap-permission-logic-scattered-across-resolvers"&gt;The trap: permission logic scattered across resolvers&lt;/h3&gt;
&lt;p&gt;A common early approach is inline checks in each resolver:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;User.email&lt;/code&gt; checks permission A&lt;/li&gt;
&lt;li&gt;&lt;code&gt;User.billingStatus&lt;/code&gt; checks permission B&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Project.confidentialNotes&lt;/code&gt; checks permission C&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This quickly becomes unmaintainable. Worse, it becomes inconsistent. One resolver forgets a check. Another handles it differently. A third does it “during fetching,” which means you might accidentally leak data through side effects or timing differences.&lt;/p&gt;
&lt;h3 id="the-right-pattern-centralize-authorization-decisions"&gt;The right pattern: centralize authorization decisions&lt;/h3&gt;
&lt;p&gt;You want authorization to be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;consistent&lt;/strong&gt; (same rules everywhere),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;composable&lt;/strong&gt; (works across nested selections),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;auditable&lt;/strong&gt; (you can explain why access was granted or denied).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement a field-level authorization mechanism, either through directives, a policy layer, or a wrapper around resolver execution.&lt;/li&gt;
&lt;li&gt;Make authorization decisions depend on the resolved context: user identity, tenant, and relevant parent object.&lt;/li&gt;
&lt;li&gt;Ensure batching doesn’t bypass auth. If you batch-load records, you still must filter or enforce permissions before returning field values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If authorization feels like “framework within a framework,” that’s not a sign you’re doing it wrong—it’s a sign you’re modeling a real problem. The goal is to make that complexity boring.&lt;/p&gt;
&lt;h2 id="query-complexity-limiting-protecting-against-your-own-frontend-team"&gt;Query Complexity Limiting: Protecting Against Your Own Frontend Team&lt;/h2&gt;
&lt;p&gt;GraphQL’s flexibility can be weaponized—accidentally or intentionally. Even if you never expose your API publicly, your own frontend team can accidentally ship an expensive query shape. Or a new feature can trigger a deep nesting selection set. Or a pagination bug can request far more than intended.&lt;/p&gt;
&lt;p&gt;The solution is query complexity limiting: measure the “cost” of a request (depth, estimated resolver work, field weights) and refuse or throttle requests that exceed thresholds.&lt;/p&gt;
&lt;h3 id="complexity-limits-are-an-operational-control-not-a-theoretical-one"&gt;Complexity limits are an operational control, not a theoretical one&lt;/h3&gt;
&lt;p&gt;A realistic approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Assign weights to fields. For example, &lt;code&gt;users&lt;/code&gt; might be 1, &lt;code&gt;projects&lt;/code&gt; might be 3, &lt;code&gt;owner&lt;/code&gt; might be 5.&lt;/li&gt;
&lt;li&gt;Penalize deep nesting. A query that goes five levels deep should not be “free” just because it’s in a single request.&lt;/li&gt;
&lt;li&gt;Consider pagination arguments. A query asking for 1,000 items should cost more than one asking for 20.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start conservative and iterate with real query logs.&lt;/li&gt;
&lt;li&gt;Return a helpful error that your frontend team can act on (“reduce depth,” “use pagination,” or “request smaller page size”).&lt;/li&gt;
&lt;li&gt;Add tooling so developers can test complexity before deploying. Otherwise complexity limiting becomes a support ticket machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is one area where GraphQL differs sharply from REST: clients can shape the server workload. You must respond by shaping—or constraining—that workload.&lt;/p&gt;
&lt;h2 id="performance-observability-graphql-needs-better-tracing-than-you-expect"&gt;Performance Observability: GraphQL Needs Better Tracing Than You Expect&lt;/h2&gt;
&lt;p&gt;Once you move beyond toy workloads, the real question becomes: &lt;em&gt;where is the time going?&lt;/em&gt; With GraphQL, it’s not just “which endpoint is slow.” It’s “which field resolver caused the slowdown,” possibly across multiple services.&lt;/p&gt;
&lt;p&gt;Without strong observability, you’ll end up guessing. And guessing is expensive when queries vary wildly by client.&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instrument resolver execution time per field, not just per request.&lt;/li&gt;
&lt;li&gt;Correlate GraphQL resolver spans with downstream service calls and database queries.&lt;/li&gt;
&lt;li&gt;Track the number of resolver invocations and the count of batched loads per request. If your DataLoader isn’t working, you’ll see it here.&lt;/li&gt;
&lt;li&gt;Log query shape metadata: operation name, selected depth, and expensive fields. You don’t need to store raw queries forever—just enough to analyze performance regressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Opinionated take: treat your GraphQL server like a query execution engine, not a thin HTTP layer. That means applying the same rigor you’d apply to a database—metrics, tracing, and controlled execution paths.&lt;/p&gt;
&lt;h2 id="shipping-graphql-at-scale-a-checklist-that-prevents-regret"&gt;Shipping GraphQL at Scale: A Checklist That Prevents Regret&lt;/h2&gt;
&lt;p&gt;If you’re building GraphQL for production use, don’t rely on good intentions. Build guardrails early:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always batch nested fetching.&lt;/strong&gt; Use per-request DataLoaders (or equivalent) and enforce it consistently across resolvers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Define schema ownership.&lt;/strong&gt; Every type and domain should have a steward. Avoid “schema by accumulation.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralize authorization decisions.&lt;/strong&gt; Field-level rules should be managed in one place with strong context handling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enforce query complexity limits.&lt;/strong&gt; Use weighted costs, depth limits, and pagination-aware rules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invest in tracing and performance visibility.&lt;/strong&gt; Resolver-level instrumentation is non-negotiable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And most importantly: socialize these rules with your product and frontend teams. Complexity limits, auth behavior, and schema boundaries aren’t backend details—they’re developer experience.&lt;/p&gt;
&lt;h2 id="conclusion-the-tax-is-real-but-its-manageable"&gt;Conclusion: The Tax Is Real, But It’s Manageable&lt;/h2&gt;
&lt;p&gt;GraphQL at scale isn’t about abandoning the model—it’s about accepting the operational reality. N+1 queries, schema sprawl, field-level authorization, and complexity attacks aren’t unsolvable. They’re just the costs of giving clients power over query shape.&lt;/p&gt;
&lt;p&gt;Pay attention early, and you can keep GraphQL’s strengths: a precise contract, efficient data fetching, and a schema that actually helps teams move faster. Ignore it, and you’ll discover that “flexible” eventually means “fragile” under load.&lt;/p&gt;</content></item><item><title>Edge Functions Are the Serverless Evolution Nobody Talks About</title><link>https://decastro.work/blog/edge-functions-serverless-evolution/</link><pubDate>Thu, 08 Sep 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/edge-functions-serverless-evolution/</guid><description>&lt;p&gt;Serverless used to mean “write code, forget servers.” But for many teams, it also meant “accept latency tax.” The breakthrough isn’t just that serverless got cheaper or easier—it’s that edge functions moved compute closer to users, turning network delay from an inevitability into a choice. Cloudflare Workers and Deno Deploy didn’t merely improve performance; they quietly rewired how modern deployment strategy should work.&lt;/p&gt;
&lt;h2 id="why-lambda-feels-fastuntil-it-doesnt"&gt;Why Lambda Feels Fast—Until It Doesn’t&lt;/h2&gt;
&lt;p&gt;AWS Lambda (and its cousins) changed application development by making compute elastic. You pay for what you run, you deploy code, and the platform handles scaling. For workloads that aren’t latency-sensitive, Lambda can be a great fit.&lt;/p&gt;</description><content>&lt;p&gt;Serverless used to mean “write code, forget servers.” But for many teams, it also meant “accept latency tax.” The breakthrough isn’t just that serverless got cheaper or easier—it’s that edge functions moved compute closer to users, turning network delay from an inevitability into a choice. Cloudflare Workers and Deno Deploy didn’t merely improve performance; they quietly rewired how modern deployment strategy should work.&lt;/p&gt;
&lt;h2 id="why-lambda-feels-fastuntil-it-doesnt"&gt;Why Lambda Feels Fast—Until It Doesn’t&lt;/h2&gt;
&lt;p&gt;AWS Lambda (and its cousins) changed application development by making compute elastic. You pay for what you run, you deploy code, and the platform handles scaling. For workloads that aren’t latency-sensitive, Lambda can be a great fit.&lt;/p&gt;
&lt;p&gt;But there’s a structural issue Lambda can’t fully erase: regionality. Even when your function is “serverless,” it still runs in a specific cloud region. If your user is halfway across the world, every request has to traverse distance, routing, and congestion before your code even starts. In other words, Lambda optimizes your server management—but not always the physics of delivery.&lt;/p&gt;
&lt;p&gt;Teams often compensate by optimizing backends: caching, query tuning, faster language runtimes, aggressive CDNs, and better keep-alive behavior. Those improvements can be real. Yet for APIs and request-driven logic, the dominant cost often becomes the round trip to the nearest compute region—not the database query itself.&lt;/p&gt;
&lt;p&gt;Edge functions attack that different problem directly: they run your code near the network edge, in many locations simultaneously.&lt;/p&gt;
&lt;h2 id="edge-functions-same-serverless-different-placement"&gt;Edge Functions: Same “Serverless,” Different Placement&lt;/h2&gt;
&lt;p&gt;Edge functions are still serverless in spirit: deploy code without managing servers. The key difference is where that code runs. Instead of a single regional deployment target, edge platforms execute your logic at a large number of points of presence worldwide.&lt;/p&gt;
&lt;p&gt;That placement matters because latency is often dominated by distance. Move compute closer to users and you shrink the time between “user pressed a button” and “your logic replied.” This is why edge approaches feel different: a cached static asset might already be served from the edge, but dynamic logic—auth, request shaping, personalization, transformations—has historically been trapped behind regional compute.&lt;/p&gt;
&lt;p&gt;Cloudflare Workers, for example, run on V8 isolates rather than containerized environments. That architectural choice is less about branding and more about behavior: startup time and request overhead are dramatically reduced compared to traditional cold-start patterns. In plain terms, “spin up” becomes almost a non-event, which is what you want for high fan-out request handling.&lt;/p&gt;
&lt;p&gt;Practical framing: if your workload involves lots of small requests that must respond quickly, edge functions can remove a whole category of latency. If your workload is a heavyweight batch job, it won’t.&lt;/p&gt;
&lt;h2 id="the-real-trade-off-constraints-that-force-better-design"&gt;The Real Trade-off: Constraints That Force Better Design&lt;/h2&gt;
&lt;p&gt;Edge isn’t “unlimited serverless.” It’s serverless with guardrails—meaning you have less flexibility than with Lambda or a container service.&lt;/p&gt;
&lt;p&gt;Common constraints you’ll feel immediately include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No filesystem access (or severe limitations):&lt;/strong&gt; You can’t assume persistent disk semantics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited memory:&lt;/strong&gt; You can’t casually load large models, huge datasets, or heavy in-memory processing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No long-running processes:&lt;/strong&gt; You can’t run background daemons the way you might in a VM or container.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime differences:&lt;/strong&gt; “Works in Node locally” doesn’t always translate perfectly to the edge runtime.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not a deal-breaker; it’s a forcing function. Edge functions are best when the request can be handled quickly and deterministically—when you can treat each request like a self-contained transaction.&lt;/p&gt;
&lt;p&gt;That suggests a different architecture mindset:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep transformations lightweight.&lt;/li&gt;
&lt;li&gt;Stream responses where possible.&lt;/li&gt;
&lt;li&gt;Use external services for heavy lifting.&lt;/li&gt;
&lt;li&gt;Cache results aggressively when safe.&lt;/li&gt;
&lt;li&gt;Design for timeouts and early exits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever built an API and then tried to shoehorn authentication, rate limiting, header normalization, and content rewrites into multiple backend layers, edge functions let you pull some of that work closer to the user—without waiting for a full regional compute pipeline.&lt;/p&gt;
&lt;h2 id="where-edge-functions-shine-with-concrete-examples"&gt;Where Edge Functions Shine (With Concrete Examples)&lt;/h2&gt;
&lt;p&gt;Edge functions are not a replacement for everything. They’re an amplifier for specific classes of problems: request-driven logic that benefits from geographic proximity and fast startup.&lt;/p&gt;
&lt;h3 id="1-authentication-and-authorization-middleware-at-the-edge"&gt;1) Authentication and Authorization Middleware at the Edge&lt;/h3&gt;
&lt;p&gt;Instead of sending every request to a backend just to validate a token, you can verify credentials at the edge and forward only authorized traffic.&lt;/p&gt;
&lt;p&gt;A common pattern: inspect the &lt;code&gt;Authorization&lt;/code&gt; header, validate a JWT signature (or consult an auth endpoint if needed), and then add identity context to upstream requests. Even if you still call a backend for business logic, you’ve removed a round trip and reduced load on your origin.&lt;/p&gt;
&lt;p&gt;Practical advice: keep verification fast. If token validation requires remote calls, cache those results or use a strategy that keeps your edge response time predictable.&lt;/p&gt;
&lt;h3 id="2-request-routing-and-url-rewriting"&gt;2) Request Routing and URL Rewriting&lt;/h3&gt;
&lt;p&gt;For multi-tenant apps, edge logic can map hostnames or paths to the right origin service.&lt;/p&gt;
&lt;p&gt;Example: &lt;code&gt;tenantA.yourapp.com&lt;/code&gt; and &lt;code&gt;tenantB.yourapp.com&lt;/code&gt; can route to different backends, while still serving shared assets from the same CDN. Or you can implement clean URLs that rewrite to internal endpoints without changing the user-facing structure.&lt;/p&gt;
&lt;p&gt;The payoff is real: fewer redirect hops and less backend logic.&lt;/p&gt;
&lt;h3 id="3-content-transformation-but-keep-it-bounded"&gt;3) Content Transformation (But Keep It Bounded)&lt;/h3&gt;
&lt;p&gt;Want to transform HTML responses, rewrite links, or apply small response mutations? Edge is ideal.&lt;/p&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rewrite image URLs to a CDN format.&lt;/li&gt;
&lt;li&gt;Adjust caching headers based on path patterns.&lt;/li&gt;
&lt;li&gt;Inject environment-specific scripts into the response.&lt;/li&gt;
&lt;li&gt;Transform JSON payloads for compatibility between services.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key is to keep transformations small and streaming-friendly. If you need to render a full page from templates, that may be a better fit for edge rendering frameworks—still edge-adjacent, but architected for it.&lt;/p&gt;
&lt;h3 id="4-security-controls-and-bot-mitigation"&gt;4) Security Controls and Bot Mitigation&lt;/h3&gt;
&lt;p&gt;Edge is naturally suited for request filtering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Block known malicious patterns.&lt;/li&gt;
&lt;li&gt;Rate limit by IP or token.&lt;/li&gt;
&lt;li&gt;Enforce header requirements.&lt;/li&gt;
&lt;li&gt;Normalize requests to reduce attack surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You get two advantages: quicker rejection (less wasted work upstream) and consistent enforcement across locations.&lt;/p&gt;
&lt;h3 id="5-personalized-responses-with-near-zero-startup"&gt;5) Personalized Responses with Near-Zero Startup&lt;/h3&gt;
&lt;p&gt;If your personalization is lightweight—say, selecting a theme or variant based on a cookie—you can compute that at the edge and return the correct result immediately.&lt;/p&gt;
&lt;p&gt;Done well, personalization becomes a fast lookup rather than an expensive orchestration step across regions.&lt;/p&gt;
&lt;h2 id="a-practical-migration-strategy-dont-edge-everything"&gt;A Practical Migration Strategy: Don’t “Edge Everything”&lt;/h2&gt;
&lt;p&gt;The best way to adopt edge functions is to target the latency contributors first, not to rewrite your entire stack.&lt;/p&gt;
&lt;p&gt;Here’s a sensible approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Measure before you guess.&lt;/strong&gt; Look at your request waterfall: where does time go? If you see meaningful delay before your backend code starts running, that’s your signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start with one endpoint class.&lt;/strong&gt; Pick a narrow slice like auth validation, header normalization, or URL rewriting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for statelessness.&lt;/strong&gt; Assume each request is independent. If you need data, fetch it efficiently (or cache it).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Choose safe caching.&lt;/strong&gt; If the edge response can be cached without violating user isolation, do it. If not, cache the pieces that are safe (like token verification artifacts or configuration).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrate with your existing infrastructure.&lt;/strong&gt; You don’t have to replace your backend. Let edge handle the first mile, then forward the request.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A common pitfall is using edge functions as a dumping ground for complex business logic. That defeats the point: you’ll end up fighting constraints, complicating debugging, and still paying upstream latency for the work that stays in the origin.&lt;/p&gt;
&lt;p&gt;Instead, treat edge code like the “front porch” of your system: fast checks, quick rewrites, and small transformations that make everything downstream cheaper.&lt;/p&gt;
&lt;h2 id="the-bigger-point-deployment-strategy-is-now-part-of-performance"&gt;The Bigger Point: Deployment Strategy Is Now Part of Performance&lt;/h2&gt;
&lt;p&gt;There’s a deeper reason people “don’t talk about” edge functions enough: they change the conversation from implementation details to deployment geography.&lt;/p&gt;
&lt;p&gt;You can’t optimize your way out of regional distance alone. You can add caching and tune code, but if the request must wait for regional compute before it can even decide what to do, your platform is still paying the network tax. Edge functions turn that decision into an immediate action near the user.&lt;/p&gt;
&lt;p&gt;Cloudflare Workers and Deno Deploy represent a shift in what developers should consider normal: not just “deploy code,” but “deploy logic at the network edge.” The runtime constraints are real, yet they align with modern web needs—short-lived request handling, streaming responses, middleware-like workflows, and fast security enforcement.&lt;/p&gt;
&lt;h2 id="conclusion-edge-functions-are-the-upgrade-you-actually-feel"&gt;Conclusion: Edge Functions Are the Upgrade You Actually Feel&lt;/h2&gt;
&lt;p&gt;Serverless was supposed to remove friction. Edge functions remove another form of friction: the latency that comes from waiting for code to run far away. When your workloads match the edge sweet spot—API middleware, auth, routing, security filters, bounded transformations—Workers-style execution can make your system feel dramatically faster, not just better optimized.&lt;/p&gt;
&lt;p&gt;Adopt edge functions like a product decision: start with one high-impact path, design for statelessness and speed, and let the edge handle the first and most time-critical moments of the request lifecycle.&lt;/p&gt;</content></item><item><title>React Fatigue Is Real, and Svelte Knows It</title><link>https://decastro.work/blog/react-fatigue-real-svelte-knows-it/</link><pubDate>Sat, 27 Aug 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/react-fatigue-real-svelte-knows-it/</guid><description>&lt;p&gt;React didn’t just start a trend—it set expectations. For years, “modern front-end” meant thinking in components, mastering hooks, and treating state as the central character in every app. But somewhere along the way, the day-to-day experience started to feel like a treadmill. Not because React is bad, but because the surrounding ecosystem has become… loud. And in the loudness, developers are getting tired.&lt;/p&gt;
&lt;p&gt;The framework wars aren’t over. They’re just evolving. React fatigue is real, and Svelte’s approach—compile away the framework at build time—hits a nerve: fewer abstractions, less ceremony, code that looks closer to what you actually ship.&lt;/p&gt;</description><content>&lt;p&gt;React didn’t just start a trend—it set expectations. For years, “modern front-end” meant thinking in components, mastering hooks, and treating state as the central character in every app. But somewhere along the way, the day-to-day experience started to feel like a treadmill. Not because React is bad, but because the surrounding ecosystem has become… loud. And in the loudness, developers are getting tired.&lt;/p&gt;
&lt;p&gt;The framework wars aren’t over. They’re just evolving. React fatigue is real, and Svelte’s approach—compile away the framework at build time—hits a nerve: fewer abstractions, less ceremony, code that looks closer to what you actually ship.&lt;/p&gt;
&lt;h2 id="the-hook-era-bargainand-the-interest-that-keeps-accruing"&gt;The hook-era bargain—and the interest that keeps accruing&lt;/h2&gt;
&lt;p&gt;Hooks were a breakthrough. They made function components first-class and unlocked patterns that class components made awkward. But the “hooks era” also introduced a kind of cognitive tax.&lt;/p&gt;
&lt;p&gt;You don’t just write UI anymore. You also write rules for how state changes flow through time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependency arrays&lt;/strong&gt; that must be correct or subtly wrong.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effect ordering&lt;/strong&gt; questions that don’t always have intuitive answers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stale closures&lt;/strong&gt; that appear only under specific timing and render sequences.&lt;/li&gt;
&lt;li&gt;A steady stream of advice like “move logic into effects” or “avoid effects entirely,” often contradicting itself depending on who you ask.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, this means more time spent reasoning about lifecycles than about interfaces. Consider a simple requirement: “When the user selects an item, fetch details and show a spinner, then render the result.” In React, you typically end up with effects, conditional logic, and careful dependency tracking:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;useEffect&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cancelled&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;setLoading&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/api/items/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;r&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;r&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; =&amp;gt; { &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;cancelled&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;setData&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;); })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;finally&lt;/span&gt;(() =&amp;gt; { &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#f92672"&gt;!&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;cancelled&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;setLoading&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;); });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; () =&amp;gt; { &lt;span style="color:#a6e22e"&gt;cancelled&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}, [&lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt;]);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It works, but it’s not inherently “UI code.” It’s choreography code—because the framework requires you to coordinate when and how side effects occur.&lt;/p&gt;
&lt;p&gt;When developers say they’re fatigued, they’re often describing this feeling: &lt;strong&gt;the codebase is teaching you more about the framework than the product.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="state-management-debates-never-end-because-the-problem-never-quite-resolves"&gt;State management debates never end because the problem never quite resolves&lt;/h2&gt;
&lt;p&gt;React’s flexibility is both its superpower and its curse. Almost any state can live anywhere: component state, context, reducers, stores like Redux/Zustand/MobX, server state libraries, caches, query layers, and so on. The ecosystem has solutions for everything—so the argument never fully shuts down.&lt;/p&gt;
&lt;p&gt;You can see the pattern on real teams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;“We need global state.”&lt;/li&gt;
&lt;li&gt;“Actually we just need derived state.”&lt;/li&gt;
&lt;li&gt;“Never mind, we need async caching.”&lt;/li&gt;
&lt;li&gt;“The app will be cleaner with a store.”&lt;/li&gt;
&lt;li&gt;“No, use context + hooks is simpler.”&lt;/li&gt;
&lt;li&gt;“We’ll regret that later.”&lt;/li&gt;
&lt;li&gt;“Let’s rewrite.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;React fatigue is often less about React itself and more about &lt;strong&gt;state architecture churn&lt;/strong&gt;. Even when the team agrees on an approach, the boilerplate and mental overhead can pile up quickly: action types, selectors, memoization, cache invalidation strategies, and “why did this re-render?” investigations.&lt;/p&gt;
&lt;p&gt;To be clear: state management is hard. Every framework faces it. But React’s model tends to encourage debates because it doesn’t prescribe a single, opinionated path for how UI state, server state, and side effects should be wired together.&lt;/p&gt;
&lt;p&gt;Svelte’s pitch is different: less “pick a library for everything,” more “the compiler can do the bookkeeping.”&lt;/p&gt;
&lt;h2 id="meta-framework-churn-when-react-turns-into-a-stack-you-have-to-maintain"&gt;Meta-framework churn: when “React” turns into a stack you have to maintain&lt;/h2&gt;
&lt;p&gt;Then there’s the meta-framework layer. In 2026, “React app” often means a particular combo of routing, rendering strategy, data fetching, bundling, styling conventions, and deployment assumptions. Next.js, Remix, custom SSR setups, edge runtimes, loading patterns, hydration quirks—the list keeps growing.&lt;/p&gt;
&lt;p&gt;The result is a subtle fatigue: teams spend more time staying current with the platform layer than improving product code.&lt;/p&gt;
&lt;p&gt;And the churn isn’t always optional. If you want the “right” conventions for SEO, caching, streaming, or routing, you inherit the framework’s worldview. That worldview then shapes your components, your fetching logic, and even how developers learn to think.&lt;/p&gt;
&lt;p&gt;This is where many React developers start to crave simplicity—not in the sense of less capability, but in the sense of fewer decisions per feature.&lt;/p&gt;
&lt;h2 id="sveltes-radical-move-compile-away-the-framework"&gt;Svelte’s radical move: compile away the framework&lt;/h2&gt;
&lt;p&gt;Svelte’s central idea is disarming: instead of shipping a big runtime that interprets your components, Svelte &lt;strong&gt;compiles your code into efficient JavaScript at build time&lt;/strong&gt;. The framework doesn’t have to “make things work” in the browser because it already transformed your intent into concrete instructions.&lt;/p&gt;
&lt;p&gt;The practical effect is that you write less scaffolding and fewer coordination rituals. React asks you to tell it &lt;em&gt;what state is&lt;/em&gt; and &lt;em&gt;when&lt;/em&gt; to react. Svelte asks you to declare &lt;em&gt;what UI depends on&lt;/em&gt;, and the compiler wires the updates.&lt;/p&gt;
&lt;p&gt;Here’s a stylized comparison. In React, a component might juggle local state, effects, and conditional renders. In Svelte, the reactive dependency model is closer to how many developers naturally describe UI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-svelte" data-lang="svelte"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;$&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/api/items/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;selectedId&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;r&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;r&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#66d9ef"&gt;finally&lt;/span&gt;(() =&amp;gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#66d9ef"&gt;#if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;loading&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;Loading…&amp;lt;/&lt;span style="color:#f92672"&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#66d9ef"&gt;:else&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;{&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;}&amp;lt;/&lt;span style="color:#f92672"&gt;h2&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&lt;span style="color:#66d9ef"&gt;/if&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This doesn’t mean you never need effects or async control. It means the “reactivity plumbing” is less verbose. And because Svelte tracks dependencies at compile time, updates can be more direct, without you constantly reasoning about render cycles and effect timing.&lt;/p&gt;
&lt;p&gt;You still write real logic. You just spend less time translating your logic into framework-specific rituals.&lt;/p&gt;
&lt;h2 id="smaller-surface-area-why-code-feels-closer-to-htmlcssjs"&gt;Smaller surface area: why code feels closer to HTML/CSS/JS&lt;/h2&gt;
&lt;p&gt;One reason Svelte feels like relief is that the code reads like a developer’s mental model of the browser: HTML for structure, CSS for presentation, JS for behavior.&lt;/p&gt;
&lt;p&gt;Svelte components co-locate template and logic in a way that keeps your attention on the UI:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Markup and state are right next to each other.&lt;/li&gt;
&lt;li&gt;Styling stays where it belongs, often in the same file without an extra ceremony layer.&lt;/li&gt;
&lt;li&gt;Event handling is straightforward and doesn’t require a new mental framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;React is absolutely capable of this style—but the surrounding ecosystem often nudges you toward patterns that pull logic into separate files, wrap everything in abstractions, and turn “a component” into a mini-architecture project.&lt;/p&gt;
&lt;p&gt;Svelte doesn’t prevent scaling. It simply gives you a smaller baseline. That matters when you’re building the second and third feature, not just the first.&lt;/p&gt;
&lt;p&gt;Practical advice if you’re considering the switch: don’t treat Svelte as a “new framework to learn.” Treat it as a different default tradeoff. You’ll likely gain speed in everyday development, especially for UI-heavy apps where reactive updates are frequent.&lt;/p&gt;
&lt;h2 id="but-react-isnt-deadsvelte-just-proves-you-can-optimize-developer-experience"&gt;But React isn’t dead—Svelte just proves you can optimize developer experience&lt;/h2&gt;
&lt;p&gt;It’s tempting to declare framework winners like sports fans. That’s not how this plays out.&lt;/p&gt;
&lt;p&gt;React is deeply entrenched. There are entire hiring pipelines, codebases, and libraries built around it. It’s not going away. And React teams can absolutely reduce fatigue—by enforcing architectural conventions, limiting the sprawl of state libraries, and choosing consistent patterns for side effects.&lt;/p&gt;
&lt;p&gt;Still, Svelte’s existence changes the conversation. It proves that a UI framework doesn’t have to ship its own runtime burden to be powerful. It can compile your intent into something closer to hand-written JS. And that shifts the center of gravity back toward developer experience: fewer moving parts, fewer “gotchas,” and fewer places for the app to reinvent the same wheel.&lt;/p&gt;
&lt;p&gt;The real evolution is this: &lt;strong&gt;React fatigue isn’t the end of React—it’s the beginning of a broader push toward simpler mental models.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="conclusion-the-framework-wars-are-evolving-toward-calm"&gt;Conclusion: the framework wars are evolving toward calm&lt;/h2&gt;
&lt;p&gt;React fatigue is real because the ecosystem grew beyond the framework: hooks complexity, endless state debates, and meta-framework churn all add up. Svelte’s response is not a new dashboard of features—it’s a smaller system with a clearer job: compile components into efficient code and let reactivity be driven by dependencies, not rituals.&lt;/p&gt;
&lt;p&gt;React will keep building the web for a long time. But Svelte’s win—developer love, not just benchmarks—signals something important: the next era of front-end isn’t about more abstraction. It’s about &lt;strong&gt;less ceremony, fewer decisions, and code that feels like it belongs to the browser, not to the framework.&lt;/strong&gt;&lt;/p&gt;</content></item><item><title>Deno Was Right About Everything (And It Might Not Matter)</title><link>https://decastro.work/blog/deno-was-right-about-everything/</link><pubDate>Mon, 15 Aug 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/deno-was-right-about-everything/</guid><description>&lt;p&gt;For a while, Deno felt like a corrective lens: the same problem space as Node.js, but with the mistakes acknowledged up front and fixed by design. Secure by default. TypeScript from day one. Web-standard APIs. Clean, predictable tooling. It’s hard to argue with that direction. The uncomfortable twist is that “better” doesn’t automatically win—especially when the winning asset isn’t code, it’s the ecosystem. Deno’s ideas may still be the real story, even if Deno itself doesn’t fully take over.&lt;/p&gt;</description><content>&lt;p&gt;For a while, Deno felt like a corrective lens: the same problem space as Node.js, but with the mistakes acknowledged up front and fixed by design. Secure by default. TypeScript from day one. Web-standard APIs. Clean, predictable tooling. It’s hard to argue with that direction. The uncomfortable twist is that “better” doesn’t automatically win—especially when the winning asset isn’t code, it’s the ecosystem. Deno’s ideas may still be the real story, even if Deno itself doesn’t fully take over.&lt;/p&gt;
&lt;h2 id="the-rewrite-the-universe-instinct-node-never-had"&gt;The “rewrite the universe” instinct Node never had&lt;/h2&gt;
&lt;p&gt;When Ryan Dahl built Node.js, it was a lightning strike: fast, pragmatic, and wildly productive. It also shipped with blind spots that only became obvious at scale—most famously around security. Node’s permission model is effectively “opt out of safety,” which works fine for controlled environments and breaks down in the messy reality of modern deployments.&lt;/p&gt;
&lt;p&gt;Deno’s core instinct was different. Instead of retrofitting safety and structure after the fact, it set defaults that assume you &lt;em&gt;will&lt;/em&gt; run untrusted code, pull dependencies from the internet, and deploy to shared infrastructure. The result is a runtime that forces you to be explicit: if you want filesystem access, you ask for it; if you want network access, you ask for it. That one decision changes how teams reason about threat models.&lt;/p&gt;
&lt;p&gt;It also changes how developers learn. With Deno, you can’t ignore permissions without noticing the friction. In Node, you can ignore permissions all day—until something goes wrong, and then you’re scrambling to patch with wrappers, conventions, or separate processes.&lt;/p&gt;
&lt;h2 id="secure-by-default-the-most-consequential-small-decision"&gt;Secure-by-default: the most consequential “small” decision&lt;/h2&gt;
&lt;p&gt;Deno’s permission flags look like a usability tax—until you realize they’re actually a clarity gain. Consider the most common failure mode for JavaScript services: a dependency pulls code you didn’t expect, and that code reaches out to sensitive resources.&lt;/p&gt;
&lt;p&gt;In Deno, the runtime nudges you toward the safer posture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your script won’t touch the filesystem unless you explicitly grant &lt;code&gt;--allow-read&lt;/code&gt; (or broader).&lt;/li&gt;
&lt;li&gt;It won’t make outbound calls unless you explicitly grant network access (e.g., &lt;code&gt;--allow-net&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;You can tighten scope to what the program needs rather than granting blanket rights.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, teams can turn this into a repeatable workflow. For example, a CLI tool that reads a config file and writes a report doesn’t need full access; it needs narrow read/write permissions. That means the “worst-case” impact of a compromised dependency is capped by the boundaries you set when running the program.&lt;/p&gt;
&lt;p&gt;Node can do permissions too, but it’s not the default experience in the same way. Most teams rely on container boundaries, network policies, and careful deployment practices—good measures, but indirect. Deno tried to make the runtime itself a first-class part of the defense, not an optional add-on.&lt;/p&gt;
&lt;p&gt;If you care about secure-by-default, Deno’s approach is objectively cleaner: fewer hidden assumptions, fewer opportunities to forget the security step, less reliance on tribal knowledge.&lt;/p&gt;
&lt;h2 id="typescript-native-fewer-tools-fewer-gaps"&gt;TypeScript native: fewer tools, fewer gaps&lt;/h2&gt;
&lt;p&gt;Deno’s other big bet was to treat TypeScript as a &lt;em&gt;native&lt;/em&gt; concern rather than a build-time afterthought. In a Node world, TypeScript often means one or more of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a compilation step,&lt;/li&gt;
&lt;li&gt;a bundler step,&lt;/li&gt;
&lt;li&gt;a runtime loader or transpiler,&lt;/li&gt;
&lt;li&gt;and sometimes “just enough” configuration to make it all work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can make it solid—but you’re building a pipeline. Deno largely collapsed that pipeline into the runtime workflow.&lt;/p&gt;
&lt;p&gt;This matters for day-to-day engineering because friction multiplies. A typical Node+TypeScript project can become a maze of config files: &lt;code&gt;tsconfig&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt; scripts, tooling versions, and sometimes edge-case handling for module formats. Deno’s workflow encourages a single mental model: write TypeScript, run it, get predictable behavior, keep moving.&lt;/p&gt;
&lt;p&gt;Even if you eventually bundle or compile for production, “TypeScript native” still pays dividends during development: faster feedback loops and fewer configuration seams where bugs hide.&lt;/p&gt;
&lt;p&gt;The punchline: Deno didn’t just make TypeScript convenient; it made correctness more continuous. You spend less time chasing tooling mismatch and more time fixing logic.&lt;/p&gt;
&lt;h2 id="url-imports-and-web-standards-the-boring-power-move"&gt;URL imports and web standards: the boring power move&lt;/h2&gt;
&lt;p&gt;Deno’s module story is another area where it felt like Deno was playing a different game. Node’s module ecosystem is deeply tied to the package manager and registry. Deno’s design, by contrast, leans toward URL imports and web-standard compatibility—so dependencies can be fetched and used in a way that resembles how the web itself works.&lt;/p&gt;
&lt;p&gt;At minimum, that changes how you think about dependency resolution and version control. In Deno, “where code came from” can be more explicit. A URL points to a specific origin; that makes it easier to reason about provenance and caching. It also reduces the mental overhead of the “package is named X, but what actually installed?” question that haunts npm-heavy workflows.&lt;/p&gt;
&lt;p&gt;This is not just aesthetic. The moment you’re dealing with serverless functions, edge runtimes, or CI systems that need reproducibility, having a cleaner import model helps you keep builds deterministic without layering on complexity.&lt;/p&gt;
&lt;p&gt;And Deno’s push toward web-standard APIs means your JavaScript model is closer to what browsers and other runtimes already understand. That reduces the gap between “web developer” and “backend developer,” which is good for teams and good for recruiting.&lt;/p&gt;
&lt;h2 id="the-ecosystem-moat-is-realand-denos-truce-is-an-admission"&gt;The ecosystem moat is real—and Deno’s truce is an admission&lt;/h2&gt;
&lt;p&gt;Here’s where opinion becomes math: Node wins because npm wins. The package ecosystem is an unassailable moat not because it’s perfect, but because it’s everywhere. Millions of developers have already invested in Node-compatible libraries. Enterprises have locked in on them. Tutorials, internal tools, and automation scripts assume npm.&lt;/p&gt;
&lt;p&gt;Deno’s npm compatibility layer exists because the only way to beat a moat is either to drain it slowly or to tunnel through it. Deno chose the tunnel: “you want to use existing npm packages? Fine—let’s integrate.”&lt;/p&gt;
&lt;p&gt;That’s not a failure of Deno’s ideas. It’s a recognition that language and runtime design don’t operate in a vacuum. If your goal is to move quickly with real-world libraries—databases, SDKs, auth integrations, web frameworks—you can’t ignore the ecosystem you inherit.&lt;/p&gt;
&lt;p&gt;So Deno’s best move might not be to replace Node’s entire universe. It might be to keep applying pressure until Node modernizes its defaults. When runtimes compete, ecosystems decide the pace. But ecosystems also respond to pain: security concerns, TypeScript friction, and the constant desire for more consistent module resolution.&lt;/p&gt;
&lt;h2 id="what-denos-legacy-could-look-like-node-pressure-safer-defaults"&gt;What Deno’s legacy could look like: Node pressure, safer defaults&lt;/h2&gt;
&lt;p&gt;The most likely future isn’t “Deno replaces Node.” It’s “Deno changes what ‘normal’ means.” Even without claiming victory, Deno forces an honest comparison.&lt;/p&gt;
&lt;p&gt;When developers and teams build with Deno, they experience its defaults as the baseline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;secure permissions that don’t depend on organizational maturity,&lt;/li&gt;
&lt;li&gt;TypeScript that isn’t bolted on,&lt;/li&gt;
&lt;li&gt;tooling that doesn’t splinter the dev loop,&lt;/li&gt;
&lt;li&gt;APIs that feel familiar if you’ve lived in the browser world.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And once you’ve tasted those defaults, you notice when the alternatives require more ceremony to achieve the same outcome.&lt;/p&gt;
&lt;p&gt;In other words, Deno may matter most as a forcing function. Node’s momentum is real, but so are the questions Deno asked out loud. “Why is security opt-out?” “Why isn’t TypeScript first-class everywhere?” “Why does module resolution feel like a pile of compromises?” Once those questions are mainstream, competitors don’t just sell features—they evolve their assumptions.&lt;/p&gt;
&lt;p&gt;If you’re working in Node today, the practical lesson is to adopt Deno’s mindset even if you don’t adopt the runtime. Make security explicit. Treat TypeScript as part of your core workflow. Push for predictable module resolution and reproducible dependency handling. Use CI to catch configuration drift. Document runtime permissions like you document environment variables. These are not Deno-specific ideas; Deno just made them obvious.&lt;/p&gt;
&lt;h2 id="a-practical-way-to-evaluate-deno-without-fan-fiction"&gt;A practical way to evaluate Deno (without fan fiction)&lt;/h2&gt;
&lt;p&gt;If you’re considering Deno for real projects, don’t evaluate it as a “replacement for Node.” Evaluate it as a tool that optimizes for specific engineering priorities.&lt;/p&gt;
&lt;p&gt;Ask yourself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do you run untrusted code or frequently pull third-party dependencies?&lt;/strong&gt;&lt;br&gt;
If yes, secure-by-default is not a nice-to-have; it’s a risk reducer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Is your TypeScript workflow currently heavy or inconsistent?&lt;/strong&gt;&lt;br&gt;
If your dev loop involves too many moving parts, Deno’s native approach can simplify the system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do you want web-standard API familiarity and cleaner module semantics?&lt;/strong&gt;&lt;br&gt;
If you’re building for environments that share web principles, Deno reduces translation layers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Do you rely heavily on npm-only libraries with no Deno-native equivalents?&lt;/strong&gt;&lt;br&gt;
If yes, you’ll likely spend time validating compatibility layers and edge cases. Not necessarily a deal-breaker, but it’s real work.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal isn’t purity. It’s fit. Deno shines when its defaults align with your constraints; Node shines when its ecosystem alignment matches your reality.&lt;/p&gt;
&lt;h2 id="conclusion-denos-ideas-already-woneven-if-deno-doesnt"&gt;Conclusion: Deno’s ideas already won—even if Deno doesn’t&lt;/h2&gt;
&lt;p&gt;Deno was right about a lot. It treated security as a default rather than a feature request. It made TypeScript integral instead of optional. It leaned toward web standards and more explicit dependency provenance. Those are objectively better design choices for modern software.&lt;/p&gt;
&lt;p&gt;But software isn’t just design—it’s adoption, inertia, and the gravitational pull of npm. Deno’s npm compatibility isn’t a defeat; it’s the acknowledgement that ecosystems are the true battleground. Even so, Deno’s greatest legacy may be forcing Node to modernize its defaults by making the “better way” impossible to ignore.&lt;/p&gt;
&lt;p&gt;In the end, the win might not be “Deno takes over.” It might be “the industry learns from Deno’s corrections—and stops requiring heroic effort to be safe.”&lt;/p&gt;</content></item><item><title>Stop Premature Abstractions: Write the Code Three Times Before You Generalize</title><link>https://decastro.work/blog/stop-premature-abstractions-rule-of-three/</link><pubDate>Mon, 08 Aug 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/stop-premature-abstractions-rule-of-three/</guid><description>&lt;p&gt;You don’t “fail fast” by abstracting early—you fail later, with interest. Premature generalization feels clean in the moment, but it quietly welds together parts of your system that haven’t earned a relationship yet. The result is a codebase that’s technically elegant and practically unchangeable.&lt;/p&gt;
&lt;p&gt;The fix is not to avoid abstraction forever. It’s to delay it—until you’ve earned it. The rule of three is simple: write the code three times before you generalize. By then, you understand the variation space, and your abstraction has a fighting chance of being the right one instead of the &lt;em&gt;most confident wrong thing&lt;/em&gt;.&lt;/p&gt;</description><content>&lt;p&gt;You don’t “fail fast” by abstracting early—you fail later, with interest. Premature generalization feels clean in the moment, but it quietly welds together parts of your system that haven’t earned a relationship yet. The result is a codebase that’s technically elegant and practically unchangeable.&lt;/p&gt;
&lt;p&gt;The fix is not to avoid abstraction forever. It’s to delay it—until you’ve earned it. The rule of three is simple: write the code three times before you generalize. By then, you understand the variation space, and your abstraction has a fighting chance of being the right one instead of the &lt;em&gt;most confident wrong thing&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="the-real-enemy-isnt-duplicationits-premature-certainty"&gt;The Real Enemy Isn’t Duplication—It’s Premature Certainty&lt;/h2&gt;
&lt;p&gt;Most developers treat duplication as a moral failing. That’s backwards. Duplication is often the symptom of a deeper truth: you don’t yet know what should vary.&lt;/p&gt;
&lt;p&gt;When you extract too early, you’re not “DRYing up.” You’re making bets about future requirements using today’s assumptions. Those assumptions become constraints. And constraints become coupling. The coupling doesn’t always show up as bugs right away; it shows up as refactoring friction later, when you need to change one case and suddenly the “shared” code won’t fit.&lt;/p&gt;
&lt;p&gt;A practical way to frame it: if you can’t explain, &lt;em&gt;in plain language&lt;/em&gt;, what might differ between the cases you’re about to unify, you’re not ready to abstract. You’re ready to copy, at least temporarily.&lt;/p&gt;
&lt;h2 id="why-second-time-abstractions-create-coupling-you-cant-undo"&gt;Why “Second Time” Abstractions Create Coupling You Can’t Undo&lt;/h2&gt;
&lt;p&gt;The second time you encounter a pattern, you feel a little spark of “I’ve seen this before.” That’s the exact moment to resist.&lt;/p&gt;
&lt;p&gt;Here’s a common scenario. You have two flows that both “process orders,” and they look similar enough to tempt you into a shared function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flow A: validates input, calculates totals, applies discounts, then writes an invoice.&lt;/li&gt;
&lt;li&gt;Flow B: validates input differently, calculates totals with tax rules, may apply promotions, then writes a receipt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the surface, both flows perform “order processing.” If you abstract on the second occurrence, you’ll likely create an interface that works for A and B &lt;em&gt;as you understand them today&lt;/em&gt;. The moment you add Flow C—say, for a subscription or a marketplace order—you discover the abstraction’s seams were in the wrong places. Now the shared function has become a funnel of conditional logic, special-case flags, or a sprawling parameter list that no one wants to call.&lt;/p&gt;
&lt;p&gt;At that point, you don’t refactor an abstraction—you &lt;em&gt;patch it&lt;/em&gt;. And patching abstractions is how teams end up with frameworks that only the original author can safely change.&lt;/p&gt;
&lt;p&gt;Duplication, by contrast, is honest. When requirements diverge, the duplicated code can diverge too—without forcing everyone through the same narrow abstraction.&lt;/p&gt;
&lt;h2 id="the-rule-of-three-a-scheduling-strategy-for-abstractions"&gt;The Rule of Three: A Scheduling Strategy for Abstractions&lt;/h2&gt;
&lt;p&gt;The rule of three isn’t a religious commandment. It’s a workflow rule.&lt;/p&gt;
&lt;p&gt;When you see repeated code, don’t immediately extract. Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Duplicate once more on purpose.&lt;/strong&gt; Keep the code separate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observe the differences across occurrences.&lt;/strong&gt; Names, types, branching, side effects, performance needs—anything that forces divergence matters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only then generalize.&lt;/strong&gt; After you’ve written three versions and felt the shape of variation, extract the stable idea.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Why three? Because the first two instances often share a superficial similarity. The third usually reveals what was missing—what changes, what doesn’t, and what you were about to force into a one-size-fits-all mold.&lt;/p&gt;
&lt;p&gt;Think of it like building a model. Two data points let you draw a line. Three points help you identify curvature. In software, the “curvature” is where business logic and system constraints start refusing to conform to your early abstractions.&lt;/p&gt;
&lt;h2 id="concrete-example-common-validation-is-usually-a-trap"&gt;Concrete Example: “Common” Validation Is Usually a Trap&lt;/h2&gt;
&lt;p&gt;Let’s say you’re building an API and you keep writing validation code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Endpoint &lt;code&gt;/users&lt;/code&gt; validates required fields, checks email format, and ensures username uniqueness.&lt;/li&gt;
&lt;li&gt;Endpoint &lt;code&gt;/admins&lt;/code&gt; validates required fields, checks email format, and enforces a different uniqueness rule.&lt;/li&gt;
&lt;li&gt;Endpoint &lt;code&gt;/invites&lt;/code&gt; validates required fields, checks email format, but also validates token expiry and rate limits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you abstract after the second endpoint, you might build something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;validateUserCommon(data, rules, uniquenessService)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It will look great until &lt;code&gt;/invites&lt;/code&gt; shows up. Now your “common” validator needs to understand token semantics and rate limiting, which are not user semantics. So you extend the interface with more parameters. Then you add optional callbacks. Then you sprinkle conditional branches. Eventually your abstraction becomes an orchestration layer with flags like &lt;code&gt;isInvite&lt;/code&gt; and &lt;code&gt;enforceRateLimit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You’ve turned duplication into a framework. It’s not DRY—it’s DRY-ISH, and it’s fragile.&lt;/p&gt;
&lt;p&gt;If you waited, you could identify the real stable core: “validate fields and normalize errors,” while allowing the rest to plug in. After three occurrences, you can design a clean boundary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shared normalization and error formatting&lt;/li&gt;
&lt;li&gt;separate strategy objects (or functions) for domain-specific checks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The abstraction becomes smaller and more correct, because it’s based on what truly varies—not on the similarity you observed too early.&lt;/p&gt;
&lt;h2 id="when-duplication-is-the-better-engineering-trade"&gt;When Duplication Is the Better Engineering Trade&lt;/h2&gt;
&lt;p&gt;It’s worth stating plainly: duplication is not inherently bad. The goal is not to eliminate repeated lines—it’s to manage change.&lt;/p&gt;
&lt;p&gt;Duplication can be cheaper because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It isolates blast radius.&lt;/strong&gt; Changing one variant doesn’t force every caller through the same contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It preserves local intent.&lt;/strong&gt; Readers see domain logic directly, rather than decoding a generic “framework” layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It makes variation visible.&lt;/strong&gt; Differences become obvious instead of hidden behind abstraction parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trick is to be disciplined about the &lt;em&gt;kind&lt;/em&gt; of duplication you tolerate. There’s a difference between “copying with care” and “copy-pasting chaos.”&lt;/p&gt;
&lt;p&gt;A pragmatic guideline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Duplicate &lt;em&gt;logic that is likely to diverge&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Abstract &lt;em&gt;mechanics that are clearly stable and well-understood&lt;/em&gt; (e.g., pure formatting utilities, trivial adapters, obvious invariants).&lt;/li&gt;
&lt;li&gt;Don’t abstract &lt;em&gt;business rules&lt;/em&gt; until you’ve seen them collide across multiple real cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words: it’s okay to repeat yourself while your understanding is still forming. It’s not okay to repeat unclear intent forever.&lt;/p&gt;
&lt;h2 id="what-write-three-times-looks-like-in-practice"&gt;What “Write Three Times” Looks Like in Practice&lt;/h2&gt;
&lt;p&gt;You need a method that doesn’t collapse under its own effort. Here’s a pattern teams can adopt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create a tiny “compare window.”&lt;/strong&gt; Keep the duplicated code close in your repository (same module or feature folder) so differences are easy to spot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name the duplicates as if they’re temporary.&lt;/strong&gt; For example, &lt;code&gt;validateUser_v1&lt;/code&gt;, &lt;code&gt;validateUser_v2&lt;/code&gt; feels awkward, but &lt;em&gt;comments and commit history&lt;/em&gt; can serve the purpose. The point is to signal intent: “we’re learning.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add targeted tests for each occurrence.&lt;/strong&gt; As you write the second and third versions, tests teach you what the system truly needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;After the third, write down the shared intent.&lt;/strong&gt; Ask: What concept repeats? What varies? What would an interface need to expose? If you can’t answer, you’re not ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refactor only once.&lt;/strong&gt; When you extract, do it decisively. Partial abstractions that half-bind everything are the worst of both worlds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, be honest about scope. This rule is most valuable in the parts of your code where domain logic and requirements churn—request handling, business workflows, integrations, pricing rules, permissions. In low-change utilities, abstractions may be fine earlier because the variation space is smaller and more predictable.&lt;/p&gt;
&lt;h2 id="conclusion-earn-abstractions-dont-claim-them"&gt;Conclusion: Earn Abstractions, Don’t Claim Them&lt;/h2&gt;
&lt;p&gt;Premature abstraction is the architectural version of a false positive: it looks like progress, but it smuggles in assumptions that will cost you later. The rule of three keeps your codebase honest by delaying generalization until you’ve actually observed the variation space.&lt;/p&gt;
&lt;p&gt;Duplicate with intent, learn through repetition, then abstract once the pattern is real. Your future self will thank you—not with compliments, but with the rarest gift in engineering: refactoring that doesn’t turn into surgery.&lt;/p&gt;</content></item><item><title>The Case Against Microservices for Your Startup</title><link>https://decastro.work/blog/case-against-microservices-startup/</link><pubDate>Wed, 03 Aug 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/case-against-microservices-startup/</guid><description>&lt;p&gt;Microservices are the startup equivalent of putting turbochargers on a bicycle. It feels ambitious. It looks modern. And it’s usually the fastest way to blow your budget, derail your delivery, and lose your team to production fire drills. For most startups, the “correct” architecture isn’t the one with the most moving parts—it’s the one you can change confidently.&lt;/p&gt;
&lt;h2 id="microservices-sell-a-storybut-you-buy-an-operating-system"&gt;Microservices Sell a Story—But You Buy an Operating System&lt;/h2&gt;
&lt;p&gt;The most common origin story for microservices is simple: “Netflix does it, so we should too.” That’s not strategy; it’s cosplay.&lt;/p&gt;</description><content>&lt;p&gt;Microservices are the startup equivalent of putting turbochargers on a bicycle. It feels ambitious. It looks modern. And it’s usually the fastest way to blow your budget, derail your delivery, and lose your team to production fire drills. For most startups, the “correct” architecture isn’t the one with the most moving parts—it’s the one you can change confidently.&lt;/p&gt;
&lt;h2 id="microservices-sell-a-storybut-you-buy-an-operating-system"&gt;Microservices Sell a Story—But You Buy an Operating System&lt;/h2&gt;
&lt;p&gt;The most common origin story for microservices is simple: “Netflix does it, so we should too.” That’s not strategy; it’s cosplay.&lt;/p&gt;
&lt;p&gt;Netflix can afford microservices because it has thousands of engineers and years of operational maturity. It also has business realities that make that complexity worthwhile: high traffic, many teams shipping concurrently, and the organizational need to isolate deployments across large domains. In other words, microservices aren’t just a technical choice—they’re an organizational investment.&lt;/p&gt;
&lt;p&gt;Startups rarely have that luxury. If you have a small engineering team, microservices don’t just increase technical complexity; they increase &lt;em&gt;coordination tax&lt;/em&gt;. Every new service becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new repository and release process (even if you try to keep it “light”)&lt;/li&gt;
&lt;li&gt;A new deployment target&lt;/li&gt;
&lt;li&gt;A new failure mode to understand&lt;/li&gt;
&lt;li&gt;A new surface area for authentication, authorization, and rate limiting&lt;/li&gt;
&lt;li&gt;A new set of logs, dashboards, alerts, and runbooks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if you build it “right,” the system you’re creating is not just code—it’s an operating model. That operating model consumes time you could have spent building the product.&lt;/p&gt;
&lt;h2 id="the-hidden-cost-distributed-complexity-is-real-and-its-not-free"&gt;The Hidden Cost: Distributed Complexity Is Real (and It’s Not Free)&lt;/h2&gt;
&lt;p&gt;Microservices trade one kind of complexity for another. The popular pitch is: “We reduce the complexity of large codebases by splitting them into smaller services.” True—until the complexity moves into the network.&lt;/p&gt;
&lt;p&gt;Once you distribute the system, you inherit a zoo of problems that monoliths mostly avoid:&lt;/p&gt;
&lt;h3 id="1-service-discovery-and-deployment-choreography"&gt;1) Service discovery and deployment choreography&lt;/h3&gt;
&lt;p&gt;If Service A needs Service B, you now need to handle service discovery, versioning, rollout strategies, and backward compatibility. That turns every change into a coordination event rather than a local refactor.&lt;/p&gt;
&lt;h3 id="2-distributed-tracing-and-debugging"&gt;2) Distributed tracing and debugging&lt;/h3&gt;
&lt;p&gt;When something breaks, you don’t just inspect one stack trace. You trace a request across multiple services and environments, often with partial data. You’ll eventually develop muscle memory for debugging graphs instead of code.&lt;/p&gt;
&lt;h3 id="3-eventual-consistency-and-data-ownership"&gt;3) Eventual consistency and data ownership&lt;/h3&gt;
&lt;p&gt;The moment you avoid a shared database, you face the question: “Who owns the truth?” In practice, you end up with replicated data, asynchronous updates, and edge cases like “user sees stale credit balance for two minutes.” Those edge cases aren’t theoretical—they become real user-reported bugs.&lt;/p&gt;
&lt;h3 id="4-network-partitions-and-retry-storms"&gt;4) Network partitions and retry storms&lt;/h3&gt;
&lt;p&gt;Networks fail. Even in “healthy” systems, timeouts happen. The blast radius of a failure can grow dramatically if retries are unbounded or if backpressure is missing.&lt;/p&gt;
&lt;p&gt;A monolith doesn’t eliminate failures—it just keeps failure modes local enough that your team can reason about them quickly. Microservices push you into cross-team and cross-service failure thinking long before you’ve earned it.&lt;/p&gt;
&lt;h2 id="but-we-need-to-scale-start-with-the-kind-of-scaling-you-can-actually-predict"&gt;“But We Need to Scale”: Start With the Kind of Scaling You Can Actually Predict&lt;/h2&gt;
&lt;p&gt;There’s a persuasive but flawed assumption behind many microservices migrations: “We’ll need independent scaling.” It’s not wrong—just uncommon early.&lt;/p&gt;
&lt;p&gt;Most startups scale first in predictable ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A single app backend handles requests&lt;/li&gt;
&lt;li&gt;One or two datastores grow&lt;/li&gt;
&lt;li&gt;A bottleneck appears in a specific module (search, reporting, payments integration, or async processing)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When that happens, you don’t need to split the whole system—you need to &lt;em&gt;target the bottleneck&lt;/em&gt;. Monoliths are extremely capable at this, especially when structured with clear module boundaries.&lt;/p&gt;
&lt;p&gt;A well-structured monolith lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep module interfaces explicit&lt;/li&gt;
&lt;li&gt;Refactor safely without distributed versioning&lt;/li&gt;
&lt;li&gt;Centralize observability&lt;/li&gt;
&lt;li&gt;Enforce data invariants where they belong&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, when you have evidence, you can extract a component.&lt;/p&gt;
&lt;p&gt;Here’s the pragmatic way to think about it: &lt;strong&gt;extract when you can name a business or technical constraint that forces independence&lt;/strong&gt;. Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A module needs to scale 20x faster than the rest&lt;/li&gt;
&lt;li&gt;A team needs to own a domain with different deployment cadence&lt;/li&gt;
&lt;li&gt;A subsystem has specialized compute needs (e.g., heavy batch processing)&lt;/li&gt;
&lt;li&gt;A data boundary is genuinely stable and benefits from isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Until you can point to one of those, microservices are more likely to be a guess than a plan.&lt;/p&gt;
&lt;h2 id="the-boring-wins-monolith-playbook-yes-you-can-still-be-modern"&gt;The “Boring Wins” Monolith Playbook (Yes, You Can Still Be Modern)&lt;/h2&gt;
&lt;p&gt;A monolith doesn’t mean a junk drawer. “Boring” is only boring if you treat the codebase like it’s disposable. If you want a monolith that won’t collapse under growth, you need discipline.&lt;/p&gt;
&lt;h3 id="design-for-module-boundaries-from-day-one"&gt;Design for module boundaries from day one&lt;/h3&gt;
&lt;p&gt;Even in a single deployable, you can structure code as modules with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clear ownership&lt;/li&gt;
&lt;li&gt;Explicit interfaces&lt;/li&gt;
&lt;li&gt;Minimal cross-module coupling&lt;/li&gt;
&lt;li&gt;Dedicated “application services” that coordinate work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Treat module boundaries like contracts. They will later make extraction straightforward, if and when you choose it.&lt;/p&gt;
&lt;h3 id="use-an-async-boundary-where-it-truly-helps"&gt;Use an async boundary where it truly helps&lt;/h3&gt;
&lt;p&gt;A lot of systems benefit from asynchronous workflows without microservices. You can introduce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A job queue for background tasks&lt;/li&gt;
&lt;li&gt;Event-driven updates within the same service boundary&lt;/li&gt;
&lt;li&gt;Dedicated workers that handle slow operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives you many of the benefits (decoupling, responsiveness, retries) without multiplying operational complexity across networked services.&lt;/p&gt;
&lt;h3 id="centralize-observability"&gt;Centralize observability&lt;/h3&gt;
&lt;p&gt;You should be able to answer, in minutes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What requests are failing?&lt;/li&gt;
&lt;li&gt;Where are the slowdowns?&lt;/li&gt;
&lt;li&gt;Which dependencies are misbehaving?&lt;/li&gt;
&lt;li&gt;What code path is responsible?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A monolith makes tracing and logging simpler because there’s one deployment boundary and fewer hop-to-hop handoffs.&lt;/p&gt;
&lt;h3 id="keep-deployment-boring-too"&gt;Keep deployment boring too&lt;/h3&gt;
&lt;p&gt;A single artifact, a single rollout process, a single rollback story. Your CI/CD pipeline becomes a force multiplier rather than a recurring coordination meeting.&lt;/p&gt;
&lt;p&gt;If you want a mental model, think of it like this: &lt;strong&gt;start with a monolith that behaves like a set of modules—and upgrade to services only when the runtime boundary is necessary.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="when-microservices-actually-make-sense-spoiler-its-usually-later"&gt;When Microservices Actually Make Sense (Spoiler: It’s Usually Later)&lt;/h2&gt;
&lt;p&gt;Microservices can be the right move when two things align:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You have enough scale that failure and latency costs matter.&lt;/li&gt;
&lt;li&gt;You have enough organizational maturity that multiple teams can operate independently.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;“Later” doesn’t mean “after you’re huge.” It means “after you’ve proven the specific pain.”&lt;/p&gt;
&lt;p&gt;Common signals that extraction is justified:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One module has become a release bottleneck (every change depends on a single shared deploy)&lt;/li&gt;
&lt;li&gt;The codebase is growing so entangled that refactoring risks breaking unrelated features&lt;/li&gt;
&lt;li&gt;Independent scaling is real (not speculative), and the cost of inefficiency is measurable&lt;/li&gt;
&lt;li&gt;You’re migrating to a data model where isolation is beneficial and stable&lt;/li&gt;
&lt;li&gt;You have a dedicated domain team that can take ownership of the operational burden&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you do extract, don’t start with “everything.” Pick a single, well-contained component with a clean contract and a clear owner. The first service should be boring to operate and easy to debug—not a sprawling “platform” that takes six months to stabilize.&lt;/p&gt;
&lt;h2 id="a-concrete-path-that-doesnt-waste-years"&gt;A Concrete Path That Doesn’t Waste Years&lt;/h2&gt;
&lt;p&gt;Here’s an approach I recommend repeatedly because it fits how startups actually work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build a modular monolith&lt;/strong&gt; with strict internal interfaces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add background jobs&lt;/strong&gt; for slow or failure-prone work (queues, workers, retries, idempotency).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instrument deeply&lt;/strong&gt; so you can identify bottlenecks and understand failure rates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract one service at a time&lt;/strong&gt; when you have proof of necessity—independent scaling, independent release cadence, or a stable data ownership boundary.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep integration contracts sharp&lt;/strong&gt;: version APIs thoughtfully, and design for graceful degradation rather than fragile synchronous calls.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach preserves speed now and keeps the door open later. It avoids the trap of “architecture as a deadline,” where the system grows complex before the product earns real traction.&lt;/p&gt;
&lt;p&gt;And importantly: it keeps your team shipping. A startup doesn’t need an enterprise platform to succeed—it needs a reliable way to deliver value and learn quickly.&lt;/p&gt;
&lt;h2 id="conclusion-complexity-is-a-tax-you-should-only-pay-when-you-can-afford-it"&gt;Conclusion: Complexity Is a Tax You Should Only Pay When You Can Afford It&lt;/h2&gt;
&lt;p&gt;Microservices are not automatically wrong. They’re just expensive—operationally, cognitively, and organizationally. For most startups, a structured monolith is the pragmatic choice: it’s easier to reason about, faster to change, and simpler to observe under pressure. Go distributed only when you have clear evidence that you need the separation—and when your team can genuinely operate it. Boring wins, because shipping wins.&lt;/p&gt;</content></item><item><title>Monorepos Are Worth the Pain (If You Use the Right Tools)</title><link>https://decastro.work/blog/monorepos-worth-pain-right-tools/</link><pubDate>Fri, 22 Jul 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/monorepos-worth-pain-right-tools/</guid><description>&lt;p&gt;Monorepos don’t become “good” because you put everything in one folder. They become good when you can &lt;em&gt;move fast without breaking things&lt;/em&gt;—with tooling that understands what changed, what depends on what, and how to reproduce builds reliably. The monorepo vs. polyrepo debate is loud because the pain is real. But the upside is also real: atomic changes across packages, fewer synchronization headaches, and a developer workflow that doesn’t collapse as your codebase grows.&lt;/p&gt;</description><content>&lt;p&gt;Monorepos don’t become “good” because you put everything in one folder. They become good when you can &lt;em&gt;move fast without breaking things&lt;/em&gt;—with tooling that understands what changed, what depends on what, and how to reproduce builds reliably. The monorepo vs. polyrepo debate is loud because the pain is real. But the upside is also real: atomic changes across packages, fewer synchronization headaches, and a developer workflow that doesn’t collapse as your codebase grows.&lt;/p&gt;
&lt;h2 id="the-real-argument-isnt-structureits-coordination"&gt;The real argument isn’t structure—it’s coordination&lt;/h2&gt;
&lt;p&gt;People fight about monorepos and polyrepos like it’s about architecture aesthetics. It isn’t. It’s about coordination cost.&lt;/p&gt;
&lt;p&gt;In a polyrepo setup, even “simple” changes can turn into choreography:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You update a shared library.&lt;/li&gt;
&lt;li&gt;You bump a version somewhere.&lt;/li&gt;
&lt;li&gt;You update a consumer repo.&lt;/li&gt;
&lt;li&gt;You coordinate PRs, merges, and release timing.&lt;/li&gt;
&lt;li&gt;You hope everyone tested the same commit state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a monorepo, you can often make one atomic change that spans packages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update the shared library.&lt;/li&gt;
&lt;li&gt;Update the consumer(s).&lt;/li&gt;
&lt;li&gt;Run the right tests.&lt;/li&gt;
&lt;li&gt;Ship together.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That atomicity is the monorepo’s most practical advantage—not “code sharing,” and certainly not the fantasy that one repo magically makes dependencies painless. If your changes usually cross package boundaries, you’re paying coordination taxes somewhere. Monorepos just give you a chance to pay them once in tooling, rather than repeatedly in process.&lt;/p&gt;
&lt;p&gt;The catch: this only works if your monorepo is managed like a system, not like a folder dump.&lt;/p&gt;
&lt;h2 id="why-monorepos-became-nightmares-and-how-modern-tools-fix-the-core-issues"&gt;Why monorepos became nightmares (and how modern tools fix the core issues)&lt;/h2&gt;
&lt;p&gt;Bad monorepos tend to fail in predictable ways. The usual suspects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No clear dependency graph&lt;/strong&gt;
If your tooling can’t tell which packages depend on which, you’ll either over-test everything or under-test and ship broken builds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Slow installs and slow builds&lt;/strong&gt;
Without workspace-aware package management and caching, a monorepo turns every developer action into a waiting room.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;“It works on my machine”&lt;/strong&gt;
If builds aren’t reproducible and caching isn’t reliable, remote teams will lose hours to mystery failures.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Testing everything, always&lt;/strong&gt;
The moment “run the tests” becomes “run all the tests,” developers will stop running tests.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Modern monorepo tooling targets these failure modes directly. The winning combination usually looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pnpm workspaces&lt;/strong&gt; for fast, deterministic dependency management across packages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turborepo&lt;/strong&gt; for caching and task orchestration, including remote caching so CI and dev machines share results.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nx&lt;/strong&gt; for “affected-only” thinking—running tests/builds based on what changed and what depends on it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’re not adopting tools for the sake of it. You’re replacing brute-force workflows with graph-aware, cache-friendly execution.&lt;/p&gt;
&lt;h2 id="pnpm-workspaces-the-foundation-most-teams-underestimate"&gt;pnpm workspaces: the foundation most teams underestimate&lt;/h2&gt;
&lt;p&gt;Before you even talk about caching and graphs, you need sane dependency management. pnpm workspaces help by treating the monorepo as a coherent package universe rather than a set of unrelated projects.&lt;/p&gt;
&lt;p&gt;In practical terms, pnpm workspaces give you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralized install behavior&lt;/strong&gt; across packages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A shared store&lt;/strong&gt; that avoids reinstalling the world.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Predictable resolution&lt;/strong&gt; so dependency drift is less likely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what this changes for a developer workflow. Suppose you’re working on &lt;code&gt;@acme/ui&lt;/code&gt; and a related app &lt;code&gt;@acme/web&lt;/code&gt;. With pnpm workspaces, switching branches or adding a dependency in &lt;code&gt;@acme/ui&lt;/code&gt; doesn’t require rethinking how the whole repo is installed. That matters because developers will only embrace monorepo workflows if everyday actions are quick.&lt;/p&gt;
&lt;p&gt;Also, workspaces make it easier to enforce consistent package standards: lint scripts, TypeScript settings, versioning conventions—whatever your team decides should be uniform.&lt;/p&gt;
&lt;p&gt;If you skip this foundation, everything above it gets harder.&lt;/p&gt;
&lt;h2 id="turborepo-make-run-the-right-tasks-feel-effortless-with-caching"&gt;Turborepo: make “run the right tasks” feel effortless with caching&lt;/h2&gt;
&lt;p&gt;Turborepo’s pitch is simple: orchestrate tasks across packages and cache results so repeated work disappears.&lt;/p&gt;
&lt;p&gt;The monorepo pain point is that “run tests for one change” is deceptively hard. Without orchestration, you end up reinventing it with brittle scripts. With Turborepo, you model tasks like build, test, and lint across your package graph—and it decides what to run and what it can reuse.&lt;/p&gt;
&lt;p&gt;The most impactful feature is &lt;strong&gt;(remote) caching&lt;/strong&gt;. Caching changes the psychology of monorepos. Instead of “CI might take forever,” it becomes “CI will often be instantaneous,” because the output for a given task and inputs already exists.&lt;/p&gt;
&lt;p&gt;Concrete example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You change a shared utility package: &lt;code&gt;@acme/utils&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You run &lt;code&gt;turbo test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turborepo figures out what depends on &lt;code&gt;@acme/utils&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It runs the tests for those dependent packages.&lt;/li&gt;
&lt;li&gt;For tasks you’ve already executed with the same effective inputs, it pulls results from cache rather than rebuilding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now imagine a team workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developer A runs tests and populates cache.&lt;/li&gt;
&lt;li&gt;Developer B pulls the changes, runs the same tasks, and gets results quickly.&lt;/li&gt;
&lt;li&gt;CI verifies the exact same task graph outcomes without burning compute.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That feedback loop is how monorepos stop feeling like punishment and start feeling like leverage.&lt;/p&gt;
&lt;h2 id="nx-trust-the-dependency-graph-then-run-only-whats-affected"&gt;Nx: trust the dependency graph, then run only what’s affected&lt;/h2&gt;
&lt;p&gt;If Turborepo focuses on task execution and caching, Nx emphasizes “affected-only” operations: run what’s needed based on the dependency graph and the changes you made.&lt;/p&gt;
&lt;p&gt;The key benefit is developer trust. When you run &lt;code&gt;affected:test&lt;/code&gt; (or the equivalent patterns Nx enables), you’re confident the system isn’t blindly running everything—or, worse, missing critical checks.&lt;/p&gt;
&lt;p&gt;A typical monorepo setup has three layers of work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lint&lt;/strong&gt; (fast, catches style and type issues early)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; (medium cost, validates behavior)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Builds&lt;/strong&gt; (expensive, ensures distributable outputs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Affected-only execution lets you scale these layers without turning “try a small change” into “wait half an hour.”&lt;/p&gt;
&lt;p&gt;What it looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You modify &lt;code&gt;@acme/auth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Nx identifies which packages depend on it (say &lt;code&gt;@acme/api&lt;/code&gt; and &lt;code&gt;@acme/web&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;You run tests for those affected packages, not the entire universe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the difference between a monorepo that stays interactive and one that quietly trains developers to stop running checks. Affected-only tooling keeps the “inner loop” tight, which is where engineering velocity is won.&lt;/p&gt;
&lt;h2 id="the-monorepo-superpower-atomic-changes-across-packages"&gt;The monorepo superpower: atomic changes across packages&lt;/h2&gt;
&lt;p&gt;Here’s the real reason monorepos are worth the pain: they make coordinated changes boring.&lt;/p&gt;
&lt;p&gt;In a polyrepo world, a breaking change in a shared package often requires coordinated PRs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PR in the library repo&lt;/li&gt;
&lt;li&gt;PR in each consumer repo&lt;/li&gt;
&lt;li&gt;Potential version bump choreography&lt;/li&gt;
&lt;li&gt;Release coordination, or at least a carefully timed merge&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In a monorepo, you can do this as a single coherent unit of work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Update &lt;code&gt;@acme/auth&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;@acme/web&lt;/code&gt; and &lt;code&gt;@acme/api&lt;/code&gt; to match.&lt;/li&gt;
&lt;li&gt;Run impacted tests.&lt;/li&gt;
&lt;li&gt;Merge once.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better, you can codify this into your workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Require that affected tests pass before merge.&lt;/li&gt;
&lt;li&gt;Use caching so PR checks don’t grind to a halt.&lt;/li&gt;
&lt;li&gt;Keep the dependency graph authoritative so “what depends on what” is never a guessing game.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is also where tooling choice matters. Without Turborepo/Nx-style execution, atomic changes turn into atomic failures—because everything is rebuilt and tested so slowly that people stop relying on automation. The tooling isn’t a luxury; it’s what turns atomicity into shipping.&lt;/p&gt;
&lt;h2 id="when-its-worth-it-and-when-its-not"&gt;When it’s worth it (and when it’s not)&lt;/h2&gt;
&lt;p&gt;Here’s my opinionated rule of thumb: if your team ships more than a few related packages, you should seriously consider a monorepo. If you have exactly one shared package and the rest are independent projects, a polyrepo might be simpler and perfectly fine.&lt;/p&gt;
&lt;p&gt;But the decision should be driven by &lt;em&gt;change patterns&lt;/em&gt;, not ideology:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do your updates frequently cross package boundaries?&lt;/li&gt;
&lt;li&gt;Do you regularly need coordinated releases?&lt;/li&gt;
&lt;li&gt;Are you already losing time to “wait for another PR” or version bump logistics?&lt;/li&gt;
&lt;li&gt;Is your shared code evolving under active development?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If yes, monorepo tooling has matured enough that the pain is mostly solvable. The hard part isn’t putting code in one repository. The hard part is building a workflow where developers can safely move fast.&lt;/p&gt;
&lt;h2 id="conclusion-manage-complexity-dont-pretend-it-wont-exist"&gt;Conclusion: manage complexity, don’t pretend it won’t exist&lt;/h2&gt;
&lt;p&gt;Monorepos aren’t inherently better than polyrepos. They’re better when you use the right tools to manage complexity: pnpm workspaces for dependency sanity, Turborepo for orchestrated tasks and remote caching, and Nx for affected-only correctness. Do that, and the monorepo becomes what it’s always promised to be—an environment where atomic changes are practical, feedback is fast, and coordination costs finally stop haunting every release.&lt;/p&gt;</content></item><item><title>Bun Is the JavaScript Runtime Nobody Asked For (That We Desperately Need)</title><link>https://decastro.work/blog/bun-javascript-runtime-nobody-asked-for/</link><pubDate>Mon, 11 Jul 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/bun-javascript-runtime-nobody-asked-for/</guid><description>&lt;p&gt;JavaScript developers have been living with a quiet assumption: “the runtime wars are basically over.” Node.js won, Deno tried to clean up the mess, and most of us just built our apps and pretended the platform never changed. Then Bun showed up—fast, opinionated, and bundled so aggressively it feels less like a runtime and more like a complete development environment that fell out of the sky. Nobody asked for another contender. That’s exactly why we need one.&lt;/p&gt;</description><content>&lt;p&gt;JavaScript developers have been living with a quiet assumption: “the runtime wars are basically over.” Node.js won, Deno tried to clean up the mess, and most of us just built our apps and pretended the platform never changed. Then Bun showed up—fast, opinionated, and bundled so aggressively it feels less like a runtime and more like a complete development environment that fell out of the sky. Nobody asked for another contender. That’s exactly why we need one.&lt;/p&gt;
&lt;h2 id="the-messy-backstory-why-fast-runtimes-keep-happening"&gt;The messy backstory: why “fast runtimes” keep happening&lt;/h2&gt;
&lt;p&gt;At some point, performance stopped being a competitive advantage and became a baseline expectation. Cold starts matter. CI minutes matter. Tooling latency matters. Developers want “save → run” to feel instant, not “save → wait” like it’s 2013.&lt;/p&gt;
&lt;p&gt;Node.js built the modern JavaScript server world, but it also set habits: the ecosystem grew around it, tooling targeted it, and even non-Node environments often emulate its behavior. Deno arrived with a different philosophy—secure by default, less configuration, and a runtime that treats the standard library like a first-class citizen. But adoption takes time, and time is expensive.&lt;/p&gt;
&lt;p&gt;Bun entering the ring isn’t really about a newcomer trying to prove it can run JavaScript. It’s about pressuring the status quo. Runtimes don’t improve in a vacuum; they improve when they have to compete with something better—or at least different enough to force everyone else to respond.&lt;/p&gt;
&lt;h2 id="what-bun-actually-is-and-why-it-feels-like-cheating"&gt;What Bun actually is (and why it feels like cheating)&lt;/h2&gt;
&lt;p&gt;Bun isn’t just “another way to run JavaScript.” It’s written in Zig, and it ships a full toolchain with it: a bundler, a transpiler, a package manager, and a test runner. That design choice is more strategic than it looks.&lt;/p&gt;
&lt;p&gt;If you’ve ever had to stitch together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a bundler (or multiple build steps),&lt;/li&gt;
&lt;li&gt;a package manager,&lt;/li&gt;
&lt;li&gt;a test runner,&lt;/li&gt;
&lt;li&gt;and a transpiler configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…you already know how quickly your “simple setup” becomes an ecosystem of scripts that all have their own version constraints and failure modes. Bun’s pitch is basically: &lt;em&gt;stop duct-taping your dev workflow; let the runtime own the experience&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Concretely, that means you can start a project, install dependencies, bundle assets, and run tests without the usual ceremony. It’s the difference between buying a car and assembling one from parts labeled “engine (probably), wheels (maybe), and instructions (good luck).”&lt;/p&gt;
&lt;p&gt;And yes—Bun’s benchmark numbers are hard to ignore. The point isn’t that benchmarks are a universal truth; it’s that they demonstrate an attention to performance that most developers now experience only indirectly through “wait, why is Node so slow in this one case?”&lt;/p&gt;
&lt;h2 id="absurd-benchmarks-and-the-more-interesting-question-whats-the-tradeoff"&gt;“Absurd benchmarks” and the more interesting question: what’s the tradeoff?&lt;/h2&gt;
&lt;p&gt;When people talk about Bun’s speed, they often jump straight to performance screenshots. That’s satisfying, but it misses the real conversation: &lt;em&gt;how does Bun achieve these gains, and what does it cost you?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here are the tradeoffs worth thinking about, especially if you’re considering Bun for real work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Workflow coupling.&lt;/strong&gt;&lt;br&gt;
Bun’s bundler/transpiler/test runner are part of the runtime story. That’s great for convenience—but if you have specialized build tooling (custom Babel plugins, complex bundler pipelines, or internal tooling), you may need to validate compatibility early.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem reality.&lt;/strong&gt;&lt;br&gt;
JavaScript libraries assume certain runtime behaviors. Most packages work everywhere, but “most” isn’t “guaranteed.” The practical approach is to pilot Bun on a non-critical service or a smaller app where you can surface incompatibilities quickly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Operational confidence.&lt;/strong&gt;&lt;br&gt;
Speed is meaningless if deployment stability becomes a guessing game. You’ll want to run performance tests and failure-mode tests in your environment, not just in the blog post that made Bun look unbeatable.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;My opinionated takeaway: don’t pick Bun because it’s faster in a benchmark. Pick it because it’s faster &lt;em&gt;and&lt;/em&gt; it gives you an integrated workflow. Then confirm—measurably—that it behaves well with your dependencies and deployment model.&lt;/p&gt;
&lt;h2 id="the-ecosystem-risk-nobody-wants-to-say-out-loud"&gt;The ecosystem risk nobody wants to say out loud&lt;/h2&gt;
&lt;p&gt;Your biggest fear with three runtimes (Node, Deno, Bun) isn’t performance. It’s fragmentation. Code reuse is the real engine of developer productivity, and fragmentation is what happens when compatibility breaks in subtle ways.&lt;/p&gt;
&lt;p&gt;The “implosion” scenario looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frameworks add runtime-specific branches.&lt;/li&gt;
&lt;li&gt;Tooling authors stop testing across all three.&lt;/li&gt;
&lt;li&gt;Tutorials start to assume one runtime.&lt;/li&gt;
&lt;li&gt;Bugs become hard to reproduce because they’re specific to one environment.&lt;/li&gt;
&lt;li&gt;Teams end up with “works on my machine” that means “works on my runtime.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But here’s the twist: fragmentation is also pressure. If Bun, Deno, and Node all have to satisfy serious users, that pressure can force ecosystems to become more consistent instead of less.&lt;/p&gt;
&lt;p&gt;The key is how maintainers respond. If the community treats runtime differences like annoying edge cases—and builds libraries and tooling that are runtime-agnostic by default—fragmentation becomes manageable. If the community embraces per-runtime divergence as a feature, you’ll feel the pain.&lt;/p&gt;
&lt;h2 id="my-bet-buns-real-contribution-is-forcing-everyone-to-speed-up"&gt;My bet: Bun’s real contribution is forcing everyone to speed up&lt;/h2&gt;
&lt;p&gt;Bun’s success isn’t just measured in star counts or headlines. It’s measured in how quickly other parts of the JavaScript platform react.&lt;/p&gt;
&lt;p&gt;Here’s what I think Bun is doing that matters most: it makes the “Node is fast enough” story harder to tell. Node is still the center of gravity, but now there’s a visible alternative that developers can try without asking for permission from their entire stack.&lt;/p&gt;
&lt;p&gt;That creates incentives across the ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node maintainers accelerate optimization work&lt;/strong&gt; because “fast enough” no longer holds up in public comparisons.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tooling becomes more runtime-aware&lt;/strong&gt;—which sounds scary, but often results in fewer assumptions and better portability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Library authors tighten standards&lt;/strong&gt; because every runtime that claims compliance becomes a test harness for sloppy behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deno gets dragged into this too, whether it wants to or not. Even if Bun wins users, the existence of multiple runtimes pushes everyone toward better conformance and clearer boundaries.&lt;/p&gt;
&lt;p&gt;And the best part? When runtimes compete, end users win with faster installs, faster tests, quicker feedback loops, and fewer build-time mysteries.&lt;/p&gt;
&lt;h2 id="practical-advice-how-to-evaluate-bun-without-gambling-your-project"&gt;Practical advice: how to evaluate Bun without gambling your project&lt;/h2&gt;
&lt;p&gt;If you want Bun’s benefits without taking reckless risks, do it like a grown-up: test it where it matters, before you bet the farm.&lt;/p&gt;
&lt;h3 id="1-start-with-a-small-but-representative-service"&gt;1) Start with a small but representative service&lt;/h3&gt;
&lt;p&gt;Pick something with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;real dependencies (including at least a couple from the ecosystem),&lt;/li&gt;
&lt;li&gt;your typical build/test flow,&lt;/li&gt;
&lt;li&gt;and a deployment path that resembles production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your app is tiny and dependency-free, you won’t learn anything meaningful.&lt;/p&gt;
&lt;h3 id="2-validate-the-edge-behaviors-you-actually-care-about"&gt;2) Validate the “edge” behaviors you actually care about&lt;/h3&gt;
&lt;p&gt;Don’t stop at “it runs.” Check:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;file system access patterns,&lt;/li&gt;
&lt;li&gt;environment variables handling,&lt;/li&gt;
&lt;li&gt;network calls,&lt;/li&gt;
&lt;li&gt;worker/thread behavior (if you use it),&lt;/li&gt;
&lt;li&gt;and your bundling strategy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your goal is confidence that the runtime works like your application expects—not just that the syntax parses.&lt;/p&gt;
&lt;h3 id="3-run-performance-tests-against-your-bottlenecks"&gt;3) Run performance tests against your bottlenecks&lt;/h3&gt;
&lt;p&gt;Benchmarks are abstractions. Your bottleneck is specific: maybe JSON serialization, maybe startup time, maybe test execution time, maybe bundling. Measure &lt;em&gt;your&lt;/em&gt; pipeline with Bun and with your current runtime.&lt;/p&gt;
&lt;p&gt;If Bun doesn’t improve the part you feel day-to-day, it may not be worth switching.&lt;/p&gt;
&lt;h3 id="4-watch-ecosystem-friction-signals-early"&gt;4) Watch ecosystem friction signals early&lt;/h3&gt;
&lt;p&gt;During evaluation, pay attention to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency issues that appear only on Bun,&lt;/li&gt;
&lt;li&gt;unclear errors that waste time,&lt;/li&gt;
&lt;li&gt;and tooling gaps that force you into workarounds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you hit those, you can still use Bun—but you should decide whether you’re willing to carry the compatibility tax.&lt;/p&gt;
&lt;h2 id="conclusion-bun-isnt-a-revolutionits-a-correction"&gt;Conclusion: Bun isn’t a revolution—it’s a correction&lt;/h2&gt;
&lt;p&gt;Bun is the JavaScript runtime nobody asked for, and that’s precisely why it might be the one we desperately need. It’s not just another engine; it’s a push toward integrated developer workflows, sharper performance focus, and better cross-runtime discipline.&lt;/p&gt;
&lt;p&gt;Will the JavaScript ecosystem fragment? It could. But competition also forces the opposite: shared standards, tighter tooling, and faster iteration everywhere. If you evaluate Bun with discipline—on representative workloads, with real dependencies—you can take the upside without courting chaos.&lt;/p&gt;
&lt;p&gt;In other words: don’t worship the benchmarks. Use Bun as leverage. And watch the platform get better because of it.&lt;/p&gt;</content></item><item><title>The Developer Experience Gap Is the New Performance Gap</title><link>https://decastro.work/blog/developer-experience-gap-new-performance-gap/</link><pubDate>Tue, 05 Jul 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/developer-experience-gap-new-performance-gap/</guid><description>&lt;p&gt;Performance used to be the deciding factor. You could feel it: faster servers, lower latency, more throughput. But the world didn’t stop there—most teams now build on stacks where raw execution speed is “good enough.” What’s left as a differentiator isn’t the runtime. It’s the moment between your intention and your code doing something useful.&lt;/p&gt;
&lt;p&gt;That gap has a name: developer experience (DX). And it’s no longer a nice-to-have. It’s the new performance gap—because DX governs how quickly you can think, test, fix, and ship. Teams that optimize for DX don’t just move faster; they produce fewer bugs, retain better engineers, and quietly build more reliable systems over time.&lt;/p&gt;</description><content>&lt;p&gt;Performance used to be the deciding factor. You could feel it: faster servers, lower latency, more throughput. But the world didn’t stop there—most teams now build on stacks where raw execution speed is “good enough.” What’s left as a differentiator isn’t the runtime. It’s the moment between your intention and your code doing something useful.&lt;/p&gt;
&lt;p&gt;That gap has a name: developer experience (DX). And it’s no longer a nice-to-have. It’s the new performance gap—because DX governs how quickly you can think, test, fix, and ship. Teams that optimize for DX don’t just move faster; they produce fewer bugs, retain better engineers, and quietly build more reliable systems over time.&lt;/p&gt;
&lt;h2 id="why-performance-moved-down-the-stack"&gt;Why “Performance” Moved Down the Stack&lt;/h2&gt;
&lt;p&gt;A decade ago, swapping tech stacks could materially change user-facing performance. Today, for most applications, the core bottleneck is rarely “the language is slow.” It’s the human pipeline: build times, deploy friction, debugging loops, and how hard it is to reproduce issues.&lt;/p&gt;
&lt;p&gt;Think about what actually slows teams down day-to-day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Waiting for builds to finish just to see a tiny UI change&lt;/li&gt;
&lt;li&gt;Spinning up local environments that don’t match production&lt;/li&gt;
&lt;li&gt;Wrestling with database setup every time a new feature branch needs a test dataset&lt;/li&gt;
&lt;li&gt;Duplicating configuration between machines, services, and environments&lt;/li&gt;
&lt;li&gt;Losing time to “what changed?” because logs, tracing, and rollback workflows are too manual&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At a certain point, performance becomes table stakes. DX is what determines whether engineering feels like momentum or like wrestling.&lt;/p&gt;
&lt;h2 id="the-dx-compounding-effect-speed-multiplies-everything"&gt;The DX Compounding Effect: Speed Multiplies Everything&lt;/h2&gt;
&lt;p&gt;Here’s the key idea: DX isn’t one metric—it’s a multiplier on every other metric you care about.&lt;/p&gt;
&lt;p&gt;When your loop is tight, you iterate more. More iteration means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More opportunities to catch bugs while they’re cheap to fix&lt;/li&gt;
&lt;li&gt;Smaller pull requests (because you can test them sooner)&lt;/li&gt;
&lt;li&gt;Better code review outcomes (because changes are easier to understand)&lt;/li&gt;
&lt;li&gt;Faster onboarding (because tools behave predictably)&lt;/li&gt;
&lt;li&gt;Higher morale (because “waiting” is a form of attrition)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good DX improvement has the same shape as performance improvements: it compounds. If it takes you 15 seconds less to get from edit to feedback, and you run that loop dozens of times every day, you’re buying back real engineering time. Multiply it across a year and the “minor” seconds become dozens of working days.&lt;/p&gt;
&lt;p&gt;And that’s before you count the second-order effects: fewer long-lived bugs, fewer broken merges, and fewer production incidents caused by rushed releases.&lt;/p&gt;
&lt;h2 id="the-tooling-pattern-instant-feedback-one-command-setup-push-to-deploy"&gt;The Tooling Pattern: Instant Feedback, One-Command Setup, Push-to-Deploy&lt;/h2&gt;
&lt;p&gt;You don’t have to take DX on faith. Look at the tools that people adopt and stick with. They share patterns: faster feedback, less setup, and fewer manual steps.&lt;/p&gt;
&lt;h3 id="instant-hmr-vite-and-the-art-of-staying-in-flow"&gt;Instant HMR: Vite and the Art of Staying in Flow&lt;/h3&gt;
&lt;p&gt;Hot Module Replacement (HMR) is deceptively powerful. It doesn’t just reduce build times—it reduces context switching.&lt;/p&gt;
&lt;p&gt;With tools like Vite, your browser updates almost immediately as you change code. That changes the psychology of development:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You experiment more boldly&lt;/li&gt;
&lt;li&gt;You refactor sooner instead of “later”&lt;/li&gt;
&lt;li&gt;You trust the feedback loop enough to keep going&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your developers routinely say, “Let’s wait for the build,” you’ve already lost a measurable amount of throughput and quality.&lt;/p&gt;
&lt;p&gt;Practical move: audit your feedback loop. Where does time go—type-checking, bundling, tests, linting, or environment startup? Optimize the loop that developers actually feel, not the one you can brag about in a benchmark.&lt;/p&gt;
&lt;h3 id="one-command-database-turso-and-environment-parity"&gt;One-Command Database: Turso and Environment Parity&lt;/h3&gt;
&lt;p&gt;Databases are where DX often goes to die. Every team has stories about “works on my machine,” migrations that don’t match, seed scripts that drift, and local schemas that are subtly broken.&lt;/p&gt;
&lt;p&gt;A tool like Turso’s “one-command” setup pattern encourages parity: developers can spin up a usable database quickly and reliably. That means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feature branches can test against real-ish data&lt;/li&gt;
&lt;li&gt;Integration testing becomes normal instead of exceptional&lt;/li&gt;
&lt;li&gt;Bugs related to schema or data shape show up earlier&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical move: treat local environment setup as a product. Make it reproducible, fast, and documented in the same place as your code. If new contributors can’t get to a working state in minutes, the system is pushing them toward errors.&lt;/p&gt;
&lt;h3 id="push-to-deploy-railway-and-the-removal-of-release-drag"&gt;Push-to-Deploy: Railway and the Removal of Release Drag&lt;/h3&gt;
&lt;p&gt;The fastest teams ship not because they have heroic release managers, but because releases aren’t painful.&lt;/p&gt;
&lt;p&gt;Push-to-deploy platforms like Railway reduce the “release tax”—the time and attention cost required to deploy, configure, and verify changes. When deployment is easy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Teams ship smaller changes more frequently&lt;/li&gt;
&lt;li&gt;Rollbacks are less scary&lt;/li&gt;
&lt;li&gt;Production feedback becomes part of the standard workflow, not a once-a-week scramble&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical move: design your release workflow so the default path is safe. Automate environment configuration, make staging mirror production, and ensure every deploy has an observable outcome (logs, metrics, and a rollback plan).&lt;/p&gt;
&lt;h2 id="dx-as-a-quality-system-not-a-productivity-hack"&gt;DX as a Quality System, Not a Productivity Hack&lt;/h2&gt;
&lt;p&gt;The common mistake is thinking DX is about developer happiness alone. It’s not. DX is a quality system because it affects the kinds of decisions developers make under pressure.&lt;/p&gt;
&lt;p&gt;When the loop is slow or unreliable, engineers compensate with shortcuts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They skip tests or run only a subset&lt;/li&gt;
&lt;li&gt;They batch changes into giant PRs&lt;/li&gt;
&lt;li&gt;They avoid refactors because the risk feels too high&lt;/li&gt;
&lt;li&gt;They rely on tribal knowledge rather than tooling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Conversely, when DX is strong, engineers do the “correct” things more often because the system makes them cheap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run tests frequently because they finish quickly&lt;/li&gt;
&lt;li&gt;Create smaller PRs because feedback arrives sooner&lt;/li&gt;
&lt;li&gt;Add observability because it’s easy to wire in&lt;/li&gt;
&lt;li&gt;Debug locally because the environment is trustworthy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A striking sign of DX maturity is that your bug reports start to look different. Instead of vague issues like “it sometimes fails,” you get logs, reproduction steps, failing tests, and clear diffs. That’s not just engineering discipline—it’s the tooling enabling it.&lt;/p&gt;
&lt;h2 id="how-to-measure-dx-without-reducing-it-to-hype"&gt;How to Measure DX Without Reducing It to Hype&lt;/h2&gt;
&lt;p&gt;DX is often discussed vaguely—“it feels faster”—which is why it gets deprioritized. You need instrumentation.&lt;/p&gt;
&lt;p&gt;Start with measurements that map directly to developer loops:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time to first local run&lt;/strong&gt;: from &lt;code&gt;clone&lt;/code&gt; to working app&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time to feedback&lt;/strong&gt;: edit → browser update, or edit → unit test results&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time to diagnose&lt;/strong&gt;: how long it takes to identify the cause of a regression&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release cycle time&lt;/strong&gt;: from merge to deployed and verified&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure rates&lt;/strong&gt;: flaky tests, broken builds, dependency drift, environment setup errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then connect those to outcomes you already care about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bug escape rate (production issues per release)&lt;/li&gt;
&lt;li&gt;Mean time to resolution (MTTR)&lt;/li&gt;
&lt;li&gt;Engineering throughput (cycle time, PR size, deployment frequency)&lt;/li&gt;
&lt;li&gt;Retention signals (self-reported friction, churn reasons, onboarding satisfaction)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical approach: run a short “DX retro” with developers. Ask for three moments where they lose the most time, then quantify them. You’ll usually find the biggest wins are not flashy—they’re boring fixes: caching, better defaults, simpler local setup, faster test selection, and fewer manual steps.&lt;/p&gt;
&lt;h2 id="building-your-dx-roadmap-focus-on-the-bottlenecks-that-hurt"&gt;Building Your DX Roadmap: Focus on the Bottlenecks That Hurt&lt;/h2&gt;
&lt;p&gt;If DX is the new performance gap, you should manage it like performance: find the bottleneck and remove it.&lt;/p&gt;
&lt;p&gt;Here’s a roadmap that works across teams:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Measure the loop&lt;/strong&gt;&lt;br&gt;
Track time to feedback and time to diagnose. Baseline it before changing anything.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Optimize the “inner loop” first&lt;/strong&gt;&lt;br&gt;
Start with build/HMR/test speed. If developers wait for feedback, nothing else matters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make environments reproducible&lt;/strong&gt;&lt;br&gt;
One-command setup, consistent configuration, and predictable migrations beat complicated documentation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automate releases and verification&lt;/strong&gt;&lt;br&gt;
Push-to-deploy isn’t just convenience—it’s risk reduction through standardization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Invest in observability for developers&lt;/strong&gt;&lt;br&gt;
When debugging is easy, fewer bugs slip through. Logs and traces that are actually usable are DX.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Close the loop with developer feedback&lt;/strong&gt;&lt;br&gt;
DX isn’t static. Treat it as a living system with regular improvements.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And be selective: a DX overhaul that chases every tool at once will turn into churn. The best DX work is incremental and targeted—each change should remove an obstacle developers hit every day.&lt;/p&gt;
&lt;h2 id="conclusion-dx-is-the-real-competitive-advantage"&gt;Conclusion: DX Is the Real Competitive Advantage&lt;/h2&gt;
&lt;p&gt;Performance still matters, but it’s no longer the differentiator it once was. Most teams can run on “fast enough” infrastructure. The real edge is how quickly developers can go from idea to validated change—and how reliably the workflow supports quality.&lt;/p&gt;
&lt;p&gt;Vite’s instant feedback, Turso’s streamlined local database setup, Railway’s push-to-deploy flow: these patterns win adoption because they reduce friction, not because they chase benchmarks. And the compounding effect is real. Better DX accelerates shipping, improves retention, and reduces the number of bugs that make it past your team’s gates.&lt;/p&gt;
&lt;p&gt;If you want a competitive advantage, stop measuring only runtime. Measure the distance between a developer’s intent and a verified result. That gap is where the new performance lives.&lt;/p&gt;</content></item><item><title>GitHub Copilot Just Shipped, and Everything Is Different Now</title><link>https://decastro.work/blog/github-copilot-shipped-everything-different/</link><pubDate>Wed, 29 Jun 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/github-copilot-shipped-everything-different/</guid><description>&lt;p&gt;GitHub Copilot is finally out of the preview era, and if you’ve been watching from the sidelines, this is your moment: the “AI autocomplete” story was always incomplete. What just shipped is the first widely available, real pair programmer experience—and it’s both more useful and more dangerous than most teams are prepared for.&lt;/p&gt;
&lt;p&gt;After months of using Copilot in preview mode, I’m convinced this is a genuine paradigm shift. It’s disturbingly good at boilerplate, surprisingly decent at algorithmic code, and absolutely unreliable when domain knowledge matters. The productivity gains are real, but uneven—and the downside lands hardest on the people least able to detect subtle wrongness.&lt;/p&gt;</description><content>&lt;p&gt;GitHub Copilot is finally out of the preview era, and if you’ve been watching from the sidelines, this is your moment: the “AI autocomplete” story was always incomplete. What just shipped is the first widely available, real pair programmer experience—and it’s both more useful and more dangerous than most teams are prepared for.&lt;/p&gt;
&lt;p&gt;After months of using Copilot in preview mode, I’m convinced this is a genuine paradigm shift. It’s disturbingly good at boilerplate, surprisingly decent at algorithmic code, and absolutely unreliable when domain knowledge matters. The productivity gains are real, but uneven—and the downside lands hardest on the people least able to detect subtle wrongness.&lt;/p&gt;
&lt;h2 id="what-copilot-ga-really-changes"&gt;What “Copilot GA” Really Changes&lt;/h2&gt;
&lt;p&gt;Copilot GA isn’t just a pricing or branding milestone. It changes how software gets drafted.&lt;/p&gt;
&lt;p&gt;In the early days of developer tooling, automation usually targeted something mechanical: formatting, linting, scaffolding, code generation from schemas. Copilot targets something more conversational: it can propose an implementation based on the code around it and your intent (as text). That means the feedback loop tightens. You go from “write it yourself, then run tools” to “try, iterate, and correct in real time.”&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You start with a function signature and a comment like “// Parse user input into a validated command.”&lt;/li&gt;
&lt;li&gt;Copilot suggests a complete function with error handling and edge-case branches.&lt;/li&gt;
&lt;li&gt;You accept most of it, tweak what’s obviously wrong, and move on.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For experienced developers, this feels like using a strong junior who’s fast and enthusiastic—except the junior has an uncanny talent for generating code that compiles, until the moment you discover it shouldn’t.&lt;/p&gt;
&lt;h2 id="the-parts-it-nails-boilerplate-and-mechanical-code"&gt;The Parts It Nails: Boilerplate and Mechanical Code&lt;/h2&gt;
&lt;p&gt;Let’s talk about where Copilot shines, because that’s where the ROI shows up immediately.&lt;/p&gt;
&lt;p&gt;Copilot is excellent at repetitive patterns and conventional structure: CRUD handlers, DTOs, mapping layers, view models, migrations, pagination logic, and the glue code that turns one library into another. It’s also very good at “fill in the blanks” tasks where your project already provides a clear shape.&lt;/p&gt;
&lt;p&gt;A concrete example: adding a new API endpoint.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You write the route and the handler stub.&lt;/li&gt;
&lt;li&gt;Copilot generates request parsing, validation scaffolding, and the typical response wrapping.&lt;/li&gt;
&lt;li&gt;You wire it to existing services and data access.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The value isn’t that Copilot “understands your product.” It’s that your product’s architecture is already in the codebase. Copilot can reflect that architecture back at you quickly—especially when the patterns are consistent.&lt;/p&gt;
&lt;p&gt;This is why senior engineers benefit most. A senior can accept a large chunk of generated code while quickly spotting whether it matches the intended behavior. The tool reduces time spent on routine work; it doesn’t eliminate judgment.&lt;/p&gt;
&lt;h2 id="the-parts-its-good-at-algorithmic-code-sometimes"&gt;The Parts It’s Good At: Algorithmic Code (Sometimes)&lt;/h2&gt;
&lt;p&gt;Copilot is also surprisingly capable when the task resembles a well-trodden problem: parsing, sorting, basic graph traversal, string processing, and standard data transformations.&lt;/p&gt;
&lt;p&gt;For instance, if you prompt for a function like “implement a stable sort for a list of objects by &lt;code&gt;priority&lt;/code&gt;,” it often produces a plausible implementation with the right edge handling. In many cases, it even uses appropriate data structures for the job.&lt;/p&gt;
&lt;p&gt;But “plausible” is the operative word. Algorithmic code still has failure modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Off-by-one errors in indexes or loop bounds&lt;/li&gt;
&lt;li&gt;Incorrect assumptions about input normalization&lt;/li&gt;
&lt;li&gt;Silent behavior changes (e.g., treating nulls one way instead of another)&lt;/li&gt;
&lt;li&gt;Performance surprises when you scale beyond the test fixture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the practical rule I’ve adopted: treat Copilot-generated algorithmic code like a draft written by someone who can follow instructions well, but can’t reliably infer the constraints you care about. If your system has invariants—like “inputs may contain Unicode combining characters” or “we must not allocate per request”—Copilot may miss them unless you spell them out.&lt;/p&gt;
&lt;h2 id="the-parts-it-fails-domain-knowledge-and-hidden-requirements"&gt;The Parts It Fails: Domain Knowledge and Hidden Requirements&lt;/h2&gt;
&lt;p&gt;This is the part that matters most for safety: Copilot can be confidently wrong when domain knowledge is involved.&lt;/p&gt;
&lt;p&gt;“Domain knowledge” doesn’t just mean business rules. It means everything you &lt;em&gt;know&lt;/em&gt; about the system but that isn’t explicitly written as code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Legal or compliance requirements&lt;/li&gt;
&lt;li&gt;Security constraints (“never log secrets,” “authorization must be enforced before lookup”)&lt;/li&gt;
&lt;li&gt;Data correctness rules that are only documented in tribal knowledge&lt;/li&gt;
&lt;li&gt;Performance constraints tied to production realities&lt;/li&gt;
&lt;li&gt;Compatibility expectations between services and data contracts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Copilot often struggles in those zones because the model doesn’t inherently know what’s sacred in your context. It fills in what looks right based on the surrounding patterns.&lt;/p&gt;
&lt;p&gt;A classic example: authentication and authorization glue.&lt;/p&gt;
&lt;p&gt;A tool may generate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a decorator that checks a role,&lt;/li&gt;
&lt;li&gt;a method that fetches a user record,&lt;/li&gt;
&lt;li&gt;and a response wrapper,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;but it might check the wrong field, skip an authorization step in a branch, or mis-handle “resource ownership” semantics. The result compiles and even passes superficial tests—until it doesn’t.&lt;/p&gt;
&lt;p&gt;The real risk isn’t that Copilot “will replace developers.” It’s that developers—especially junior developers—may stop asking the questions that keep systems correct. When the code arrives already assembled, it’s easy to skip the interrogation.&lt;/p&gt;
&lt;h2 id="how-seniors-should-use-copilot-and-why-jrs-need-guardrails"&gt;How Seniors Should Use Copilot (and Why Jrs Need Guardrails)&lt;/h2&gt;
&lt;p&gt;If you’re a senior developer, you should treat Copilot like a co-pilot, not an autopilot.&lt;/p&gt;
&lt;p&gt;My recommended workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use it for scaffolding and translation, not authority.&lt;/strong&gt;&lt;br&gt;
Generate boilerplate, adapters, and structure. Then own the behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demand explicit invariants.&lt;/strong&gt;&lt;br&gt;
If correctness depends on constraints, encode them in tests or in comments that Copilot can see and that reviewers can verify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refuse “accept-all” habits.&lt;/strong&gt;&lt;br&gt;
Don’t rubber-stamp the entire suggestion. Read it like you wrote it under time pressure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use diff-driven review.&lt;/strong&gt;&lt;br&gt;
When Copilot proposes a change, the only acceptable review is one that scrutinizes control flow, error handling, and side effects.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For junior developers, the danger is more subtle: Copilot can become a shortcut around fundamentals. When you’re learning, you need to build working mental models—of data flow, invariants, security boundaries, and failure handling. If the tool writes the code before you understand it, your learning becomes accidental.&lt;/p&gt;
&lt;p&gt;So teams should implement guardrails that keep juniors productive without letting them bypass understanding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Require &lt;strong&gt;unit tests&lt;/strong&gt; for any Copilot-authored logic beyond trivial boilerplate.&lt;/li&gt;
&lt;li&gt;Mandate &lt;strong&gt;review explanations&lt;/strong&gt;: “What does this code assume? What happens on invalid inputs?”&lt;/li&gt;
&lt;li&gt;Provide a &lt;strong&gt;style guide plus behavioral checklists&lt;/strong&gt; (e.g., how to handle errors, how to validate input, what not to log).&lt;/li&gt;
&lt;li&gt;Limit early access to Copilot for high-risk areas like authentication, billing, and data migrations—at least until competency is proven.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t about restricting creativity. It’s about making sure the shortcut doesn’t become a blindfold.&lt;/p&gt;
&lt;h2 id="practical-advice-for-teams-shipping-faster-without-shipping-flawed-code"&gt;Practical Advice for Teams Shipping Faster Without Shipping Flawed Code&lt;/h2&gt;
&lt;p&gt;Copilot’s biggest advantage is speed—but speed without discipline is just faster failure. If you want to capture the productivity benefits without the downside, you need process changes that are boring in the best way.&lt;/p&gt;
&lt;p&gt;Here’s a set of pragmatic moves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Create “safe lanes” in your codebase.&lt;/strong&gt;&lt;br&gt;
For example: allow Copilot-generated changes in UI components, view models, and serialization layers with automated tests. Force manual scrutiny in security-critical modules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add targeted tests for the tricky parts.&lt;/strong&gt;&lt;br&gt;
Property-based tests can be especially helpful when Copilot might mishandle edge cases. Even a small suite that asserts invariants catches a lot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use static analysis as a backstop, not a substitute.&lt;/strong&gt;&lt;br&gt;
Linters and type checks help, but they won’t detect semantic flaws like “authorization performed too late.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adopt a review rubric that explicitly covers risk.&lt;/strong&gt;&lt;br&gt;
Your reviewer should check: input validation, error handling, security boundaries, performance assumptions, and logging hygiene.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log what Copilot produces—then learn from it.&lt;/strong&gt;&lt;br&gt;
Keep an internal record of which Copilot suggestions were accepted, corrected, or rejected. Over time, you’ll learn where the model is reliably helpful in your stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point isn’t to pretend Copilot is “bad.” It’s to stop treating it like an all-knowing collaborator. It’s a code generator with a remarkable ability to match the shape of your project. That can accelerate delivery—or accelerate mistakes—depending on how you steer it.&lt;/p&gt;
&lt;h2 id="conclusion-the-tool-isnt-the-threatthe-habits-are"&gt;Conclusion: The Tool Isn’t the Threat—The Habits Are&lt;/h2&gt;
&lt;p&gt;GitHub Copilot GA is a real shift in how code gets written. It’s excellent for boilerplate, often competent for generic algorithmic tasks, and unreliable where domain knowledge and subtle invariants matter. The productivity gains are real, but uneven—and the biggest danger isn’t that it replaces developers.&lt;/p&gt;
&lt;p&gt;It’s that teams accidentally replace learning with output. If you treat Copilot as a generator you must verify—and you build guardrails that keep junior engineers grounded in fundamentals—you’ll get faster without getting sloppy. Ignore that, and you’ll ship software that feels smooth right up until it breaks in ways nobody can explain.&lt;/p&gt;</content></item><item><title>Tailwind CSS: The Ugly Truth About Why It Works</title><link>https://decastro.work/blog/tailwind-css-ugly-truth-why-it-works/</link><pubDate>Sun, 19 Jun 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/tailwind-css-ugly-truth-why-it-works/</guid><description>&lt;p&gt;There’s a moment every front-end developer recognizes: you open a component, see a wall of class names, and feel your soul leave your body. Then—almost immediately—you start shipping faster. That’s the paradox of Tailwind CSS: aesthetically offensive, pragmatically brilliant, and weirdly aligned with how real teams actually build software.&lt;/p&gt;
&lt;h2 id="why-tailwind-looks-like-it-shouldnt-work"&gt;Why Tailwind Looks Like It “Shouldn’t” Work&lt;/h2&gt;
&lt;p&gt;Let’s be honest: the default Tailwind experience is visually loud. The class attribute becomes a mini-program you’re forced to parse: &lt;code&gt;flex justify-center items-center p-4 bg-blue-500&lt;/code&gt;. It’s not subtle, and it doesn’t pretend to be.&lt;/p&gt;</description><content>&lt;p&gt;There’s a moment every front-end developer recognizes: you open a component, see a wall of class names, and feel your soul leave your body. Then—almost immediately—you start shipping faster. That’s the paradox of Tailwind CSS: aesthetically offensive, pragmatically brilliant, and weirdly aligned with how real teams actually build software.&lt;/p&gt;
&lt;h2 id="why-tailwind-looks-like-it-shouldnt-work"&gt;Why Tailwind Looks Like It “Shouldn’t” Work&lt;/h2&gt;
&lt;p&gt;Let’s be honest: the default Tailwind experience is visually loud. The class attribute becomes a mini-program you’re forced to parse: &lt;code&gt;flex justify-center items-center p-4 bg-blue-500&lt;/code&gt;. It’s not subtle, and it doesn’t pretend to be.&lt;/p&gt;
&lt;p&gt;If you’re a CSS purist—someone who believes in clean separation, semantic naming, and “styles should live in CSS, not in markup”—Tailwind feels like a violation. The HTML stops being HTML and starts acting like a template for CSS rules. It’s aesthetically offensive, and not in an ironic, design-nerd way. More like: “Did we break the boundary between content and presentation?”&lt;/p&gt;
&lt;p&gt;And yet, the ugliness is part of the point. Tailwind refuses to hide decisions behind names that mean nothing. It spells them out in the open, in plain view, right where the UI is defined.&lt;/p&gt;
&lt;h2 id="the-real-reason-tailwind-works-it-makes-choices-cheap"&gt;The Real Reason Tailwind Works: It Makes Choices Cheap&lt;/h2&gt;
&lt;p&gt;The mainstream pitch for Tailwind is productivity: fewer context switches, less hand-written CSS, faster iteration. True, but incomplete. The deeper reason Tailwind works is structural: it makes design decisions cheap and reversible.&lt;/p&gt;
&lt;p&gt;In a traditional workflow, you often “name the thing” before you style it. You create &lt;code&gt;ButtonPrimary&lt;/code&gt;, then later &lt;code&gt;ButtonPrimary:hover&lt;/code&gt;, then you discover you need a slightly different padding for a specific layout, so you either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add another variant,&lt;/li&gt;
&lt;li&gt;introduce a new component,&lt;/li&gt;
&lt;li&gt;or tweak the existing one and hope you didn’t break something elsewhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s not just bureaucracy—it’s friction baked into the system. Tailwind flips the cost model. You can change spacing, alignment, or color in seconds without inventing a new semantic artifact for every tiny variation.&lt;/p&gt;
&lt;p&gt;Concrete example: imagine a card layout.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Traditional approach: you define &lt;code&gt;.card&lt;/code&gt;, &lt;code&gt;.card-title&lt;/code&gt;, &lt;code&gt;.card-body&lt;/code&gt;, &lt;code&gt;.card--highlighted&lt;/code&gt;, and eventually you add more selectors when reality differs from the initial plan.&lt;/li&gt;
&lt;li&gt;Tailwind approach: you describe the card where it lives: &lt;code&gt;p-6 rounded-xl border bg-white shadow-sm&lt;/code&gt;, then optionally &lt;code&gt;bg-blue-50 border-blue-200&lt;/code&gt; for the highlighted state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yes, the class list grows. But the feedback loop gets dramatically tighter. You iterate against the actual UI, not against an abstract CSS taxonomy.&lt;/p&gt;
&lt;h2 id="its-just-classes-isnt-the-pointdesign-systems-are"&gt;“It’s Just Classes” Isn’t the Point—Design Systems Are&lt;/h2&gt;
&lt;p&gt;Here’s the ugly truth behind Tailwind that most debates miss: Tailwind utilities aren’t the end state. They’re the raw material.&lt;/p&gt;
&lt;p&gt;A real design system doesn’t aim to give developers infinite freedom. It constrains choices. Tailwind is most powerful when you configure it to encode your decisions—colors, spacing scale, typography, radii, shadows—so your “ugly” class strings become consistent with your brand.&lt;/p&gt;
&lt;p&gt;For example, instead of relying on arbitrary values like &lt;code&gt;bg-blue-500&lt;/code&gt;, you can set up semantic tokens in your Tailwind config (and then use them via the theme). You can enforce that buttons use a controlled palette, that spacing follows a scale your design team agreed on, and that components stay coherent.&lt;/p&gt;
&lt;p&gt;And importantly: you don’t have to accept the full chaos of utility-first CSS. You can still create components when it’s beneficial. Tailwind encourages composition: use utilities for layout and specifics, and wrap repeated patterns into a component only when the pattern stabilizes.&lt;/p&gt;
&lt;p&gt;Pragmatic rule of thumb: let utilities power the exploration; graduate to components when you’ve proven the shape.&lt;/p&gt;
&lt;h2 id="specificity-wars-tailwinds-quiet-superpower"&gt;Specificity Wars: Tailwind’s Quiet Superpower&lt;/h2&gt;
&lt;p&gt;One of the most underrated benefits of Tailwind is that it sidesteps the whole specificity drama. When styles live in dedicated CSS files and components stack on top of each other, you eventually hit the same familiar cliff:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;some rule “should” apply but doesn’t,&lt;/li&gt;
&lt;li&gt;another rule overrides it unexpectedly,&lt;/li&gt;
&lt;li&gt;and now you’re reaching for &lt;code&gt;!important&lt;/code&gt; or reorganizing selectors like it’s a hostage negotiation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tailwind’s utilities typically apply through a predictable class-based mechanism. You still have overrides, but you’re not constantly fighting your own stylesheet architecture.&lt;/p&gt;
&lt;p&gt;This is especially valuable in real projects where multiple contributors touch the same components. Without a disciplined CSS strategy, “quick fixes” accumulate. Tailwind doesn’t magically remove design flaws, but it prevents the most common failure mode: dead CSS growing silently while layout breaks loudly.&lt;/p&gt;
&lt;p&gt;A practical example: conditional styling for error states. With Tailwind you can keep the logic near the markup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normal: &lt;code&gt;border-gray-300 text-gray-900&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Error: &lt;code&gt;border-red-500 text-red-700&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of adding selectors like &lt;code&gt;.field.error .label&lt;/code&gt; and hoping nothing else overrides them, you switch classes based on state. The specificity problem simply doesn’t get a seat at the table.&lt;/p&gt;
&lt;h2 id="team-adoption-how-to-keep-tailwind-from-becoming-a-mess"&gt;Team Adoption: How to Keep Tailwind from Becoming a Mess&lt;/h2&gt;
&lt;p&gt;Tailwind works best when the team treats it like a system, not a free-for-all. Here are concrete ways to avoid the pitfalls—and yes, the “ugly HTML” complaint still matters, even if it’s not the whole story.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) Adopt a class ordering convention.&lt;/strong&gt;&lt;br&gt;
When you see &lt;code&gt;p-4 bg-blue-500 flex justify-center&lt;/code&gt;, your brain pays a tax every time. Decide on an order (layout → spacing → typography → colors → states), and stick to it. Tools like editor plugins can enforce sorting. The goal isn’t beauty for its own sake—it’s faster scanning and fewer mistakes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Use components where repetition proves itself.&lt;/strong&gt;&lt;br&gt;
If the same utility combination appears in twenty places, it’s time to abstract. Don’t wait for “architecture purity”—wait for repetition. That’s how you avoid premature abstraction while still keeping the codebase sane.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) Prefer semantic tokens over raw magic.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;bg-blue-500&lt;/code&gt; might be fine for a prototype. In a mature system, you want consistency and intent: “surface” and “primary” beat arbitrary shades. Configure your theme so your utilities reflect the product’s vocabulary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) Keep utilities for styling, not for logic.&lt;/strong&gt;&lt;br&gt;
Tailwind can tempt you to express complex decisions in long class lists. Use conditional class merging thoughtfully, and keep state logic readable. If your component’s class attribute becomes a novel, your UI logic is probably due for refactoring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5) Don’t pretend it eliminates design work.&lt;/strong&gt;&lt;br&gt;
Tailwind doesn’t remove the hard parts of UI—accessibility, responsive behavior, interaction states, and visual hierarchy still matter. It just reduces the friction between design intent and implementation.&lt;/p&gt;
&lt;h2 id="the-dead-css-argument-is-realand-tailwind-refuses-to-lie-about-it"&gt;The “Dead CSS” Argument Is Real—and Tailwind Refuses to Lie About It&lt;/h2&gt;
&lt;p&gt;Traditional CSS workflows have a silent cost: styles accrete even after components are removed or redesigned. Even teams with good intentions end up with stale selectors, unused variables, and “maybe we still need it” code. Purging helps, but it’s often procedural and easy to postpone.&lt;/p&gt;
&lt;p&gt;Tailwind flips the model. If a utility class isn’t used, it won’t matter. Your stylesheets are generated around what you actually reference. This directly addresses the fear that utility-first CSS creates noise. Tailwind doesn’t eliminate the need for discipline—it eliminates the need for constant housekeeping just to avoid CSS archaeology.&lt;/p&gt;
&lt;p&gt;The result is practical: fewer leftovers, fewer surprises, and fewer moments where the UI looks wrong because someone forgot which CSS file is still doing something.&lt;/p&gt;
&lt;h2 id="conclusion-ugly-markup-beautiful-outcomes"&gt;Conclusion: Ugly Markup, Beautiful Outcomes&lt;/h2&gt;
&lt;p&gt;Tailwind CSS doesn’t win because it’s pretty. It wins because it matches how development actually happens: fast iteration, shared components, shifting requirements, and teams that can’t afford endless specificity battles.&lt;/p&gt;
&lt;p&gt;The real insight isn’t that you should sprinkle utilities everywhere forever. It’s that design systems should constrain choices, not enable infinite ones—and Tailwind gives you a framework to encode constraints directly into the tooling. Yes, the HTML looks like someone threw up. But the product ships, the code stays coherent, and the system scales. That’s the ugly truth—and the pragmatic brilliance underneath it.&lt;/p&gt;</content></item><item><title>Elixir Is the Best-Kept Secret in Web Development</title><link>https://decastro.work/blog/elixir-best-kept-secret-web-development/</link><pubDate>Sun, 12 Jun 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/elixir-best-kept-secret-web-development/</guid><description>&lt;p&gt;If you’ve ever watched your “simple” web app turn into a concurrency nightmare—mysterious timeouts, cascading failures, and heroic scaling efforts—you already know the problem: most web runtimes treat concurrency as an afterthought. Elixir doesn’t. It leans into concurrency as a first-class design principle, and the result is a stack that feels both modern and strangely inevitable.&lt;/p&gt;
&lt;p&gt;This is why the BEAM VM (the engine behind Elixir and Erlang) keeps winning the same argument from different angles: lightweight concurrency, fault-tolerant architecture, and message passing that makes reliability easier—not harder. Meanwhile, the JavaScript and Python ecosystems keep papering over edge cases with more servers and more glue code.&lt;/p&gt;</description><content>&lt;p&gt;If you’ve ever watched your “simple” web app turn into a concurrency nightmare—mysterious timeouts, cascading failures, and heroic scaling efforts—you already know the problem: most web runtimes treat concurrency as an afterthought. Elixir doesn’t. It leans into concurrency as a first-class design principle, and the result is a stack that feels both modern and strangely inevitable.&lt;/p&gt;
&lt;p&gt;This is why the BEAM VM (the engine behind Elixir and Erlang) keeps winning the same argument from different angles: lightweight concurrency, fault-tolerant architecture, and message passing that makes reliability easier—not harder. Meanwhile, the JavaScript and Python ecosystems keep papering over edge cases with more servers and more glue code.&lt;/p&gt;
&lt;h2 id="the-real-reason-youre-struggling-with-concurrency"&gt;The real reason you’re struggling with concurrency&lt;/h2&gt;
&lt;p&gt;Let’s name the thing everyone dances around: concurrency isn’t just “how many requests per second.” It’s how your system behaves when &lt;em&gt;everything&lt;/em&gt; is happening at once—slow clients, intermittent network issues, timeouts in dependencies, retries, and partial failures.&lt;/p&gt;
&lt;p&gt;In many stacks, the runtime gives you primitives (threads, async/await, event loops), and then your application code becomes the safety net. You end up implementing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;backpressure handling (usually late),&lt;/li&gt;
&lt;li&gt;supervision logic (often as custom retry loops),&lt;/li&gt;
&lt;li&gt;isolation between failures (rarely comprehensive),&lt;/li&gt;
&lt;li&gt;and resource control (mostly via operational discipline).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So when the system degrades, you don’t get a clean failure. You get a feedback loop: one component slows down, queues grow, memory spikes, latency rises, and suddenly the entire service becomes “unavailable” even though most of it is still technically working.&lt;/p&gt;
&lt;p&gt;Elixir’s approach is different: it’s designed for concurrency and failure as normal conditions, not exceptional ones.&lt;/p&gt;
&lt;h2 id="beams-concurrency-model-processes-not-promises"&gt;BEAM’s concurrency model: processes, not promises&lt;/h2&gt;
&lt;p&gt;The BEAM VM runs your code with lightweight “processes” and a scheduler built for massive concurrency. These are not OS threads, and they’re not the same mental model as Node’s event loop or Python’s async tasks.&lt;/p&gt;
&lt;p&gt;Here’s the practical distinction: you can structure your application around many independent processes that do one thing well, communicate by sending messages, and fail without taking down the world.&lt;/p&gt;
&lt;h3 id="a-concrete-example-a-connection-heavy-web-service"&gt;A concrete example: a connection-heavy web service&lt;/h3&gt;
&lt;p&gt;Imagine a WebSocket service that tracks presence, chat messages, and typing indicators. In a typical JavaScript implementation, you might end up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;one large event loop coordinating everything,&lt;/li&gt;
&lt;li&gt;shared state guarded by careful code discipline,&lt;/li&gt;
&lt;li&gt;lots of “just do async” and “don’t block” rules,&lt;/li&gt;
&lt;li&gt;and a web of try/catch, retry policies, and cleanup logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With BEAM/Elixir, you can model each connection as its own process. That gives you isolation by default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If one client behaves badly, you can time out and terminate just that process.&lt;/li&gt;
&lt;li&gt;If message handling for one connection crashes, supervisors can restart it cleanly.&lt;/li&gt;
&lt;li&gt;If a downstream dependency hiccups, the rest of the system continues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And because message passing is built into the runtime model, you don’t need to invent a concurrency strategy from scratch. You write processes and define what happens when they fail. The VM does the scheduling and the heavy lifting.&lt;/p&gt;
&lt;p&gt;This is why concurrency discussions in other ecosystems often feel like they’re chasing a moving target. BEAM started with concurrency as the foundation. Everything else was layered later.&lt;/p&gt;
&lt;h2 id="supervision-trees-reliability-you-can-actually-reason-about"&gt;Supervision trees: reliability you can actually reason about&lt;/h2&gt;
&lt;p&gt;Fault tolerance in many runtimes is reactive. You detect failure, log it, and hope it doesn’t cascade. In the BEAM world, fault tolerance is explicit and structured.&lt;/p&gt;
&lt;p&gt;Elixir’s supervision trees are a hierarchy of processes where parents oversee children. If a child process crashes, the supervisor decides what to do: restart it, escalate, or shut down a branch.&lt;/p&gt;
&lt;p&gt;That sounds like an implementation detail—until you watch what it does to operational reality.&lt;/p&gt;
&lt;h3 id="practical-payoff-stop-unknown-state-incidents"&gt;Practical payoff: stop “unknown state” incidents&lt;/h3&gt;
&lt;p&gt;A common production horror story looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A background job crashes halfway through a workflow.&lt;/li&gt;
&lt;li&gt;Some in-memory state is now inconsistent.&lt;/li&gt;
&lt;li&gt;Subsequent retries either fail again or, worse, corrupt more state.&lt;/li&gt;
&lt;li&gt;The only safe fix is manual intervention.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With supervision, you can design systems so that crashing is survivable. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your connection process can crash and restart without leaving phantom subscriptions.&lt;/li&gt;
&lt;li&gt;Your worker can restart from a known baseline (or fetch state from a datastore) rather than continuing in a half-broken memory state.&lt;/li&gt;
&lt;li&gt;Your supervisors can apply backoff strategies when dependencies are unhealthy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key idea is not “fail less.” It’s “fail in a way that your architecture expects.”&lt;/p&gt;
&lt;h2 id="phoenix-delivers-rails-like-velocity-but-keeps-the-reliability-spine"&gt;Phoenix delivers Rails-like velocity, but keeps the reliability spine&lt;/h2&gt;
&lt;p&gt;Elixir is only half the story. Phoenix is where it becomes a product-building machine.&lt;/p&gt;
&lt;p&gt;Phoenix gives you an approach that feels familiar if you’ve built with Rails: a cohesive framework, conventions, and a clear path from controller to template to channel. You get productivity without surrendering the runtime advantages that come from BEAM’s concurrency model.&lt;/p&gt;
&lt;p&gt;Consider real-time features. Phoenix Channels let you implement WebSockets and long-lived connections without turning your codebase into a tangle of ad-hoc event handlers. You can build chat, notifications, dashboards, and collaborative experiences with the same disciplined process model—because the framework and runtime are designed to cooperate.&lt;/p&gt;
&lt;p&gt;And because Phoenix is built around Elixir’s concurrency primitives, you don’t have to contort your application architecture to “fit” the runtime. You simply build the way BEAM wants you to build.&lt;/p&gt;
&lt;h2 id="two-million-connections-isnt-the-pointpredictable-behavior-is"&gt;“Two million connections” isn’t the point—predictable behavior is&lt;/h2&gt;
&lt;p&gt;It’s tempting to lead with headline performance claims, but the more important takeaway is predictability under load.&lt;/p&gt;
&lt;p&gt;When people say Elixir can handle massive concurrency “without breaking a sweat,” what they usually mean is this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the system continues to respond under pressure,&lt;/li&gt;
&lt;li&gt;failures remain contained,&lt;/li&gt;
&lt;li&gt;and resource usage doesn’t turn chaotic in the same way that often happens in less structured concurrency models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, you’re not just buying throughput. You’re buying control.&lt;/p&gt;
&lt;h3 id="a-practical-rule-of-thumb"&gt;A practical rule of thumb&lt;/h3&gt;
&lt;p&gt;In ecosystems that rely heavily on shared event loops and async glue, you can often “get away with it” until you hit the wrong combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slow clients,&lt;/li&gt;
&lt;li&gt;burst traffic,&lt;/li&gt;
&lt;li&gt;and downstream latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Elixir’s model pushes you toward isolation and explicit recovery. That means you don’t just scale horizontally—you scale &lt;em&gt;safely&lt;/em&gt;, because the failure modes are governed by design, not luck.&lt;/p&gt;
&lt;h2 id="the-adoption-problem-is-cultural-not-technical"&gt;The adoption problem is cultural, not technical&lt;/h2&gt;
&lt;p&gt;Let’s be blunt: Elixir isn’t less popular because it’s hard. It’s less popular because the industry has trained itself to distrust anything that doesn’t look like it already knows how to work.&lt;/p&gt;
&lt;p&gt;There’s a persistent bias for curly braces, a preference for tooling that matches existing workflows, and a tendency to treat unfamiliar languages as “niche experiments.” That’s not a technical argument; it’s an inertia problem.&lt;/p&gt;
&lt;p&gt;Elixir is modern, pragmatic, and deeply engineering-oriented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Functional programming without turning everything into academic puzzles.&lt;/li&gt;
&lt;li&gt;A framework that emphasizes real-world application needs.&lt;/li&gt;
&lt;li&gt;Runtime behavior designed for reliability, not just speed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your team can learn how to build with Elixir once, the architecture tends to become the kind of “default good taste” that prevents whole categories of production failures.&lt;/p&gt;
&lt;h2 id="conclusion-stop-papering-over-concurrencyembrace-it"&gt;Conclusion: stop papering over concurrency—embrace it&lt;/h2&gt;
&lt;p&gt;Elixir’s best-kept-secret value isn’t that it can out-benchmark your stack on a chart. It’s that BEAM treats concurrency and failure as foundational concerns rather than afterthoughts.&lt;/p&gt;
&lt;p&gt;Phoenix then makes that runtime advantage practical: you can build real web apps—controllers, templates, channels, background work—with a framework that rewards disciplined architecture. And the biggest win? You spend less time firefighting because your system is structured to recover.&lt;/p&gt;
&lt;p&gt;If your current stack feels like it’s scaling by duct tape, Elixir is the rare alternative that doesn’t just add more features—it changes the shape of reliability itself.&lt;/p&gt;</content></item><item><title>PostgreSQL Won the Database War While Nobody Was Watching</title><link>https://decastro.work/blog/postgresql-won-database-war/</link><pubDate>Tue, 07 Jun 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/postgresql-won-database-war/</guid><description>&lt;p&gt;For years, developers watched database headlines like sports—Mongo this, NewSQL that, graphs everywhere, streams always. Meanwhile, one unglamorous workhorse quietly kept showing up in production, powering dashboards, APIs, admin panels, and “just one more feature” workflows. PostgreSQL didn’t win with drama. It won with taste, reliability, and the kind of tooling that makes you forget you’re using a database at all.&lt;/p&gt;
&lt;h2 id="the-real-war-friction-not-features"&gt;The real “war”: friction, not features&lt;/h2&gt;
&lt;p&gt;Most database debates aren’t about raw capability. They’re about friction—how quickly you can ship, how safely you can change, and how much pain you’ll feel when the product inevitably grows teeth.&lt;/p&gt;</description><content>&lt;p&gt;For years, developers watched database headlines like sports—Mongo this, NewSQL that, graphs everywhere, streams always. Meanwhile, one unglamorous workhorse quietly kept showing up in production, powering dashboards, APIs, admin panels, and “just one more feature” workflows. PostgreSQL didn’t win with drama. It won with taste, reliability, and the kind of tooling that makes you forget you’re using a database at all.&lt;/p&gt;
&lt;h2 id="the-real-war-friction-not-features"&gt;The real “war”: friction, not features&lt;/h2&gt;
&lt;p&gt;Most database debates aren’t about raw capability. They’re about friction—how quickly you can ship, how safely you can change, and how much pain you’ll feel when the product inevitably grows teeth.&lt;/p&gt;
&lt;p&gt;PostgreSQL won because it handles the boring stuff extremely well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correctness under pressure&lt;/strong&gt;: transactions, constraints, and consistent query behavior aren’t optional when your business logic has edge cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A sane query language&lt;/strong&gt;: SQL is expressive enough to model real problems without turning your app into a data-munging Rube Goldberg machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational maturity&lt;/strong&gt;: backups, migrations, monitoring, and failure recovery are not “enterprise-only fantasies.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;NoSQL hype promised escape from relational rigidity. What teams actually needed was escape from operational chaos. PostgreSQL gave them that—and did it without asking them to rewrite everything into a new mental model.&lt;/p&gt;
&lt;p&gt;A practical example: imagine you’re building a billing system. You don’t want “eventually consistent” charges that get reconciled later by a nightly job you don’t fully trust. You want atomic updates, constraints, and queries that can answer questions like, “Show me customers whose invoice totals don’t match payments due to refunds within the last 30 days.” That’s the relational sweet spot.&lt;/p&gt;
&lt;h2 id="the-trap-in-the-nosql-story-you-still-need-relationships"&gt;The trap in the NoSQL story: you still need relationships&lt;/h2&gt;
&lt;p&gt;MongoDB (and other document databases) did something important: they made “schema” feel optional and made it easy to store JSON-like documents. That lowered the barrier to building prototypes fast—especially for teams that were already living in JavaScript objects.&lt;/p&gt;
&lt;p&gt;But production has a way of demanding structure. Over time, applications discover that they need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;joins (even if they’re inconvenient),&lt;/li&gt;
&lt;li&gt;referential integrity (even if it’s “later”),&lt;/li&gt;
&lt;li&gt;complex filtering across entities,&lt;/li&gt;
&lt;li&gt;and consistency guarantees when money or permissions are involved.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL absorbed the best parts of the modern web without abandoning relational discipline. JSON support means you can keep the flexibility you want. Relational modeling means you don’t paint yourself into a corner when “flexible” starts to mean “ambiguous.”&lt;/p&gt;
&lt;p&gt;And for many teams, that’s the key takeaway: PostgreSQL isn’t a relic of old databases. It’s the database that lets you postpone schema decisions without permanently surrendering the ability to reason about data.&lt;/p&gt;
&lt;h2 id="postgres-covers-the-messiest-90and-then-gets-out-of-the-way"&gt;Postgres covers the messiest 90%—and then gets out of the way&lt;/h2&gt;
&lt;p&gt;If you’ve ever built an application that started simple and ended up with a web of requirements—search, filters, locations, analytics, background jobs—you know the “single database” fantasy rarely survives contact with stakeholders.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s advantage is that it can grow with you inside one system. Instead of introducing new infrastructure every time you hit a capability gap, Postgres has a growing set of built-in features and extensions that keep the stack coherent.&lt;/p&gt;
&lt;p&gt;Here are a few common requirements and how Postgres handles them without ceremony:&lt;/p&gt;
&lt;h3 id="json-without-losing-query-power"&gt;JSON without losing query power&lt;/h3&gt;
&lt;p&gt;You can store semi-structured data in &lt;code&gt;jsonb&lt;/code&gt; and still query it efficiently. In real projects, this often looks like: store user metadata, feature flags, or third-party payloads as JSON, then index the parts you actually filter on.&lt;/p&gt;
&lt;p&gt;Instead of “everything is a document and we hope,” you get “we store the flexible stuff, and we still know how to ask questions.”&lt;/p&gt;
&lt;h3 id="full-text-search-that-doesnt-feel-like-a-separate-universe"&gt;Full-text search that doesn’t feel like a separate universe&lt;/h3&gt;
&lt;p&gt;Postgres can handle search use cases without immediately pulling in a separate search engine. You can implement keyword search, ranking, and text normalization in ways that integrate cleanly with your relational data.&lt;/p&gt;
&lt;p&gt;A good pattern: keep your core records in Postgres, and only escalate to a dedicated search platform when you truly need advanced relevance tuning or massive scale.&lt;/p&gt;
&lt;h3 id="geospatial-queries-that-work-with-your-data-model"&gt;Geospatial queries that work with your data model&lt;/h3&gt;
&lt;p&gt;If your product has locations—delivery zones, user proximity, asset tracking—PostGIS turns Postgres into a geospatial powerhouse. Crucially, it keeps your location data tied to the rest of your domain: permissions, ownership, time windows, status transitions.&lt;/p&gt;
&lt;h3 id="the-just-one-more-feature-effect"&gt;The “just one more feature” effect&lt;/h3&gt;
&lt;p&gt;Most teams don’t die from missing features. They die from feature sprawl. When your database strategy forces you to split state across multiple systems, every new requirement becomes a distributed-systems problem.&lt;/p&gt;
&lt;p&gt;PostgreSQL makes it easier to stay focused on product work instead of infrastructure glue.&lt;/p&gt;
&lt;h2 id="the-extension-ecosystem-postgres-as-a-platform-not-a-product"&gt;The extension ecosystem: Postgres as a platform, not a product&lt;/h2&gt;
&lt;p&gt;The modern Postgres story isn’t “here’s a database.” It’s “here’s a platform.” That might sound like marketing, but in practice it changes how teams design systems.&lt;/p&gt;
&lt;p&gt;When your needs expand—from time-series metrics to embeddings for AI features—Postgres can often extend rather than replace.&lt;/p&gt;
&lt;p&gt;Some of the most practical examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PostGIS&lt;/strong&gt; for geospatial data and queries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TimescaleDB&lt;/strong&gt; for time-series patterns like rollups, retention policies, and efficient queries over time-bounded windows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pg_vector&lt;/strong&gt; for vector similarity search, letting you bring embeddings into the same database that already owns your user and permission data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s why that matters: when you move everything into one consistent data store, you reduce the number of sources of truth. Your application logic becomes simpler because data retrieval is consistent and transactional where it matters.&lt;/p&gt;
&lt;p&gt;Even if you still use other systems (caches, message queues, dedicated analytics warehouses), you avoid turning your core domain model into a patchwork.&lt;/p&gt;
&lt;p&gt;A concrete design tip: if you’re building something AI-adjacent, store the canonical entity data in Postgres, store embeddings in Postgres via a vector extension, and only add a specialized vector database if you outgrow your query patterns. Start with fewer moving parts. Upgrade when you have evidence, not vibes.&lt;/p&gt;
&lt;h2 id="why-developers-kept-coming-back-sql-beats-rewrites"&gt;Why developers kept coming back: SQL beats rewrites&lt;/h2&gt;
&lt;p&gt;The hype cycles always sell a version of the dream: “Reimagine your data model from scratch.” And sometimes that works—especially for products that are tightly scoped or whose domain is inherently document-structured.&lt;/p&gt;
&lt;p&gt;But for most software teams, rewriting is the real cost. It’s not the database choice at day one. It’s the migration pain at day 600, when requirements are entrenched and institutional knowledge lives in production code.&lt;/p&gt;
&lt;p&gt;SQL also has an underrated advantage: it scales across teams. Querying data is something you can teach, review, and audit. You can reason about it with plain text. You can introspect the schema. You can validate assumptions.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s strongest feature may be its pragmatism. It’s a database that doesn’t demand ideology. If you want relational integrity, it’s there. If you want JSON flexibility, it’s there. If you want full-text search or geospatial queries, it’s there. And if you want new capabilities, the extension ecosystem often meets you halfway.&lt;/p&gt;
&lt;p&gt;The result is the “quiet victory” you can feel in teams: fewer database debates, more shipping, and fewer late-night incidents caused by mismatched persistence assumptions.&lt;/p&gt;
&lt;h2 id="conclusion-the-quiet-win-is-the-best-kind"&gt;Conclusion: the quiet win is the best kind&lt;/h2&gt;
&lt;p&gt;PostgreSQL didn’t conquer the database war by being loud. It conquered it by being useful—again and again—across the messy reality of application development.&lt;/p&gt;
&lt;p&gt;If you’re choosing a database today, don’t treat Postgres as a default because it’s old. Treat it as a default because it’s complete: relational rigor with modern flexibility, powerful built-ins, and extensions that turn it into a real data platform. In a world obsessed with the next thing, that kind of stability is rare—and worth paying attention to.&lt;/p&gt;</content></item><item><title>Vim Motions Changed How I Think About Text Editing</title><link>https://decastro.work/blog/vim-motions-changed-text-editing/</link><pubDate>Thu, 26 May 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/vim-motions-changed-text-editing/</guid><description>&lt;p&gt;I didn’t “learn Vim” so much as I discovered a different way to talk to my editor. For fifteen years I used conventional shortcuts—Ctrl+X here, Ctrl+F there—without ever feeling in control of the &lt;em&gt;structure&lt;/em&gt; of my text. Then I started using Vim motions, and suddenly every change I made felt less like hunting and pecking and more like composing a sentence. The mouse still works, but now it feels like writing with mittens on.&lt;/p&gt;</description><content>&lt;p&gt;I didn’t “learn Vim” so much as I discovered a different way to talk to my editor. For fifteen years I used conventional shortcuts—Ctrl+X here, Ctrl+F there—without ever feeling in control of the &lt;em&gt;structure&lt;/em&gt; of my text. Then I started using Vim motions, and suddenly every change I made felt less like hunting and pecking and more like composing a sentence. The mouse still works, but now it feels like writing with mittens on.&lt;/p&gt;
&lt;p&gt;And no, you don’t need to ditch VS Code or switch to Neovim to get the benefit. What you need is the &lt;em&gt;grammar&lt;/em&gt; of modal editing: verbs that define intent (yank, delete, change) combined with nouns that define scope (word, paragraph, matching bracket). Learn that grammar, and your editing becomes fast in the specific way that only composable tools feel fast.&lt;/p&gt;
&lt;h2 id="the-moment-it-stopped-being-shortcuts-and-became-language"&gt;The moment it stopped being “shortcuts” and became “language”&lt;/h2&gt;
&lt;p&gt;Most editors teach you keystrokes as a set of tricks. “Press Ctrl+Shift+L to select multiple cursors,” “Use Alt+Click to add cursors,” “Hit Ctrl+K, Ctrl+C to comment.” Useful, but still mostly procedural: do this, then that, then maybe a third thing.&lt;/p&gt;
&lt;p&gt;Vim is different. Modal editing flips the model from &lt;em&gt;actions&lt;/em&gt; to &lt;em&gt;commands&lt;/em&gt;. In normal mode, you don’t “manipulate” text; you &lt;em&gt;request transformations&lt;/em&gt; in a compact syntax:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verb&lt;/strong&gt;: what you want to do (yank, delete, change, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Noun&lt;/strong&gt;: what you want to affect (word, line, paragraph, block, bracketed text, etc.)&lt;/li&gt;
&lt;li&gt;Optional &lt;strong&gt;modifiers&lt;/strong&gt;: how precisely you mean it (counts like &lt;code&gt;3&lt;/code&gt;, motions like &lt;code&gt;f&lt;/code&gt;/&lt;code&gt;t&lt;/code&gt;, and text objects)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you start thinking in those terms, your brain stops asking “what’s the shortcut?” and starts asking “what’s the unit of text I intend to operate on?” That shift is the real turbo mode.&lt;/p&gt;
&lt;h2 id="modal-editing-why-normal-mode-makes-you-faster-and-calmer"&gt;Modal editing: why “normal mode” makes you faster (and calmer)&lt;/h2&gt;
&lt;p&gt;The simplest way to understand Vim motions is this: &lt;strong&gt;normal mode is for navigating and transforming&lt;/strong&gt;, insert mode is for writing, and every key press is categorized.&lt;/p&gt;
&lt;p&gt;In conventional editors, the same key might both navigate and edit depending on timing or focus state. That’s one reason keyboard-driven editing can feel chaotic—your hands are constantly negotiating context.&lt;/p&gt;
&lt;p&gt;Vim resolves that by making state explicit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Normal mode&lt;/strong&gt;: commands like &lt;code&gt;dw&lt;/code&gt;, &lt;code&gt;ci(&lt;/code&gt;, &lt;code&gt;yip&lt;/code&gt;, &lt;code&gt;ggVG&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insert mode&lt;/strong&gt;: you’re literally typing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual mode&lt;/strong&gt;: you’re selecting something to operate on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical example: imagine you’re editing a function and want to replace the argument list.&lt;/p&gt;
&lt;p&gt;In a traditional workflow you might: select, delete, retype, and then carefully re-check commas and spacing.&lt;/p&gt;
&lt;p&gt;In Vim grammar, you can do something like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ci(&lt;/code&gt; — &lt;strong&gt;change&lt;/strong&gt; inside the &lt;strong&gt;parentheses&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even without deep knowledge of Vim, the intent is readable. You’re telling the editor: &lt;em&gt;target the argument region, and replace it&lt;/em&gt;. Your eyes can stay on the structure; your fingers don’t have to “search for the right selection behavior.”&lt;/p&gt;
&lt;h2 id="the-core-grammar-verb--noun-with-real-examples"&gt;The core “grammar”: verb + noun (with real examples)&lt;/h2&gt;
&lt;p&gt;If you take nothing else from Vim motions, take this: &lt;strong&gt;the most productive commands are built from reusable parts&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="verbs-youll-use-constantly"&gt;Verbs you’ll use constantly&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt; = &lt;strong&gt;yank&lt;/strong&gt; (copy)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt; = &lt;strong&gt;delete&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; = &lt;strong&gt;change&lt;/strong&gt; (delete and enter insert mode)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;&lt;/code&gt; = indent shift&lt;/li&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; = re-indent a region (handy when formatting drifts)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nouns-targets-that-map-to-how-humans-think"&gt;Nouns (targets) that map to how humans think&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;w&lt;/code&gt; = &lt;strong&gt;word&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;W&lt;/code&gt; = WORD (big, space-delimited)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; / &lt;code&gt;p&lt;/code&gt; = sentence / paragraph&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(&lt;/code&gt;, &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt; = matching bracketed expressions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; / &lt;code&gt;a&lt;/code&gt; text objects: &lt;strong&gt;inside&lt;/strong&gt; vs &lt;strong&gt;around&lt;/strong&gt; (more on this next)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="two-examples-that-show-the-pattern"&gt;Two examples that show the pattern&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Delete a word&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dw&lt;/code&gt;: delete from the cursor to the start of the next word&lt;br&gt;
This turns “remove the next word” into a single, repeatable instruction.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;&lt;strong&gt;Change inside braces&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ci{&lt;/code&gt; (or more generally &lt;code&gt;ci{&lt;/code&gt; after a cursor near &lt;code&gt;{&lt;/code&gt;)&lt;br&gt;
You’re not selecting manually. You’re declaring the semantic boundary: &lt;em&gt;inside the braces&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the mental advantage: the command reads like a plan. Once you internalize that, you stop fighting the selection tool and start steering the document.&lt;/p&gt;
&lt;h2 id="text-objects-the-superpower-youll-feel-after-a-few-days"&gt;Text objects: the superpower you’ll feel after a few days&lt;/h2&gt;
&lt;p&gt;Navigation motions get you to places. &lt;strong&gt;Text objects&lt;/strong&gt; let you grab meaningful chunks without measuring by hand.&lt;/p&gt;
&lt;p&gt;The classic pair is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; = inside&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a&lt;/code&gt; = around&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iw&lt;/code&gt; / &lt;code&gt;aw&lt;/code&gt; = inside/around a word&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip&lt;/code&gt; / &lt;code&gt;ap&lt;/code&gt; = inside/around a paragraph&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i(&lt;/code&gt; / &lt;code&gt;a(&lt;/code&gt; = inside/around parentheses&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i{&lt;/code&gt; / &lt;code&gt;a{&lt;/code&gt; = inside/around braces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete scenario: you’re refactoring a config file and need to swap a JSON value that sits between braces. You don’t want to highlight a range by eye and risk including or excluding quotes and commas. Instead, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;move the cursor somewhere within the target region&lt;/li&gt;
&lt;li&gt;use a text object to define the boundary&lt;/li&gt;
&lt;li&gt;apply a verb: &lt;code&gt;ci{&lt;/code&gt; to replace, &lt;code&gt;di{&lt;/code&gt; to remove, or &lt;code&gt;yi{&lt;/code&gt; to copy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the “turbo mode” feeling really kicks in: you’re not spending time micromanaging the edit region. You’re describing it.&lt;/p&gt;
&lt;h2 id="just-learn-the-grammar-a-practical-two-week-ramp"&gt;“Just learn the grammar”: a practical two-week ramp&lt;/h2&gt;
&lt;p&gt;You don’t need to memorize the entire keymap. You need a small set of moves that let you build confidence, then expand.&lt;/p&gt;
&lt;p&gt;Here’s a sane two-week approach that matches how people actually learn:&lt;/p&gt;
&lt;h3 id="days-13-learn-verbs--motions-that-map-to-your-muscle-memory"&gt;Days 1–3: learn verbs + motions that map to your muscle memory&lt;/h3&gt;
&lt;p&gt;Pick a few everyday actions you already do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;delete a word (&lt;code&gt;dw&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;yank a word (&lt;code&gt;yw&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;change a word (&lt;code&gt;cw&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;move by word (&lt;code&gt;w&lt;/code&gt; / &lt;code&gt;b&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practice with tiny edits in a scratch buffer or a real file you’re already touching. The goal is to stop thinking about “shortcuts” and start thinking “word boundaries, sentence boundaries, block boundaries.”&lt;/p&gt;
&lt;h3 id="days-47-add-text-objects"&gt;Days 4–7: add text objects&lt;/h3&gt;
&lt;p&gt;Introduce just one family:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i(&lt;/code&gt; and &lt;code&gt;a(&lt;/code&gt; for parentheses&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i{&lt;/code&gt; and &lt;code&gt;a{&lt;/code&gt; for braces&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip&lt;/code&gt; for paragraphs (great for prose and comments)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then practice a single loop: replace something inside a structure. If you can do that confidently, your editing strategy will start to change immediately.&lt;/p&gt;
&lt;h3 id="days-814-build-a-small-repertoire-of-repeatable-commands"&gt;Days 8–14: build a small repertoire of repeatable commands&lt;/h3&gt;
&lt;p&gt;Add:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt; to repeat the last change (this alone can feel like cheating)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt; for deleting a line&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yy&lt;/code&gt; for yanking a line&lt;/li&gt;
&lt;li&gt;selection via Visual mode for when you need “human judgment” rather than grammar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You’ll feel slow early on. That’s normal. After a week, you’ll start issuing commands almost without looking at the keys. After two weeks, the mouse will become optional—not because you’re stubborn, but because the keyboard finally has a coherent system.&lt;/p&gt;
&lt;h2 id="you-can-start-in-vs-code-with-a-vim-style-extensionno-ideology-required"&gt;You can start in VS Code with a Vim-style extension—no ideology required&lt;/h2&gt;
&lt;p&gt;If your current setup is VS Code, you can get 90% of the benefit without religiously switching editors. Install a Vim extension in VS Code and treat it like a training regimen.&lt;/p&gt;
&lt;p&gt;Important advice: &lt;strong&gt;don’t wait for “perfect Vim.”&lt;/strong&gt; Use the extension on tasks where you edit text repeatedly—code review, logs, refactors, documentation. When you hit a command you don’t know, don’t quit. Look it up, try it once, and move on.&lt;/p&gt;
&lt;p&gt;Two weeks is realistic because you’re not learning “Vim as an identity.” You’re learning a language for editing: verbs and nouns, composable targets, and a reliable state machine (normal vs insert vs visual).&lt;/p&gt;
&lt;h2 id="conclusion-once-you-think-in-commands-the-mouse-feels-clumsy"&gt;Conclusion: once you think in commands, the mouse feels clumsy&lt;/h2&gt;
&lt;p&gt;Vim motions didn’t just make me faster—they changed how I &lt;em&gt;frame&lt;/em&gt; the act of editing. Instead of selecting regions and hoping I got the boundary right, I started describing transformations: delete this unit, change that structure, yank this block. That’s why the mouse suddenly feels like writing with mittens on. It’s not that it’s bad; it’s that it’s no longer the most natural way to express intent.&lt;/p&gt;
&lt;p&gt;Give it two weeks. Learn the grammar—verbs, nouns, and text objects—and watch your text editing stop being a grab-bag of shortcuts and start behaving like composition.&lt;/p&gt;</content></item><item><title>Every Developer Should Understand Observability (Not Just Monitoring)</title><link>https://decastro.work/blog/every-developer-understand-observability-not-monitoring/</link><pubDate>Sat, 14 May 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/every-developer-understand-observability-not-monitoring/</guid><description>&lt;p&gt;You can’t reliably fix what you can’t explain. Monitoring may light up a dashboard when your system is in trouble, but observability is what lets you answer the real question: &lt;em&gt;why&lt;/em&gt; did it break, &lt;em&gt;where&lt;/em&gt; did it start, and &lt;em&gt;what&lt;/em&gt; changed? In modern production systems—especially distributed ones—understanding observability isn’t optional. It’s core engineering literacy.&lt;/p&gt;
&lt;h2 id="monitoring-vs-observability-two-different-answers-to-two-different-questions"&gt;Monitoring vs. Observability: Two Different Answers to Two Different Questions&lt;/h2&gt;
&lt;p&gt;Monitoring and observability often get lumped together, but they solve different problems.&lt;/p&gt;</description><content>&lt;p&gt;You can’t reliably fix what you can’t explain. Monitoring may light up a dashboard when your system is in trouble, but observability is what lets you answer the real question: &lt;em&gt;why&lt;/em&gt; did it break, &lt;em&gt;where&lt;/em&gt; did it start, and &lt;em&gt;what&lt;/em&gt; changed? In modern production systems—especially distributed ones—understanding observability isn’t optional. It’s core engineering literacy.&lt;/p&gt;
&lt;h2 id="monitoring-vs-observability-two-different-answers-to-two-different-questions"&gt;Monitoring vs. Observability: Two Different Answers to Two Different Questions&lt;/h2&gt;
&lt;p&gt;Monitoring and observability often get lumped together, but they solve different problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Monitoring&lt;/strong&gt; is proactive detection. You set thresholds (“CPU &amp;gt; 90%,” “error rate &amp;gt; 1%,” “P99 latency &amp;gt; 500ms”) and alert when something crosses them. This is valuable, but it’s inherently reactive and limited: monitoring tells you &lt;em&gt;that&lt;/em&gt; you have an issue, not &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt; is about diagnostic capability. You instrument your system so that, when something goes wrong, you can ask arbitrary questions—questions you didn’t pre-plan—using the data you already emitted. Instead of building one dashboard per incident, you build a system of signals that can be queried to reconstruct causality.&lt;/p&gt;
&lt;p&gt;A useful mental model: monitoring answers &lt;strong&gt;“Are we broken?”&lt;/strong&gt; Observability answers &lt;strong&gt;“What’s broken and why?”&lt;/strong&gt; When teams say they “moved to observability,” what they usually mean is: they stopped treating telemetry as a checklist and started treating it as an investigative tool.&lt;/p&gt;
&lt;h2 id="the-three-lenses-logs-metrics-and-tracesone-system-different-views"&gt;The Three Lenses: Logs, Metrics, and Traces—One System, Different Views&lt;/h2&gt;
&lt;p&gt;Logs, metrics, and traces are often presented as three separate stacks. In practice, they’re three lenses on the same runtime behavior. Treating them as one correlated system is the difference between “we have dashboards” and “we can explain production.”&lt;/p&gt;
&lt;h3 id="logs-context-and-narrative"&gt;Logs: Context and narrative&lt;/h3&gt;
&lt;p&gt;Logs are your detailed narrative—events in time order. The key is to make logs &lt;strong&gt;structured&lt;/strong&gt; and &lt;strong&gt;queryable&lt;/strong&gt;, not just “human-readable text dumped to stdout.”&lt;/p&gt;
&lt;p&gt;A structured log line might look like this conceptually:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;service.name&lt;/code&gt;: &lt;code&gt;checkout-api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user.id&lt;/code&gt;: &lt;code&gt;12345&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;correlation_id&lt;/code&gt;: &lt;code&gt;b7f8...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;event&lt;/code&gt;: &lt;code&gt;payment_authorization_failed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error.code&lt;/code&gt;: &lt;code&gt;card_declined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;duration_ms&lt;/code&gt;: &lt;code&gt;42&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without structure, you end up grepping through noise during an incident. With structure—and consistent fields—you can slice logs by correlation IDs, service boundaries, error codes, and time windows.&lt;/p&gt;
&lt;h3 id="metrics-quantities-you-can-slice"&gt;Metrics: Quantities you can slice&lt;/h3&gt;
&lt;p&gt;Metrics summarize behavior over time. They’re excellent for trend detection and for measuring performance characteristics like latency distributions and saturation (CPU, memory, queue depth).&lt;/p&gt;
&lt;p&gt;The trap is &lt;strong&gt;cardinality&lt;/strong&gt;—labels with too many unique values (like raw user IDs or request IDs) can explode cost and overwhelm your time-series database. The practical rule: labels should represent &lt;em&gt;dimensions of analysis&lt;/em&gt;, not raw identifiers.&lt;/p&gt;
&lt;p&gt;Use labels like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;route&lt;/code&gt;: &lt;code&gt;/checkout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependency&lt;/code&gt;: &lt;code&gt;payment-gateway&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status_code&lt;/code&gt;: &lt;code&gt;200|400|500&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt;: &lt;code&gt;us-east-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid labels like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;request_id&lt;/code&gt;: unique per request (unless you’re using it carefully in limited contexts)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user_id&lt;/code&gt;: unbounded&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="traces-causality-across-boundaries"&gt;Traces: Causality across boundaries&lt;/h3&gt;
&lt;p&gt;Traces are the glue for distributed systems. A trace represents a single request (or workflow) flowing across services, with spans for each hop. Traces let you see where time is spent, where errors originate, and how dependencies behave.&lt;/p&gt;
&lt;p&gt;The critical point: traces are most powerful when they’re connected to logs and metrics via &lt;strong&gt;shared identifiers&lt;/strong&gt; (especially correlation IDs / trace IDs).&lt;/p&gt;
&lt;h2 id="instrumentation-that-actually-helps-correlation-ids-and-opentelemetry"&gt;Instrumentation That Actually Helps: Correlation IDs and OpenTelemetry&lt;/h2&gt;
&lt;p&gt;If you want observability that can answer “why,” you need consistent instrumentation. That means two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Propagate context across services&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Emit data in compatible formats with consistent fields&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="start-with-correlation-ids"&gt;Start with correlation IDs&lt;/h3&gt;
&lt;p&gt;A correlation ID is the simplest “thread” you can follow end-to-end. In an HTTP request, you can generate a correlation ID at the edge, include it in response headers, and propagate it to downstream calls.&lt;/p&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you’re behind an API gateway or load balancer, inject the correlation ID there.&lt;/li&gt;
&lt;li&gt;In your application, read the incoming header (if present) and set it on outgoing requests.&lt;/li&gt;
&lt;li&gt;Make sure every log line includes it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="then-adopt-distributed-tracing-opentelemetry"&gt;Then adopt distributed tracing (OpenTelemetry)&lt;/h3&gt;
&lt;p&gt;Distributed tracing becomes real when it’s standardized and automatically handled at scale. OpenTelemetry gives you a common way to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create spans&lt;/li&gt;
&lt;li&gt;capture timing and errors&lt;/li&gt;
&lt;li&gt;propagate trace context between services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A strong implementation pattern looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each incoming request creates a root span.&lt;/li&gt;
&lt;li&gt;Each outbound dependency call becomes a child span.&lt;/li&gt;
&lt;li&gt;Errors are recorded on the span with meaningful attributes.&lt;/li&gt;
&lt;li&gt;Your exporter ships traces to your tracing backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best part? Once you have trace IDs in your logs and trace summaries in your tracing UI, you can pivot instantly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“This request is slow” → “Show me the trace” → “Which span was slow?” → “Open related logs for that span.”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="cardinality-sampling-and-the-reality-of-production-costs"&gt;Cardinality, Sampling, and the Reality of Production Costs&lt;/h2&gt;
&lt;p&gt;Observability isn’t free. Good systems engineering includes constraints and trade-offs.&lt;/p&gt;
&lt;h3 id="cardinality-rules-your-budget"&gt;Cardinality rules your budget&lt;/h3&gt;
&lt;p&gt;If you label metrics with overly specific identifiers, you’ll pay in storage, query performance, and cost. Even worse, developers stop using the metrics because results are unreliable or expensive to query.&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer enums with bounded values (&lt;code&gt;status_code&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;route&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Bucket dimensions (e.g., latency buckets, not raw durations)&lt;/li&gt;
&lt;li&gt;For high-cardinality fields, move them to logs or traces where they belong&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="sampling-isnt-a-compromiseits-a-strategy"&gt;Sampling isn’t a compromise—it’s a strategy&lt;/h3&gt;
&lt;p&gt;You generally can’t trace every request forever. Sampling controls volume. But sampling done blindly can make incidents harder to debug.&lt;/p&gt;
&lt;p&gt;Better approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Head-based sampling&lt;/strong&gt;: choose a sample rate and accept that you’ll miss some traces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tail-based sampling&lt;/strong&gt; (where supported): keep traces that match “interesting” criteria (errors, high latency).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic sampling policies&lt;/strong&gt;: increase sampling when incidents start or when error rates spike.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is to preserve the ability to answer &lt;em&gt;why&lt;/em&gt; during failures. During normal periods, you can sample more aggressively.&lt;/p&gt;
&lt;h3 id="treat-telemetry-like-product-code"&gt;Treat telemetry like product code&lt;/h3&gt;
&lt;p&gt;Observability instrumentation changes over time. Add fields carefully. Deprecate old ones. Ensure that changes don’t break your correlation strategy. A common failure mode is “instrumentation drift,” where one service starts emitting different attribute names and suddenly dashboards and trace searches stop working.&lt;/p&gt;
&lt;h2 id="how-developers-use-observability-during-real-incidents"&gt;How Developers Use Observability During Real Incidents&lt;/h2&gt;
&lt;p&gt;This is where observability earns its keep. Consider a scenario that monitoring catches quickly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alerts fire for “P99 latency increased” last Tuesday.&lt;/li&gt;
&lt;li&gt;Error rates may or may not be elevated.&lt;/li&gt;
&lt;li&gt;You need a root cause fast.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With a mature observability setup, your investigation might look like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Confirm the scope&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use metrics to pinpoint the affected service and route.&lt;/li&gt;
&lt;li&gt;Break down latency by &lt;code&gt;region&lt;/code&gt; and &lt;code&gt;dependency&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You discover, for example, that only &lt;code&gt;/checkout&lt;/code&gt; in &lt;code&gt;eu-west-1&lt;/code&gt; is impacted.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Find the failing dependency&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compare latency metrics labeled by &lt;code&gt;dependency&lt;/code&gt; (e.g., &lt;code&gt;payment-gateway&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If the dependency latency spikes, your suspicion narrows quickly.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pivot to traces&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter traces by &lt;code&gt;trace_id&lt;/code&gt; (from logs) or by time window and service attributes.&lt;/li&gt;
&lt;li&gt;Identify a span with long duration and check error tags.&lt;/li&gt;
&lt;li&gt;You might see &lt;code&gt;payment-gateway &amp;gt; auth_request&lt;/code&gt; consistently taking longer during the incident window.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read the logs behind the spans&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open logs correlated to a representative trace.&lt;/li&gt;
&lt;li&gt;Look for structured attributes like &lt;code&gt;error.code&lt;/code&gt;, &lt;code&gt;retry_count&lt;/code&gt;, &lt;code&gt;timeout_ms&lt;/code&gt;, or circuit breaker state.&lt;/li&gt;
&lt;li&gt;You may discover the gateway started throttling and your client retried until a timeout.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Turn the story into action&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement backoff/jitter or circuit breaker tuning.&lt;/li&gt;
&lt;li&gt;Adjust request timeouts based on observed dependency behavior.&lt;/li&gt;
&lt;li&gt;Add targeted dashboards for the exact attribute you used to debug (“throttling response code,” “retries,” “timeout_ms buckets”).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice what’s happening: you aren’t just reading one chart. You’re reconstructing causality across the system using correlated signals.&lt;/p&gt;
&lt;h2 id="building-an-observability-stack-your-team-will-actually-use"&gt;Building an Observability Stack Your Team Will Actually Use&lt;/h2&gt;
&lt;p&gt;The temptation is to “set up tools” and assume the value follows. It won’t. The real work is aligning instrumentation, naming, and developer workflows.&lt;/p&gt;
&lt;h3 id="define-the-fields-that-matter"&gt;Define the fields that matter&lt;/h3&gt;
&lt;p&gt;Agree on conventions for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;service.name&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;environment&lt;/code&gt; (prod/staging)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;route&lt;/code&gt; or &lt;code&gt;operation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;correlation_id&lt;/code&gt; / &lt;code&gt;trace_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error.code&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependency.name&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure every team uses the same names so queries don’t become archaeology.&lt;/p&gt;
&lt;h3 id="make-it-easy-to-pivot"&gt;Make it easy to pivot&lt;/h3&gt;
&lt;p&gt;Your tooling should support the same flow every time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;click from a metric spike to traces&lt;/li&gt;
&lt;li&gt;click from a trace to logs for a specific span&lt;/li&gt;
&lt;li&gt;use shared IDs to avoid manual searching&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If this pivoting isn’t smooth, people will fall back to guesswork.&lt;/p&gt;
&lt;h3 id="start-small-then-deepen"&gt;Start small, then deepen&lt;/h3&gt;
&lt;p&gt;You don’t need perfect coverage everywhere on day one. You need coverage where it counts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;critical request paths&lt;/li&gt;
&lt;li&gt;high-impact dependencies&lt;/li&gt;
&lt;li&gt;services with known latency or reliability risk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then iterate. The second biggest observability mistake (after missing correlation) is stopping too early.&lt;/p&gt;
&lt;h2 id="conclusion-observability-is-developer-power"&gt;Conclusion: Observability Is Developer Power&lt;/h2&gt;
&lt;p&gt;Monitoring tells you when something is broken. Observability tells you how it broke, where it started, and what to change next. When logs, metrics, and traces work as three lenses on the same system—connected by correlation IDs and standardized by OpenTelemetry—you gain the ability to ask arbitrary questions about production, not just respond to pre-written alerts.&lt;/p&gt;
&lt;p&gt;If you can’t explain why your P99 spiked last Tuesday, you don’t need “more dashboards.” You need better instrumentation and correlated signals. That’s a developer competency—so build it, own it, and make it part of how you ship.&lt;/p&gt;</content></item><item><title>Why Every Team Should Have a Developer Platform</title><link>https://decastro.work/blog/every-team-should-have-developer-platform/</link><pubDate>Sun, 08 May 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/every-team-should-have-developer-platform/</guid><description>&lt;p&gt;Your engineers don’t actually spend their time “building software.” They spend their time coordinating—waiting for environments, chasing failing CI jobs, guessing which deployment steps belong to which service, and rediscovering tribal knowledge that lives only in Slack threads. A developer platform turns that chaos into a reliable workflow. Not because it’s fashionable, but because it’s the clearest lever you have to move from 1x delivery to 10x.&lt;/p&gt;
&lt;h2 id="the-real-bottleneck-isnt-talentits-friction"&gt;The real bottleneck isn’t talent—it’s friction&lt;/h2&gt;
&lt;p&gt;In high-performing orgs, the difference isn’t that developers are smarter. It’s that the path from idea to production is short, predictable, and repeatable.&lt;/p&gt;</description><content>&lt;p&gt;Your engineers don’t actually spend their time “building software.” They spend their time coordinating—waiting for environments, chasing failing CI jobs, guessing which deployment steps belong to which service, and rediscovering tribal knowledge that lives only in Slack threads. A developer platform turns that chaos into a reliable workflow. Not because it’s fashionable, but because it’s the clearest lever you have to move from 1x delivery to 10x.&lt;/p&gt;
&lt;h2 id="the-real-bottleneck-isnt-talentits-friction"&gt;The real bottleneck isn’t talent—it’s friction&lt;/h2&gt;
&lt;p&gt;In high-performing orgs, the difference isn’t that developers are smarter. It’s that the path from idea to production is short, predictable, and repeatable.&lt;/p&gt;
&lt;p&gt;Think about the typical “day in the life” of a modern engineering team:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A developer writes code, but can’t run it locally the same way it will run in production.&lt;/li&gt;
&lt;li&gt;CI runs, but failing jobs are hard to interpret or fix without paging someone from infrastructure.&lt;/li&gt;
&lt;li&gt;Environments are scarce, manually provisioned, and often outdated.&lt;/li&gt;
&lt;li&gt;Deployments require a checklist, a credential bundle, and a careful sequence of commands that only some team members remember.&lt;/li&gt;
&lt;li&gt;Rollbacks involve panic and coordination, because the process varies by service.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t just inconvenience. It’s compounding delay. Every hour of friction shifts engineering effort away from product work and toward operational babysitting. Over time, velocity drops, confidence erodes, and delivery becomes a series of controlled burns rather than a pipeline.&lt;/p&gt;
&lt;p&gt;A developer platform attacks the problem at the source: it removes repeatable toil and turns operational complexity into paved roads.&lt;/p&gt;
&lt;h2 id="what-an-internal-developer-platform-actually-includes"&gt;What an internal developer platform actually includes&lt;/h2&gt;
&lt;p&gt;“Developer platform” can sound like an abstract ambition. In practice, it’s a set of pragmatic capabilities that make engineering safer and faster:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CI/CD standards and automation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Opinionated pipelines (or templates) that run tests, build artifacts, scan for issues, and publish deployable outputs consistently.&lt;/li&gt;
&lt;li&gt;Clear failure messages and fast feedback loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Environment provisioning&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automated creation of dev/staging/test environments with sensible defaults.&lt;/li&gt;
&lt;li&gt;Clean teardown to prevent environment sprawl.&lt;/li&gt;
&lt;li&gt;Ideally, preview environments for branches so validation happens before merges.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Service catalog and templates&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A registry of services, ownership, supported runtimes, and integration patterns.&lt;/li&gt;
&lt;li&gt;Templates that generate new services or scaffolding with correct conventions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deployment abstractions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A consistent deployment interface (not a different ritual per repo).&lt;/li&gt;
&lt;li&gt;Rollouts, rollbacks, and release tracking that don’t depend on who happens to be online.&lt;/li&gt;
&lt;li&gt;Guardrails—like required health checks and safe concurrency settings—baked into the workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Observability and operational hooks&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Standard dashboards and alerting for new services.&lt;/li&gt;
&lt;li&gt;Logs/metrics/traces wired by default.&lt;/li&gt;
&lt;li&gt;Links from the developer workflow to the operational evidence they’ll need.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re wondering whether this is “too much,” ask a simpler question: do your engineers repeatedly do the same operational steps for each service? If yes, that’s exactly where a platform pays off.&lt;/p&gt;
&lt;h2 id="the-moment-you-feel-the-difference-day-one-deployment"&gt;The moment you feel the difference: day-one deployment&lt;/h2&gt;
&lt;p&gt;The most persuasive argument for a developer platform is not a theoretical model—it’s the lived experience of onboarding.&lt;/p&gt;
&lt;p&gt;Here’s the common reality today: a new hire can deploy on day one only if they already know the org’s operational rituals. Otherwise, they spend their first weeks waiting for accounts, learning undocumented conventions, and coordinating access to environments.&lt;/p&gt;
&lt;p&gt;With a developer platform, the promise changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The onboarding flow includes a service template and a working pipeline.&lt;/li&gt;
&lt;li&gt;Environments are provisioned automatically using a documented request or self-service button.&lt;/li&gt;
&lt;li&gt;The deployment path is consistent, so a new team member isn’t learning a new deployment language for every repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the “10x vs 1x” framing holds up. You don’t need every engineer to be heroic. You need every engineer—especially new ones—to be unblocked quickly.&lt;/p&gt;
&lt;p&gt;Backstage by Spotify became popular as a starting point for a reason: it’s a practical framework for building developer portals and service catalogs. You can adopt it as a foundation, then incrementally add integrations that match how your org works. The goal isn’t to install a tool. It’s to standardize the workflow.&lt;/p&gt;
&lt;h2 id="concrete-examples-of-platform-features-that-cut-real-work"&gt;Concrete examples of platform features that cut real work&lt;/h2&gt;
&lt;p&gt;Let’s make this tangible. What do these capabilities look like in day-to-day engineering?&lt;/p&gt;
&lt;h3 id="example-1-branch-preview-environments-that-actually-help"&gt;Example 1: Branch preview environments that actually help&lt;/h3&gt;
&lt;p&gt;Instead of “staging is shared, and it’s always broken,” your pipeline spins up a short-lived preview environment per pull request. Developers can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run integration tests against a real deployment,&lt;/li&gt;
&lt;li&gt;share a stable URL with product and QA,&lt;/li&gt;
&lt;li&gt;validate configuration changes early.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: start with one runtime (say, Node or Python), one deployment target (one cluster), and one naming convention. The platform should feel “boringly reliable” before you expand.&lt;/p&gt;
&lt;h3 id="example-2-a-deployment-command-that-never-surprises-you"&gt;Example 2: A deployment command that never surprises you&lt;/h3&gt;
&lt;p&gt;Imagine a single interface like &lt;code&gt;deploy &amp;lt;service&amp;gt; &amp;lt;env&amp;gt;&lt;/code&gt;. Under the hood, it uses the platform’s deployment controller, enforces required checks, and records release metadata. A developer shouldn’t need to remember:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which CI artifact to promote,&lt;/li&gt;
&lt;li&gt;which secrets to attach,&lt;/li&gt;
&lt;li&gt;what manual step is required for blue/green.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: standardize around artifact promotion and environment config management. Even partial standardization reduces cognitive load.&lt;/p&gt;
&lt;h3 id="example-3-service-catalog-with-ownership-and-integration-info"&gt;Example 3: Service catalog with ownership and integration info&lt;/h3&gt;
&lt;p&gt;A service is more than a repository. The platform’s catalog includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;owners (and on-call escalation paths),&lt;/li&gt;
&lt;li&gt;runtime and version constraints,&lt;/li&gt;
&lt;li&gt;downstream dependencies,&lt;/li&gt;
&lt;li&gt;standard dashboards.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When something breaks, developers don’t start with “where does this run?” They start with “here’s where it runs and who owns it.”&lt;/p&gt;
&lt;p&gt;Practical advice: treat the catalog as living documentation. Make updates required in PR workflows when service metadata changes.&lt;/p&gt;
&lt;h3 id="example-4-built-in-quality-gates-in-cicd"&gt;Example 4: Built-in quality gates in CI/CD&lt;/h3&gt;
&lt;p&gt;If every pipeline is slightly different, teams learn to treat CI as a roulette wheel. A platform enforces consistent gates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unit and integration tests,&lt;/li&gt;
&lt;li&gt;dependency scanning,&lt;/li&gt;
&lt;li&gt;security checks,&lt;/li&gt;
&lt;li&gt;artifact signing (if you use it),&lt;/li&gt;
&lt;li&gt;deterministic build outputs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice: don’t boil the ocean. Define a minimum standard pipeline that every service must adopt, then progressively add checks based on risk.&lt;/p&gt;
&lt;h2 id="the-operational-truth-platforms-turn-expertise-into-leverage"&gt;The operational truth: platforms turn expertise into leverage&lt;/h2&gt;
&lt;p&gt;A common pushback is: “We already have infrastructure experts. Why build a platform?”&lt;/p&gt;
&lt;p&gt;Because the experts shouldn’t have to be the bottleneck.&lt;/p&gt;
&lt;p&gt;In many orgs, operations knowledge is stored in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tribal memory,&lt;/li&gt;
&lt;li&gt;scattered runbooks,&lt;/li&gt;
&lt;li&gt;one-off scripts,&lt;/li&gt;
&lt;li&gt;personal expertise.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A platform codifies that knowledge into tooling and interfaces. It doesn’t eliminate ops; it changes ops from reactive “helpdesk mode” to proactive “systems design mode.”&lt;/p&gt;
&lt;p&gt;The team that owns the platform should be measured by outcomes like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reduced deployment lead time,&lt;/li&gt;
&lt;li&gt;fewer environment-related incidents,&lt;/li&gt;
&lt;li&gt;faster onboarding,&lt;/li&gt;
&lt;li&gt;fewer CI escalations,&lt;/li&gt;
&lt;li&gt;higher deployment frequency without increased risk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those metrics only matter if engineers feel the improvement directly. If the platform makes it harder, not easier, developers will route around it—and you’ll be back to Slack archaeology.&lt;/p&gt;
&lt;h2 id="how-to-build-a-platform-without-stalling-the-business"&gt;How to build a platform without stalling the business&lt;/h2&gt;
&lt;p&gt;A platform program can fail for one reason: it becomes a multi-quarter replatforming project with no immediate value. The winning approach is incremental and tightly scoped.&lt;/p&gt;
&lt;p&gt;Start with one workflow that hurts today—then automate it end-to-end:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Pick a single pain point with clear impact:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;environment provisioning,&lt;/li&gt;
&lt;li&gt;deployment steps,&lt;/li&gt;
&lt;li&gt;CI reliability,&lt;/li&gt;
&lt;li&gt;service template creation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Deliver a thin “happy path”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;new service scaffold + working pipeline,&lt;/li&gt;
&lt;li&gt;deploy to dev automatically,&lt;/li&gt;
&lt;li&gt;logs/metrics wired by default.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add guardrails:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;consistent health checks,&lt;/li&gt;
&lt;li&gt;standardized rollout strategy,&lt;/li&gt;
&lt;li&gt;permissions and secret handling.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Expand coverage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more runtimes,&lt;/li&gt;
&lt;li&gt;more environments,&lt;/li&gt;
&lt;li&gt;richer catalog metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you’re using Backstage, treat it as the interface layer: a place to discover services, provision environments, and trigger workflows. Then wire in the actual automation behind the scenes. The portal is not the platform—the workflows are.&lt;/p&gt;
&lt;h2 id="conclusion-speed-is-a-system-not-a-personality"&gt;Conclusion: Speed is a system, not a personality&lt;/h2&gt;
&lt;p&gt;Fast engineering isn’t an accident of hiring. It’s an outcome of how much friction exists between code and production. Internal developer platforms eliminate the repetitive glue work—CI/CD orchestration, environment provisioning, service discovery, and deployment complexity—so engineers can spend their time shipping product, not negotiating processes.&lt;/p&gt;
&lt;p&gt;Build the platform one valuable workflow at a time. The payoff arrives quickly: the first new hire who deploys to production on day one, and the last time you have to explain to someone how to “do it the old way.”&lt;/p&gt;</content></item><item><title>The Unreasonable Effectiveness of SQLite</title><link>https://decastro.work/blog/unreasonable-effectiveness-of-sqlite/</link><pubDate>Mon, 02 May 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/unreasonable-effectiveness-of-sqlite/</guid><description>&lt;p&gt;SQLite doesn’t feel like it should be this good. It’s small, it’s file-based, and it doesn’t come with a deployment ceremony worthy of enterprise budgets. And yet—quietly, everywhere—it runs the stuff that actually matters: phones, browsers, embedded controllers, and the offline-first apps people rely on. The punchline is simple: a lot of backend developers dismiss SQLite as a “toy,” and that dismissal is mostly nostalgia for client-server complexity.&lt;/p&gt;
&lt;h2 id="sqlite-is-everywhereand-not-by-accident"&gt;SQLite is everywhere—and not by accident&lt;/h2&gt;
&lt;p&gt;“Toy” is the wrong word for software that shows up by default across platforms you didn’t choose. SQLite is built into iOS and Android through common libraries, ships inside countless apps, and powers embedded systems where running a separate database server would be an operational joke. Even in more “modern” stacks, SQLite keeps showing up in the cracks: caches, local state, background sync buffers, and analytics scratchpads.&lt;/p&gt;</description><content>&lt;p&gt;SQLite doesn’t feel like it should be this good. It’s small, it’s file-based, and it doesn’t come with a deployment ceremony worthy of enterprise budgets. And yet—quietly, everywhere—it runs the stuff that actually matters: phones, browsers, embedded controllers, and the offline-first apps people rely on. The punchline is simple: a lot of backend developers dismiss SQLite as a “toy,” and that dismissal is mostly nostalgia for client-server complexity.&lt;/p&gt;
&lt;h2 id="sqlite-is-everywhereand-not-by-accident"&gt;SQLite is everywhere—and not by accident&lt;/h2&gt;
&lt;p&gt;“Toy” is the wrong word for software that shows up by default across platforms you didn’t choose. SQLite is built into iOS and Android through common libraries, ships inside countless apps, and powers embedded systems where running a separate database server would be an operational joke. Even in more “modern” stacks, SQLite keeps showing up in the cracks: caches, local state, background sync buffers, and analytics scratchpads.&lt;/p&gt;
&lt;p&gt;Here’s the deeper reason: SQLite collapses the classic database deployment surface area into something you can reason about. Instead of “database server + network + authentication + migrations tooling + connection pooling + ops,” you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a single file (or a set of files),&lt;/li&gt;
&lt;li&gt;a small set of modes and pragmas,&lt;/li&gt;
&lt;li&gt;and a predictable lifecycle: create DB → write transactions → read results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That predictability matters when the product is small, the team is lean, and the cost of being wrong is measured in developer hours, not cloud bills.&lt;/p&gt;
&lt;h2 id="wal-mode-the-concurrency-story-backend-devs-usually-miss"&gt;WAL mode: the concurrency story backend devs usually miss&lt;/h2&gt;
&lt;p&gt;The most common reason developers underestimate SQLite is that they remember old performance myths or default settings they never touched. SQLite has a concurrency mode that changes the entire conversation: &lt;strong&gt;WAL (Write-Ahead Logging)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;WAL mode&lt;/strong&gt;, SQLite separates reads from writes in a way that’s especially friendly to typical app workloads: many concurrent readers, one writer at a time (or at least “not a writer stampede”). Practically, this means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Readers don’t block behind writers nearly as often.&lt;/li&gt;
&lt;li&gt;You get better performance under mixed read/write traffic.&lt;/li&gt;
&lt;li&gt;Long read transactions are safer because they work against a consistent snapshot.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve ever built an app where the database is “mostly reads” (e.g., serving API responses, resolving user sessions, fetching cached entities), WAL is exactly what you want.&lt;/p&gt;
&lt;p&gt;A concrete example: imagine a service that stores user profiles and feature flags. Writes happen when users update settings; reads happen constantly when requests come in. With SQLite in WAL mode, that pattern maps cleanly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One writer transaction applies updates.&lt;/li&gt;
&lt;li&gt;Many readers fetch profile data concurrently.&lt;/li&gt;
&lt;li&gt;Clients don’t wait on the writer except when they truly must.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To enable it, you’ll typically set the database pragma once in your app startup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PRAGMA journal_mode = WAL;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re using a library (ORMs or drivers), confirm it doesn’t override this—and ensure your connection strategy doesn’t fight SQLite with overly aggressive connection churn.&lt;/p&gt;
&lt;h2 id="replication-without-heroics-litestream-and-the-real-serverless-database-story"&gt;Replication without heroics: Litestream and the real “serverless database” story&lt;/h2&gt;
&lt;p&gt;If your mental model is “SQLite means no reliability,” you’re thinking about local files as disposable. That’s the wrong framing. You don’t need to abandon SQLite; you need a replication layer that matches its strengths.&lt;/p&gt;
&lt;p&gt;Enter &lt;strong&gt;Litestream&lt;/strong&gt;: an approach to replicate SQLite databases continuously so you can treat a local SQLite file as a durable, recoverable data source. The win is that you keep the operational simplicity of SQLite while gaining the safety net you expect from a server-based database.&lt;/p&gt;
&lt;p&gt;The practical pattern looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your app writes to the local SQLite database (fast, simple, no network round trips).&lt;/li&gt;
&lt;li&gt;Litestream continuously ships changes to a remote storage target.&lt;/li&gt;
&lt;li&gt;In a failure scenario, you restore from the replicated stream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is particularly compelling for systems where you already like the “single writer” or “local-first” design. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A mobile app that syncs to a backend: store locally, replicate server-side from the device or from a companion process.&lt;/li&gt;
&lt;li&gt;A web service that prioritizes low latency: write locally to SQLite (on the same host), replicate for durability.&lt;/li&gt;
&lt;li&gt;Edge deployments: run a local DB where network jitter exists, replicate changes upstream.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important point isn’t that SQLite can be replicated; it’s that the replication story can be practical. You’re not asking SQLite to behave like a distributed database. You’re using it the way it’s meant to be used: transactional engine with a clean replication pipeline.&lt;/p&gt;
&lt;h2 id="when-sqlite-beats-postgresql-and-why-at-a-fraction-of-the-cost-is-believable"&gt;When SQLite beats PostgreSQL (and why “at a fraction of the cost” is believable)&lt;/h2&gt;
&lt;p&gt;Let’s kill a recurring misconception: SQLite is not “always slower than PostgreSQL.” It’s different. And for a surprising number of real workloads—especially &lt;strong&gt;read-heavy&lt;/strong&gt; workloads with a &lt;strong&gt;single-writer&lt;/strong&gt; pattern—SQLite can outperform PostgreSQL while requiring far less operational overhead.&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fewer moving parts: no client-server protocol overhead for every request.&lt;/li&gt;
&lt;li&gt;Local I/O: reads and writes happen on the same machine (or at least close to it).&lt;/li&gt;
&lt;li&gt;Lightweight connections: you don’t fight pool configuration or connection storms.&lt;/li&gt;
&lt;li&gt;Simpler deployment: no “database is down” because of networking, auth misconfigurations, or cluster topology issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suppose you’re building a backend for a small SaaS: think dashboards, content delivery, and API endpoints. Writes might happen through background jobs, scheduled syncs, or user actions that are relatively infrequent compared to read traffic. In that scenario, PostgreSQL’s strength—high-concurrency multi-writer workloads and distributed operational features—may be overkill.&lt;/p&gt;
&lt;p&gt;SQLite shines when the bottleneck is not “global write throughput under contention,” but “how quickly can we serve requests with clean transactional semantics.”&lt;/p&gt;
&lt;p&gt;A rule of thumb I’ve adopted: if your system design doesn’t require the database to coordinate dozens of concurrent writers across many networked instances, start with SQLite. If you later discover a write contention issue, you can revisit the architecture. But you’ll have learned something valuable before you pay the operational tax.&lt;/p&gt;
&lt;h2 id="practical-guidance-how-to-use-sqlite-like-you-mean-it"&gt;Practical guidance: how to use SQLite like you mean it&lt;/h2&gt;
&lt;p&gt;If you’re going to take SQLite seriously, treat it like production software, not a demo.&lt;/p&gt;
&lt;h3 id="use-wal-mode-and-plan-for-cleanup"&gt;Use WAL mode and plan for cleanup&lt;/h3&gt;
&lt;p&gt;WAL mode changes the concurrency profile in your favor, but it comes with operational considerations (like checkpointing behavior). Don’t ignore it. Make sure your application or maintenance process runs appropriate checkpointing so WAL files don’t grow without bound.&lt;/p&gt;
&lt;h3 id="design-for-the-single-writer-reality"&gt;Design for the single-writer reality&lt;/h3&gt;
&lt;p&gt;Many systems naturally fit SQLite’s sweet spot: one writer transaction stream, lots of readers. If your design spawns multiple writers aggressively, you’ll feel it. Instead:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Batch writes when possible.&lt;/li&gt;
&lt;li&gt;Route all writes through a single queue/worker.&lt;/li&gt;
&lt;li&gt;Keep write transactions small and fast.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="pick-the-right-locking-assumptions"&gt;Pick the right locking assumptions&lt;/h3&gt;
&lt;p&gt;SQLite locks at the database file level. That means your app should avoid patterns that cause unnecessary writes during request handling. A common anti-pattern is “log to the DB synchronously on every request.” If you want traceability, queue the logs and persist them in a background batch.&lt;/p&gt;
&lt;h3 id="tune-your-connection-strategy"&gt;Tune your connection strategy&lt;/h3&gt;
&lt;p&gt;SQLite supports concurrency, but the way you manage connections still matters. Prefer long-lived connections when the runtime is stable, and be careful with high-frequency connect/disconnect loops.&lt;/p&gt;
&lt;h3 id="validate-with-the-workload-you-actually-have"&gt;Validate with the workload you actually have&lt;/h3&gt;
&lt;p&gt;Before declaring victory or failure, load test your real query mix. SQLite performance is sensitive to schema design, indexes, and transaction sizing. If your queries are unindexed or your transactions are chunky, you’ll hurt any database—but SQLite tends to reveal these issues sooner because the system is so direct.&lt;/p&gt;
&lt;h2 id="the-no-database-server-mindset-that-unlocks-shipping"&gt;The “no database server” mindset that unlocks shipping&lt;/h2&gt;
&lt;p&gt;The most compelling benefit of SQLite isn’t raw performance—it’s the ability to ship. Backend projects stall for predictable reasons: database migration pipelines break, production connection settings drift, replication lags, and someone inevitably asks why the cluster is misbehaving “only in staging.”&lt;/p&gt;
&lt;p&gt;With SQLite, you remove an entire class of problems. Your database becomes an artifact of your application, not a separately administered service you have to babysit.&lt;/p&gt;
&lt;p&gt;And that doesn’t mean you can’t be serious about durability. Replication tools like Litestream let you keep the simplicity while moving from “local file” to “recoverable system.” WAL mode gives you concurrency that fits real-world read patterns. At that point, the “toy database” label starts to look like a bias, not a technical assessment.&lt;/p&gt;
&lt;p&gt;If you’re building your next side project, start here. If you’re building your next startup, seriously consider it. Most teams don’t need more infrastructure—they need fewer failure modes and faster iteration loops.&lt;/p&gt;
&lt;h2 id="conclusion-sqlite-earns-its-place-not-your-nostalgia"&gt;Conclusion: SQLite earns its place, not your nostalgia&lt;/h2&gt;
&lt;p&gt;SQLite is effective in the unreasonable way: it’s small, boring-looking, and operationally elegant, yet it handles real workloads across phones, browsers, and embedded devices. With WAL mode for concurrency, and replication options like Litestream for durability, it becomes a practical backbone for read-heavy systems with manageable write patterns. Treat it as a first-class backend datastore—not a toy—and you may find you can build faster, deploy safer, and spend less time fighting infrastructure.&lt;/p&gt;</content></item><item><title>Web3 Is Mostly Nonsense, and That's Okay</title><link>https://decastro.work/blog/web3-mostly-nonsense-thats-okay/</link><pubDate>Wed, 20 Apr 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/web3-mostly-nonsense-thats-okay/</guid><description>&lt;p&gt;“Web3” has become a branding incantation: sprinkle “decentralized” on your slide deck, bolt on a token, and somehow everyone forgets to ask whether the product needs a blockchain. The result is a grifter ecosystem so loud it drowns out the genuinely interesting work. The twist is that you don’t need to hate Web3 to see through it—you need to be able to separate hype from primitives.&lt;/p&gt;
&lt;p&gt;Here’s my take: most Web3 projects are unnecessary, and many are actively harmful. But the underlying cryptographic ideas are real, useful, and worth learning. If you focus on the primitives—content addressing, verifiable computation, and programmable money—you can ignore the token casino without throwing away the baby with the bathwater.&lt;/p&gt;</description><content>&lt;p&gt;“Web3” has become a branding incantation: sprinkle “decentralized” on your slide deck, bolt on a token, and somehow everyone forgets to ask whether the product needs a blockchain. The result is a grifter ecosystem so loud it drowns out the genuinely interesting work. The twist is that you don’t need to hate Web3 to see through it—you need to be able to separate hype from primitives.&lt;/p&gt;
&lt;p&gt;Here’s my take: most Web3 projects are unnecessary, and many are actively harmful. But the underlying cryptographic ideas are real, useful, and worth learning. If you focus on the primitives—content addressing, verifiable computation, and programmable money—you can ignore the token casino without throwing away the baby with the bathwater.&lt;/p&gt;
&lt;h2 id="why-blockchain-is-often-just-a-ui-feature"&gt;Why “Blockchain” Is Often Just a UI Feature&lt;/h2&gt;
&lt;p&gt;Let’s start with the uncomfortable truth: a lot of Web3 is only “decentralized” in the same way a spreadsheet is “database-backed.” In practice, many systems behave like centralized services with extra steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Users trust a front-end server to validate data.&lt;/li&gt;
&lt;li&gt;The “smart contract” is a billing wrapper around an off-chain database.&lt;/li&gt;
&lt;li&gt;The token doesn’t improve anything technically—it just finances the growth stage.&lt;/li&gt;
&lt;li&gt;Governance is a forum, not a decision mechanism.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simple litmus test: if your application can be implemented with normal HTTPS, a relational database, and a queue, then adding a blockchain doesn’t magically make it better. It usually makes it slower, harder to update, and more expensive to operate.&lt;/p&gt;
&lt;p&gt;Example: consider a SaaS dashboard that shows user analytics. If the analytics are produced off-chain and merely recorded on-chain for “auditability,” you’ve added cost without improving correctness. If you can’t explain &lt;em&gt;why&lt;/em&gt; you need an append-only, globally replicated ledger, you probably don’t need one.&lt;/p&gt;
&lt;p&gt;My rule of thumb is brutally practical: &lt;strong&gt;use a blockchain when you need a shared, tamper-evident history among parties that don’t fully trust each other.&lt;/strong&gt; Everything else is usually a marketing problem.&lt;/p&gt;
&lt;h2 id="the-interesting-part-content-addressed-storage"&gt;The Interesting Part: Content-Addressed Storage&lt;/h2&gt;
&lt;p&gt;One of the most useful primitives coming out of the broader ecosystem is content-addressing: the idea that data is identified by a cryptographic hash of its contents, not by a mutable filename. This is conceptually simple, technically powerful, and—crucially—doesn’t require a token.&lt;/p&gt;
&lt;p&gt;When you content-address data, you get properties that centralized systems struggle to guarantee:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integrity:&lt;/strong&gt; if the content changes, the address changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Immutability by reference:&lt;/strong&gt; you can’t silently swap the underlying bytes while keeping the same address.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deduplication:&lt;/strong&gt; identical content naturally maps to the same identifier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think about what this enables in real systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Verifiable document publishing:&lt;/strong&gt; publish a document and reference its hash. Anyone can later verify they’re seeing the exact version you claimed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducible artifacts:&lt;/strong&gt; build outputs (like model weights or compiled binaries) can be pinned by hash so deployments are auditable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portable caching:&lt;/strong&gt; distributed nodes can fetch content by address without negotiating “which version do you mean?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve used Git, you already understand the mental model. Git commit hashes are content-addressed fingerprints of trees and files. Web3 storage systems essentially extend that idea to the wider world, with networks designed to serve data by hash.&lt;/p&gt;
&lt;p&gt;Practical advice: if you’re building something that involves long-lived artifacts—images, proofs, bundles, datasets—consider &lt;strong&gt;hash-first design&lt;/strong&gt;. Store content where you want, but treat the hash as the source of truth. The blockchain, if used at all, can reference these hashes—not the other way around.&lt;/p&gt;
&lt;h2 id="verifiable-computation-when-trust-me-isnt-good-enough"&gt;Verifiable Computation: When “Trust Me” Isn’t Good Enough&lt;/h2&gt;
&lt;p&gt;Another genuinely interesting primitive is verifiable computation: the ability for one party to produce a computational result &lt;em&gt;and&lt;/em&gt; a proof that the result is correct, without requiring the verifier to redo the entire computation.&lt;/p&gt;
&lt;p&gt;This category includes a range of techniques—proof systems, zero-knowledge proofs, succinct verification—that aim to make verification fast while keeping the proof itself compact.&lt;/p&gt;
&lt;p&gt;Why do people care? Because “trust me” is a weakness you can’t afford in adversarial environments.&lt;/p&gt;
&lt;p&gt;Concrete example: Suppose you run a large off-chain computation—say, processing trades, generating game state, or computing eligibility for a reward. A centralized operator could publish the result. But if users don’t trust the operator (or you don’t fully control what users trust), they need assurance that the result is computed correctly from the inputs.&lt;/p&gt;
&lt;p&gt;With verifiable computation, the workflow looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inputs are committed (often via hashes).&lt;/li&gt;
&lt;li&gt;The prover performs computation off-chain.&lt;/li&gt;
&lt;li&gt;A proof is generated that the computation followed the rules.&lt;/li&gt;
&lt;li&gt;Verifiers check the proof quickly, without re-running the whole process.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Is this trivial? No. It’s engineering-heavy, and the tooling ecosystem is still maturing. But the &lt;em&gt;direction&lt;/em&gt; is right: rather than replicating everything on-chain, you prove correctness of what happened somewhere else.&lt;/p&gt;
&lt;p&gt;Practical advice for builders: start by asking where verification matters. If your computation directly affects money, permissions, or scarce resources, that’s where proofs can replace brittle trust assumptions. If you can tolerate trusting one party, you probably don’t need proofs yet—build the app first, then harden only the parts that truly require verification.&lt;/p&gt;
&lt;p&gt;And yes, this is one area where blockchain can be a useful substrate: smart contracts can act as verifiers, recording proofs and mediating dispute resolution. But the proof system is the primitive, not the chain.&lt;/p&gt;
&lt;h2 id="programmable-money-what-smart-contracts-actually-do"&gt;Programmable Money: What Smart Contracts Actually Do&lt;/h2&gt;
&lt;p&gt;“Programmable money” is often used as a synonym for “token.” That’s marketing. Smart contracts are better understood as &lt;strong&gt;deterministic state machines&lt;/strong&gt;: given inputs, they transition state according to code rules, with transparent execution and a verifiable history.&lt;/p&gt;
&lt;p&gt;When programmable money is valuable, it’s usually because you need these properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Atomicity:&lt;/strong&gt; multiple actions either happen together or not at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enforceable rules:&lt;/strong&gt; parties can rely on code-based constraints rather than bilateral agreements.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared settlement:&lt;/strong&gt; the state lives where multiple parties can consult it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Classic examples include escrow, royalties, and automated market mechanisms. But the more interesting lesson is this: the contract isn’t magic; it’s a coordination mechanism for parties that don’t want to negotiate everything from scratch.&lt;/p&gt;
&lt;p&gt;Here’s the concrete gotcha: smart contracts are only as good as their interfaces. If your contract depends on off-chain data supplied by a trusted party, you’ve introduced a central point of failure. “On-chain” doesn’t automatically mean “trustless.” It means “verifiable where the contract can verify.”&lt;/p&gt;
&lt;p&gt;Practical advice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer designs that are &lt;strong&gt;self-contained&lt;/strong&gt; or that can validate inputs on-chain.&lt;/li&gt;
&lt;li&gt;Be suspicious of projects that say “we use decentralization” while quietly relying on centralized operators for the critical data path.&lt;/li&gt;
&lt;li&gt;If you must use off-chain data, think hard about incentives, replay protection, and how disputes are resolved.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-token-casino-problem-and-how-to-avoid-it"&gt;The Token Casino Problem (and How to Avoid It)&lt;/h2&gt;
&lt;p&gt;Now for the part almost everyone gets wrong: tokens. Tokens are not inherently evil, but they are frequently used as a substitute for product-market fit.&lt;/p&gt;
&lt;p&gt;In a healthy system, a token is typically aligned with a real technical purpose—governance, access control, staking for security, fee markets, or incentives that are actually necessary for the mechanism to work.&lt;/p&gt;
&lt;p&gt;In the grifter ecosystem, tokens are a fundraising layer. They fund marketing. They motivate influencers. They provide a distraction from the fact that the app either doesn’t require decentralization or fails at the decentralization part.&lt;/p&gt;
&lt;p&gt;How to detect token-first nonsense quickly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The token can be removed without changing the user experience.&lt;/strong&gt; If yes, it’s probably not integral.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “decentralization” is cosmetic.&lt;/strong&gt; If everything important is controlled by one company, the blockchain is a logging system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upgrades are centralized.&lt;/strong&gt; If governance is hand-wavy and emergencies are single-signer, users are trusting people, not code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The roadmap is vibes.&lt;/strong&gt; If there’s no clear technical path to the stated capabilities, you’re watching hype.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Practical advice for teams and investors: separate your analysis into two tracks.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What cryptographic or architectural primitive does this system use?&lt;/li&gt;
&lt;li&gt;What is the user value &lt;em&gt;without&lt;/em&gt; the token?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can’t answer (1) clearly and (2 sounds like a normal product, the token is likely just a financing mechanism.&lt;/p&gt;
&lt;h2 id="learning-the-right-stuff-without-getting-burned"&gt;Learning the Right Stuff Without Getting Burned&lt;/h2&gt;
&lt;p&gt;If you want to build with integrity in this space, you don’t need to memorize every new chain or token standard. You need to learn the underlying concepts that survive every hype cycle.&lt;/p&gt;
&lt;p&gt;A smart developer path looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cryptographic fundamentals:&lt;/strong&gt; hashing, signatures, commitments, and threat models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content addressing:&lt;/strong&gt; how hashes act as references; how immutability-by-reference changes system design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proof systems at a high level:&lt;/strong&gt; what it means to prove computation, what verification costs look like, and where proofs can replace trust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smart contract basics as state machines:&lt;/strong&gt; determinism, gas costs, failure modes, and secure interfaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then keep one cultural discipline: &lt;strong&gt;ship small, verifiable slices&lt;/strong&gt;. If your system can’t be inspected, tested, or audited meaningfully, it’s not mature enough to justify the complexity it adds.&lt;/p&gt;
&lt;p&gt;And treat the token ecosystem as optional background noise. You can learn from the primitives without joining the casino.&lt;/p&gt;
&lt;h2 id="conclusion-ignore-the-noise-build-the-primitives"&gt;Conclusion: Ignore the Noise, Build the Primitives&lt;/h2&gt;
&lt;p&gt;Web3 is mostly nonsense—but not because the technology is fake. It’s mostly nonsense because most builders are using a ledger as a replacement for engineering judgment and because tokens have become a reflexive layer.&lt;/p&gt;
&lt;p&gt;The good news is that the interesting ideas are real and practical: content-addressed storage for integrity and long-lived references, verifiable computation for correctness without re-running everything, and programmable state machines for enforceable coordination. Focus on those primitives, demand clear trust properties, and you’ll be positioned to build systems that deserve decentralization—rather than ones that just announce it.&lt;/p&gt;</content></item><item><title>Go Is the Language for People Who Want to Ship</title><link>https://decastro.work/blog/go-language-people-who-want-to-ship/</link><pubDate>Sat, 09 Apr 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/go-language-people-who-want-to-ship/</guid><description>&lt;p&gt;Every engineering team eventually hits the same wall: not “can we build it?” but “can we build it &lt;em&gt;again&lt;/em&gt;—and again—without dragging the entire org through a rewrite.” If that’s your problem, Go is the rare language that optimizes for shipping over showing off. Yes, it’s boring. Yes, it’s simpler than the cool kids. And yes, that’s the point.&lt;/p&gt;
&lt;h2 id="boring-is-the-operating-system-not-a-bug"&gt;Boring Is the Operating System, Not a Bug&lt;/h2&gt;
&lt;p&gt;Go gets criticized for being “too basic,” as if the only acceptable technology is the kind that demands a tutorial and a glossary. But the most successful teams don’t start with language fireworks—they start with reliable delivery.&lt;/p&gt;</description><content>&lt;p&gt;Every engineering team eventually hits the same wall: not “can we build it?” but “can we build it &lt;em&gt;again&lt;/em&gt;—and again—without dragging the entire org through a rewrite.” If that’s your problem, Go is the rare language that optimizes for shipping over showing off. Yes, it’s boring. Yes, it’s simpler than the cool kids. And yes, that’s the point.&lt;/p&gt;
&lt;h2 id="boring-is-the-operating-system-not-a-bug"&gt;Boring Is the Operating System, Not a Bug&lt;/h2&gt;
&lt;p&gt;Go gets criticized for being “too basic,” as if the only acceptable technology is the kind that demands a tutorial and a glossary. But the most successful teams don’t start with language fireworks—they start with reliable delivery.&lt;/p&gt;
&lt;p&gt;Go’s surface area is intentionally small. There’s one clear way to format and compile code. Concurrency has a mental model you can explain without a whiteboard dissertation. You don’t need a framework to do “hello web” or “hello API.” You don’t need five different libraries to add TLS, validate input, or make an HTTP call.&lt;/p&gt;
&lt;p&gt;A concrete example: consider a service that needs an HTTP endpoint and a background worker. In Go, you can often fit the core of the solution into a few files and run it immediately. The language nudges you toward composition rather than ceremony:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP handler functions are straightforward.&lt;/li&gt;
&lt;li&gt;Goroutines are the default tool for concurrency.&lt;/li&gt;
&lt;li&gt;Channels are available when you truly need coordination, not as a forced abstraction.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t make Go magical. It makes it predictable. Predictability is what lets teams move fast without inventing new “best practices” every sprint.&lt;/p&gt;
&lt;h2 id="deployability-the-single-binary-advantage"&gt;Deployability: The Single Binary Advantage&lt;/h2&gt;
&lt;p&gt;Ship speed isn’t just about coding speed—it’s about how frictionless deployment is. Go’s build model supports a workflow that many teams want and few languages make pleasant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compile from source.&lt;/li&gt;
&lt;li&gt;Produce a single binary.&lt;/li&gt;
&lt;li&gt;Deploy that binary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No runtime dependency chain to reason about. No “works on my machine” caused by a missing version of some interpreter, framework, or transitive dependency that quietly changed overnight. This matters most when your pipeline is busy and your team is tired.&lt;/p&gt;
&lt;p&gt;Imagine you run a dozen services, each with its own CI/CD job. In a “clever” ecosystem, you can spend real engineering time untangling dependency graphs, base images, and subtle runtime differences. In Go, you can usually focus on the service itself. Your CI builds something immutable; your deploys move that artifact into place.&lt;/p&gt;
&lt;p&gt;Is Go boring? Yes. Does it reduce operational uncertainty? Also yes—and that’s the kind of boring you should want.&lt;/p&gt;
&lt;h2 id="the-standard-library-coverage-without-dependencies"&gt;The Standard Library: Coverage Without Dependencies&lt;/h2&gt;
&lt;p&gt;One of the most practical reasons Go ships well is its standard library. It includes what teams commonly need: HTTP servers and clients, JSON encoding, file I/O, TLS helpers, and crypto primitives you can use without chasing half a dozen third-party packages.&lt;/p&gt;
&lt;p&gt;This is where “boring” becomes a strategic advantage. Every dependency you add introduces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A versioning decision.&lt;/li&gt;
&lt;li&gt;A security patch path.&lt;/li&gt;
&lt;li&gt;An upgrade tax.&lt;/li&gt;
&lt;li&gt;A future debugging session when something changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A strong team treats dependencies as costs to be justified, not trophies. Go’s standard library covers enough ground that many services can stay dependency-light.&lt;/p&gt;
&lt;p&gt;For example, you can build a small REST API with HTTP handlers and JSON without pulling in a routing framework. You can validate inputs with the language-level tools you already know. You can do authenticated requests, handle TLS, and manage errors in ways that are consistent across services.&lt;/p&gt;
&lt;p&gt;This consistency pays dividends when multiple teams share a platform. New engineers can learn one set of patterns and apply it everywhere.&lt;/p&gt;
&lt;h2 id="error-handling-verbose-predictable-and-easy-to-debug"&gt;Error Handling: Verbose, Predictable, and Easy to Debug&lt;/h2&gt;
&lt;p&gt;Go’s error handling is another lightning rod. Critics complain about verbosity; experienced Go developers appreciate the clarity.&lt;/p&gt;
&lt;p&gt;In Go, errors aren’t exceptions that explode up the stack. They’re values you explicitly check. That forces decisions to happen at the right place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;handle locally when you can&lt;/li&gt;
&lt;li&gt;wrap with context when you can’t&lt;/li&gt;
&lt;li&gt;return upward when it’s the only sane option&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s the mindset shift that makes this work: Go makes control flow visible. You can glance at a function and understand what can go wrong and what the function does in those cases.&lt;/p&gt;
&lt;p&gt;That visibility matters when your service fails in production at 2 a.m. A “clever” approach might hide the path to failure behind layers of magic. Go doesn’t. The code reads like it’s telling you exactly where it expects trouble—and what it will do when trouble arrives.&lt;/p&gt;
&lt;p&gt;You will write more lines. You’ll also spend less time guessing.&lt;/p&gt;
&lt;h2 id="generics-came-lateso-what"&gt;Generics Came Late—So What?&lt;/h2&gt;
&lt;p&gt;Go’s generics arrived later than some ecosystems. Fair criticism. But “late” can still be “right” if teams use the feature when it truly adds value rather than forcing it everywhere.&lt;/p&gt;
&lt;p&gt;Even without generics, Go’s approach to maintainable code has been consistent for years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;keep types simple&lt;/li&gt;
&lt;li&gt;model your domain with structs&lt;/li&gt;
&lt;li&gt;use interfaces where they clarify behavior&lt;/li&gt;
&lt;li&gt;write small functions that compose cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When generics finally arrived, they didn’t replace Go’s core strengths. They complemented them. And the practical lesson is: don’t let the presence of a new feature determine architecture. Let needs drive architecture.&lt;/p&gt;
&lt;p&gt;If your team wants to ship, you don’t adopt generics because they’re fashionable. You adopt them when they reduce duplication without obscuring meaning. If a generic abstraction makes the code harder to read, it’s not a win—it’s future work disguised as progress.&lt;/p&gt;
&lt;h2 id="concurrency-thats-understandable-under-pressure"&gt;Concurrency That’s Understandable Under Pressure&lt;/h2&gt;
&lt;p&gt;Go’s concurrency story is often described as a selling point, and it deserves credit. But “powerful” isn’t the most important word here—“understandable” is.&lt;/p&gt;
&lt;p&gt;Goroutines are lightweight, and channels are a clear abstraction for coordination. That combination can lead to systems that scale without becoming an unreadable maze.&lt;/p&gt;
&lt;p&gt;A practical example: suppose you need to call three external APIs to build a response. You can fan out requests concurrently and then gather results. In many languages, the pattern is either overly complex or encourages “clever” concurrency frameworks that are hard to reason about. In Go, the concurrency code stays close to the business logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;start goroutines for independent work&lt;/li&gt;
&lt;li&gt;collect results&lt;/li&gt;
&lt;li&gt;handle cancellation when the request ends&lt;/li&gt;
&lt;li&gt;avoid sharing mutable state without a plan&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better: because Go makes this style idiomatic, your team can learn it once and apply it everywhere. That’s a “boring” advantage again—shared understanding is a shipping multiplier.&lt;/p&gt;
&lt;h2 id="the-real-trade-off-fewer-features-more-throughput"&gt;The Real Trade-Off: Fewer Features, More Throughput&lt;/h2&gt;
&lt;p&gt;Let’s be honest about the trade-off. Go is not trying to be a playground for language maximalists. It gives you fewer features, fewer patterns, and fewer ways to be clever. The compiler is strict. The tooling is straightforward. The ecosystem is practical.&lt;/p&gt;
&lt;p&gt;That’s why teams choose Go when they want throughput:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fast compilation helps iteration&lt;/li&gt;
&lt;li&gt;single-binary deployment helps operations&lt;/li&gt;
&lt;li&gt;standard library coverage reduces dependency sprawl&lt;/li&gt;
&lt;li&gt;explicit error handling reduces mystery&lt;/li&gt;
&lt;li&gt;understandable concurrency reduces production time sinks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also means you’ll occasionally fight the language. Error handling can feel wordy. Some abstractions require discipline. You may miss more expressive type systems. But compared to the cost of delays—blocked releases, broken builds, insecure dependencies, unclear production behavior—those complaints often shrink fast.&lt;/p&gt;
&lt;p&gt;Boring technology delivered fast beats clever technology delivered never.&lt;/p&gt;
&lt;h2 id="conclusion-ship-first-engineering-deserves-boring"&gt;Conclusion: Ship-First Engineering Deserves Boring&lt;/h2&gt;
&lt;p&gt;Go earns its reputation because it makes the path from idea to running service short and repeatable. It doesn’t reward novelty; it rewards momentum. If your team’s bottleneck is delivery—whether you’re building APIs, internal tools, or production services—Go is a strong default.&lt;/p&gt;
&lt;p&gt;Choose it when you want code that any developer can read on day one and a workflow that keeps your releases boring in the best possible way.&lt;/p&gt;</content></item><item><title>CSS Grid Changed Layout Forever and Flexbox People Need to Accept It</title><link>https://decastro.work/blog/css-grid-changed-layout-forever/</link><pubDate>Tue, 05 Apr 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/css-grid-changed-layout-forever/</guid><description>&lt;p&gt;For years, Flexbox was the hero of modern CSS layout. It made layout feel human again. But the minute you start building real pages—dashboards, marketing grids, responsive galleries—Flexbox runs out of runway. CSS Grid didn’t “replace” Flexbox. It fixed the part Flexbox was never built to do well. If you’ve been nesting flex containers like they’re going out of style, it’s time to learn Grid properly.&lt;/p&gt;
&lt;h2 id="flexbox-incredible-for-one-dimension-and-exhausting-for-two"&gt;Flexbox: incredible for one dimension (and exhausting for two)&lt;/h2&gt;
&lt;p&gt;Flexbox shines when your layout is essentially one-dimensional: you’re arranging items in a row &lt;em&gt;or&lt;/em&gt; a column, and you care about alignment, ordering, and distribution along a single axis.&lt;/p&gt;</description><content>&lt;p&gt;For years, Flexbox was the hero of modern CSS layout. It made layout feel human again. But the minute you start building real pages—dashboards, marketing grids, responsive galleries—Flexbox runs out of runway. CSS Grid didn’t “replace” Flexbox. It fixed the part Flexbox was never built to do well. If you’ve been nesting flex containers like they’re going out of style, it’s time to learn Grid properly.&lt;/p&gt;
&lt;h2 id="flexbox-incredible-for-one-dimension-and-exhausting-for-two"&gt;Flexbox: incredible for one dimension (and exhausting for two)&lt;/h2&gt;
&lt;p&gt;Flexbox shines when your layout is essentially one-dimensional: you’re arranging items in a row &lt;em&gt;or&lt;/em&gt; a column, and you care about alignment, ordering, and distribution along a single axis.&lt;/p&gt;
&lt;p&gt;Classic flex use-cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A nav bar with evenly spaced links&lt;/li&gt;
&lt;li&gt;A toolbar where buttons wrap or scroll horizontally&lt;/li&gt;
&lt;li&gt;A “stack” of components where you want consistent spacing and simple reordering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But once your problem becomes two-dimensional—rows &lt;em&gt;and&lt;/em&gt; columns with responsive behavior—Flexbox starts turning into a philosophical argument with yourself. You’ll find yourself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating nested containers to simulate rows and columns&lt;/li&gt;
&lt;li&gt;Repeating media queries to adjust widths and spacing&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;flex-wrap&lt;/code&gt; and hoping it “just works” across breakpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point matters. &lt;code&gt;flex-wrap&lt;/code&gt; is not a grid system. It’s a wrapping behavior. When you need a predictable matrix, your code should reflect that.&lt;/p&gt;
&lt;h2 id="the-real-mindset-shift-stop-forcing-grids-into-flexbox"&gt;The real mindset shift: stop forcing grids into flexbox&lt;/h2&gt;
&lt;p&gt;Here’s the pattern I keep seeing in production code: developers learned Flexbox first, so they reach for it by default—then compensate for missing features by nesting more flex containers.&lt;/p&gt;
&lt;p&gt;A simplified example of what that looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;cardGrid&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;flex&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;flex-wrap&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;wrap&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;gap&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;cardRow&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;flex&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;gap&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;width&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then suddenly you need three rows at some widths, two rows at others, and different card sizes in different sections. The fix becomes “more wrappers” and “more breakpoint rules,” not a clearer layout declaration.&lt;/p&gt;
&lt;p&gt;Grid flips the relationship: you declare the layout you want—rows, columns, and placement—and let the browser do the math.&lt;/p&gt;
&lt;h2 id="css-grid-declare-the-matrix-not-the-workaround"&gt;CSS Grid: declare the matrix, not the workaround&lt;/h2&gt;
&lt;p&gt;CSS Grid is brilliant because it treats layout as a two-dimensional problem from the start. You define a grid and place items into it. The mental model is closer to how designers actually think: columns, rows, and areas.&lt;/p&gt;
&lt;p&gt;The most productive way to start is to focus on the columns first.&lt;/p&gt;
&lt;h3 id="a-responsive-grid-without-media-query-spaghetti"&gt;A responsive grid without media-query spaghetti&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;gallery&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;grid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;gap&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-columns&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;repeat&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;fit, &lt;span style="color:#a6e22e"&gt;minmax&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;240&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;fr));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That single declaration usually replaces multiple breakpoints. Why? Because you’re telling Grid how to size columns: each card wants at least &lt;code&gt;240px&lt;/code&gt;, and remaining space is shared.&lt;/p&gt;
&lt;p&gt;Now add your items:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;div&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;class&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;gallery&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;...&amp;lt;/&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;...&amp;lt;/&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;...&amp;lt;/&lt;span style="color:#f92672"&gt;article&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You’ll get a responsive layout that naturally adapts as the container width changes. This is what “learn Grid properly” means in practice: stop hand-tuning widths and start describing intent.&lt;/p&gt;
&lt;h2 id="named-areas-the-cleanest-way-to-communicate-layout"&gt;Named areas: the cleanest way to communicate layout&lt;/h2&gt;
&lt;p&gt;Grid’s named areas are one of those features that feel like cheating—until you realize how much time they save and how clearly they express layout structure.&lt;/p&gt;
&lt;p&gt;Imagine a landing page section with this layout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Header spans full width&lt;/li&gt;
&lt;li&gt;Main content is left; sidebar is right&lt;/li&gt;
&lt;li&gt;A promo band spans full width&lt;/li&gt;
&lt;li&gt;Footer spans full width&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can express that like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;page&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;grid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;gap&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-columns&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;fr &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;fr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-areas&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;header header&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main sidebar&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;promo promo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer footer&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;header&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;grid-area&lt;/span&gt;: header; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;main&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;grid-area&lt;/span&gt;: main; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;sidebar&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;grid-area&lt;/span&gt;: sidebar; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;promo&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;grid-area&lt;/span&gt;: promo; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;footer&lt;/span&gt; { &lt;span style="color:#66d9ef"&gt;grid-area&lt;/span&gt;: footer; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now let it adapt:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;max-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;800px&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;page&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-columns&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;fr;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-areas&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;header&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;sidebar&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;promo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;footer&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Yes, there’s still a media query—but it’s a &lt;em&gt;layout decision&lt;/em&gt;, not a width-math patch. You’re changing the shape of the template, not tweaking individual item sizes.&lt;/p&gt;
&lt;p&gt;This is the difference between “Grid thinking” and “Flex thinking with extra steps.”&lt;/p&gt;
&lt;h2 id="auto-fit-vs-auto-fill-choose-the-behavior-you-actually-want"&gt;Auto-fit vs auto-fill: choose the behavior you actually want&lt;/h2&gt;
&lt;p&gt;People treat &lt;code&gt;auto-fit&lt;/code&gt; and &lt;code&gt;auto-fill&lt;/code&gt; like they’re interchangeable. They aren’t. The difference shows up when there’s leftover space.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auto-fill&lt;/code&gt; keeps the grid tracks even if some end up empty.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto-fit&lt;/code&gt; collapses tracks that don’t have content to fill them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most “cards that should expand to fill the row” designs, &lt;code&gt;auto-fit&lt;/code&gt; feels right. For layouts where you want consistent track structure regardless of how many items there are, &lt;code&gt;auto-fill&lt;/code&gt; can be the better fit.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;tiles&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;grid&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;gap&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;grid-template-columns&lt;/span&gt;: &lt;span style="color:#a6e22e"&gt;repeat&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;auto&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;fill&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;minmax&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;px&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;fr));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you have three tiles and a wide container, &lt;code&gt;auto-fill&lt;/code&gt; will preserve the grid’s track count, keeping the visual rhythm stable. If you’d rather have the existing tiles stretch to occupy the extra space, &lt;code&gt;auto-fit&lt;/code&gt; will feel more natural.&lt;/p&gt;
&lt;p&gt;A practical rule: if the layout looks “too sparse” with &lt;code&gt;auto-fill&lt;/code&gt;, try &lt;code&gt;auto-fit&lt;/code&gt;. If it starts looking “too floaty,” stick with &lt;code&gt;auto-fill&lt;/code&gt; and control the container width instead.&lt;/p&gt;
&lt;h2 id="subgrid-the-missing-piece-when-nested-layouts-must-align"&gt;Subgrid: the missing piece when nested layouts must align&lt;/h2&gt;
&lt;p&gt;Once you build real systems—design systems, component libraries, complex admin pages—you run into a problem: nested components shouldn’t fight the parent’s alignment.&lt;/p&gt;
&lt;p&gt;CSS Grid’s &lt;code&gt;subgrid&lt;/code&gt; is the answer for grid-to-grid alignment. It lets a child inherit the parent’s track sizing, so columns and spacing line up without hacks.&lt;/p&gt;
&lt;p&gt;Even if you’ve never used &lt;code&gt;subgrid&lt;/code&gt;, you’ve probably seen the workaround:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hard-coded padding values&lt;/li&gt;
&lt;li&gt;Re-declared &lt;code&gt;grid-template-columns&lt;/code&gt; inside components “so they match”&lt;/li&gt;
&lt;li&gt;Mismatched gutters that only appear in edge widths&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;code&gt;subgrid&lt;/code&gt;, you can structure layouts so columns remain consistent across component boundaries. The exact availability depends on browser support, so test in your target environments—but as a concept, subgrid is exactly how you should think about nested layout: alignment is a shared contract, not a best-effort coincidence.&lt;/p&gt;
&lt;h2 id="so-is-grid-a-replacement-for-flexbox"&gt;So… is Grid a replacement for Flexbox?&lt;/h2&gt;
&lt;p&gt;No. Flexbox still matters—and if you try to use Grid for everything, you’ll create your own mess.&lt;/p&gt;
&lt;p&gt;Use Flexbox when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You’re aligning items along one axis&lt;/li&gt;
&lt;li&gt;You need easy ordering within a row or column&lt;/li&gt;
&lt;li&gt;You’re building toolbars, lists, navs, and inline components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use Grid when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need rows and columns as first-class citizens&lt;/li&gt;
&lt;li&gt;You want templates that describe layout structure&lt;/li&gt;
&lt;li&gt;You’re building responsive page regions (not just “wrapping items”)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s a simple decision shortcut:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you can draw the layout as a table of rows/columns, use Grid.&lt;/li&gt;
&lt;li&gt;If you can describe it as “items in a line,” use Flexbox.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you find yourself nesting flex containers to create columns, stop and ask: “Am I recreating a grid with wrappers?” If yes, you already know what to use.&lt;/p&gt;
&lt;h2 id="conclusion-accept-grid-as-the-default-for-page-layout"&gt;Conclusion: accept Grid as the default for page layout&lt;/h2&gt;
&lt;p&gt;Flexbox is brilliant for one-dimensional layout. CSS Grid is brilliant for the two-dimensional reality of actual pages. The cost of clinging to Flexbox-first thinking isn’t just longer code—it’s unclear intent, duplicated layout logic, and brittle breakpoints that feel like maintenance debt on day one.&lt;/p&gt;
&lt;p&gt;Learn Grid properly: &lt;code&gt;repeat(auto-*, minmax())&lt;/code&gt; for responsive columns, named areas for readable templates, and subgrid for alignment across component boundaries. You’ll write less CSS, communicate layout intent more clearly, and stop fighting the browser.&lt;/p&gt;</content></item><item><title>Stop Writing Unit Tests for Your React Components</title><link>https://decastro.work/blog/stop-writing-unit-tests-react-components/</link><pubDate>Mon, 28 Mar 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/stop-writing-unit-tests-react-components/</guid><description>&lt;p&gt;React unit tests have become a habit loop: write tests, ship code, refactor, watch tests explode anyway. It’s not that unit tests are “bad”—it’s that most React teams accidentally use them to test the wrong thing. When your tests mostly assert implementation details, you pay twice: first in time, then in constant maintenance. The fix isn’t to write &lt;em&gt;more&lt;/em&gt; tests. It’s to write &lt;em&gt;better&lt;/em&gt; tests—fewer, behavior-driven integration tests that exercise the UI like a user would.&lt;/p&gt;</description><content>&lt;p&gt;React unit tests have become a habit loop: write tests, ship code, refactor, watch tests explode anyway. It’s not that unit tests are “bad”—it’s that most React teams accidentally use them to test the wrong thing. When your tests mostly assert implementation details, you pay twice: first in time, then in constant maintenance. The fix isn’t to write &lt;em&gt;more&lt;/em&gt; tests. It’s to write &lt;em&gt;better&lt;/em&gt; tests—fewer, behavior-driven integration tests that exercise the UI like a user would.&lt;/p&gt;
&lt;h2 id="why-react-unit-tests-fail-in-practice"&gt;Why React Unit Tests Fail in Practice&lt;/h2&gt;
&lt;p&gt;A typical “unit test for a component” story goes like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Render the component shallowly.&lt;/li&gt;
&lt;li&gt;Mock half the world.&lt;/li&gt;
&lt;li&gt;Assert internal calls: &lt;code&gt;dispatch&lt;/code&gt; happened, a prop was passed, a callback was invoked, a certain component was rendered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach tends to couple your tests to how the component is built rather than what it does. You refactor the component—rename a handler, lift state, switch from one component to another, change how props are wired—and the tests break even though the UI still behaves correctly.&lt;/p&gt;
&lt;p&gt;Worse, the “shallow rendering” era taught many teams to verify structure, not behavior. Snapshot tests reinforced the same problem: if the rendered output changes, the snapshot changes, and now you’re deciding whether the test or your refactor is “right.” That’s not confidence. That’s noise.&lt;/p&gt;
&lt;p&gt;The real goal of a test is simple: answer “Will this user-facing behavior still work?” Unit tests often can’t answer that question without turning into brittle mocks and internal assertions.&lt;/p&gt;
&lt;h2 id="behavior-beats-implementation-what-testing-library-changed"&gt;Behavior Beats Implementation: What Testing Library Changed&lt;/h2&gt;
&lt;p&gt;Testing Library flipped the default mindset. Instead of reaching into the component and interrogating its internal shape, you query the rendered UI the way a user would:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find elements by accessible role or label.&lt;/li&gt;
&lt;li&gt;Interact using events that simulate reality (typing, clicking, submitting).&lt;/li&gt;
&lt;li&gt;Assert outcomes that matter (text appears, navigation happens, a request is sent, an error is displayed).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This style is powerful because it naturally discourages testing internal implementation. When you query by label, you’re asserting “the form is usable.” When you submit and check for success, you’re asserting “the flow works.” Refactors become less scary because the test is anchored to behavior, not structure.&lt;/p&gt;
&lt;p&gt;Here’s the key shift: you’re not testing React components. You’re testing user journeys through your interface.&lt;/p&gt;
&lt;h2 id="the-integration-test-that-replaces-a-pile-of-unit-tests"&gt;The Integration Test That Replaces a Pile of Unit Tests&lt;/h2&gt;
&lt;p&gt;Consider a simple but realistic case: a checkout form.&lt;/p&gt;
&lt;p&gt;In a unit-test-heavy world, you might write fifteen tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Renders fields&lt;/li&gt;
&lt;li&gt;Validates email format&lt;/li&gt;
&lt;li&gt;Validates credit card fields&lt;/li&gt;
&lt;li&gt;Shows error message when invalid&lt;/li&gt;
&lt;li&gt;Calls the submit function with correct payload&lt;/li&gt;
&lt;li&gt;Disables button while loading&lt;/li&gt;
&lt;li&gt;Shows spinner&lt;/li&gt;
&lt;li&gt;Handles API error&lt;/li&gt;
&lt;li&gt;Handles API success&lt;/li&gt;
&lt;li&gt;Resets form on success&lt;/li&gt;
&lt;li&gt;…and so on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each “unit” test focuses on a piece of logic in isolation. That sounds thorough—until the form grows, a validation rule changes, or you refactor your state management. Suddenly, the tests don’t reflect what the user experiences anymore.&lt;/p&gt;
&lt;p&gt;A single integration test can cover the same risk surface with far less maintenance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Render the form.&lt;/li&gt;
&lt;li&gt;Enter values.&lt;/li&gt;
&lt;li&gt;Submit it.&lt;/li&gt;
&lt;li&gt;Assert the resulting UI state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example (with Testing Library):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; { &lt;span style="color:#a6e22e"&gt;render&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt; } &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;@testing-library/react&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;userEvent&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;@testing-library/user-event&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CheckoutForm&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;./CheckoutForm&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;test&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;submits valid checkout and shows confirmation&amp;#39;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; () &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;userEvent&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;setup&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;render&lt;/span&gt;(&amp;lt;&lt;span style="color:#f92672"&gt;CheckoutForm&lt;/span&gt; /&amp;gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByLabelText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/email/i&lt;/span&gt;), &lt;span style="color:#e6db74"&gt;&amp;#39;ava@example.com&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByLabelText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/card number/i&lt;/span&gt;), &lt;span style="color:#e6db74"&gt;&amp;#39;4242 4242 4242 4242&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByLabelText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/name on card/i&lt;/span&gt;), &lt;span style="color:#e6db74"&gt;&amp;#39;Ava Example&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByLabelText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/expiration/i&lt;/span&gt;), &lt;span style="color:#e6db74"&gt;&amp;#39;12/34&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByLabelText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/cvv/i&lt;/span&gt;), &lt;span style="color:#e6db74"&gt;&amp;#39;123&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;click&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByRole&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;button&amp;#39;&lt;/span&gt;, { &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;/place order/i&lt;/span&gt; }));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;expect&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;findByText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/order confirmed/i&lt;/span&gt;)).&lt;span style="color:#a6e22e"&gt;toBeInTheDocument&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;expect&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;screen&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getByText&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;/receipt/i&lt;/span&gt;)).&lt;span style="color:#a6e22e"&gt;toBeVisible&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That test is not “just one more test.” It’s the highest-signal one: it checks the entire flow works, end to end, in the same way users do. It will also catch real integration bugs—miswired handlers, broken form wiring, incorrect disabling logic, missing success rendering, and many more issues that unit tests often miss.&lt;/p&gt;
&lt;h2 id="when-unit-tests-still-make-sense-yes-really"&gt;When Unit Tests Still Make Sense (Yes, Really)&lt;/h2&gt;
&lt;p&gt;Let’s be clear: unit tests aren’t inherently evil. The mistake is using them as the primary safety net for UI behavior.&lt;/p&gt;
&lt;p&gt;Unit tests are excellent for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pure utilities&lt;/strong&gt;: date formatting, validation functions, mapping transforms, reducers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business rules with no UI&lt;/strong&gt;: if you can feed inputs and assert outputs without rendering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State machines / selectors&lt;/strong&gt;: where behavior is deterministic and stable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the closer your code gets to the UI layer, the more your tests should resemble usage. If your “unit test” is mostly verifying that a particular internal prop was passed to a child component, it’s probably testing implementation detail. Prefer asserting what the user sees instead: the child content appears, the button enables, the message changes.&lt;/p&gt;
&lt;p&gt;A practical rule I’ve found useful:&lt;br&gt;
If you can delete your component and still keep the unit test meaningful, it’s probably not a UI behavior test. That’s fine—just move it to the right layer (utility or domain tests). If the component must exist for the test to be meaningful, then test the UI behavior.&lt;/p&gt;
&lt;h2 id="how-to-write-fewer-better-tests-without-losing-coverage"&gt;How to Write Fewer, Better Tests (Without Losing Coverage)&lt;/h2&gt;
&lt;p&gt;If you’re currently drowning in tests, don’t “refactor tests.” Replace them with a smaller set of integration tests that cover the critical paths.&lt;/p&gt;
&lt;p&gt;Here’s a strategy that works well on real teams:&lt;/p&gt;
&lt;h3 id="1-identify-the-user-journeys-not-the-components"&gt;1) Identify the user journeys, not the components&lt;/h3&gt;
&lt;p&gt;Instead of “Test &lt;code&gt;UserProfileCard&lt;/code&gt;,” think “Test viewing the profile.” You want the test to fail when the user experience breaks.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Login with valid credentials shows the dashboard”&lt;/li&gt;
&lt;li&gt;“Submitting an invalid form highlights the correct fields”&lt;/li&gt;
&lt;li&gt;“Deleting an item updates the list immediately”&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-use-the-ui-as-your-api"&gt;2) Use the UI as your API&lt;/h3&gt;
&lt;p&gt;Write tests that interact with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getByRole&lt;/code&gt;, &lt;code&gt;getByLabelText&lt;/code&gt;, &lt;code&gt;getByText&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;buttons by accessible name&lt;/li&gt;
&lt;li&gt;inputs by labels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you find yourself querying &lt;code&gt;componentInstance&lt;/code&gt; or reaching into internals, that’s a smell.&lt;/p&gt;
&lt;h3 id="3-mock-only-what-you-must"&gt;3) Mock only what you must&lt;/h3&gt;
&lt;p&gt;For network calls and external dependencies, mocking is normal. But avoid mocking everything under the sun. A common pattern is to mock the API layer and let the component’s wiring happen naturally.&lt;/p&gt;
&lt;p&gt;If your form submits via &lt;code&gt;fetch&lt;/code&gt;, mock the request and assert on UI outcomes—not internal &lt;code&gt;fetch&lt;/code&gt; call counts unless it truly matters.&lt;/p&gt;
&lt;h3 id="4-assert-outcomes-not-call-chains"&gt;4) Assert outcomes, not call chains&lt;/h3&gt;
&lt;p&gt;Good assertions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Success message appears&lt;/li&gt;
&lt;li&gt;Error message is shown&lt;/li&gt;
&lt;li&gt;The submit button re-enables after failure&lt;/li&gt;
&lt;li&gt;Navigation occurs (or a route change indicator updates)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Weak assertions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;handleSubmit&lt;/code&gt; was called with exact arguments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dispatch&lt;/code&gt; was called N times&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ChildComponent&lt;/code&gt; was rendered with prop X&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Call-chain assertions may catch bugs, but they’re brittle. Outcome assertions catch more bugs with less maintenance.&lt;/p&gt;
&lt;h2 id="common-traps-and-how-to-avoid-them"&gt;Common Traps (And How to Avoid Them)&lt;/h2&gt;
&lt;p&gt;Even behavior-driven integration tests can go wrong. Avoid these traps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Testing non-accessible UI selectors&lt;/strong&gt;: If you rely on &lt;code&gt;data-testid&lt;/code&gt; everywhere, you’ll end up recreating brittle coupling. Use it sparingly—accessibility queries should be your first choice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overfitting to exact text&lt;/strong&gt;: Assert key phrases or patterns that represent user intent, not the entire formatting of a sentence that designers love to tweak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One giant test for everything&lt;/strong&gt;: Integration tests should still be focused. One test can cover one journey. If you’re testing five different flows in a single file, split it into separate behavior tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshotting UI&lt;/strong&gt;: Snapshots are useful for debugging, not for long-term confidence. If the UI changes frequently, snapshots become maintenance work disguised as verification.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is stable tests that tell you something meaningful when they fail. When tests fail, you should understand the user-facing behavior that broke.&lt;/p&gt;
&lt;h2 id="conclusion-replace-brittle-unit-tests-with-real-confidence"&gt;Conclusion: Replace Brittle Unit Tests With Real Confidence&lt;/h2&gt;
&lt;p&gt;React unit tests often become a maintenance tax because they validate implementation details, not user behavior. Integration tests with Testing Library give you a better signal: render the UI, interact like a user, and assert outcomes that matter. You’ll write fewer tests, catch real bugs, and refactor with confidence instead of dread.&lt;/p&gt;</content></item><item><title>Docker Changed Everything. Kubernetes Is Changing It Again.</title><link>https://decastro.work/blog/docker-changed-everything-kubernetes-changing-again/</link><pubDate>Thu, 17 Mar 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/docker-changed-everything-kubernetes-changing-again/</guid><description>&lt;p&gt;Containers didn’t just make deployments easier—they changed the mental model. Overnight, “it works on my machine” became less of a battle cry and more of an embarrassing punchline. Docker made packaging software feel portable and predictable. And then Kubernetes arrived and promised something even bigger: not just portability, but automation at scale.&lt;/p&gt;
&lt;p&gt;Here’s the dirty secret, though: most teams running Kubernetes don’t need Kubernetes. The orchestration layer is where complexity hides—exactly the kind of complexity that can slow down teams who are still growing, still learning, or still delivering business value faster than they’re managing infrastructure.&lt;/p&gt;</description><content>&lt;p&gt;Containers didn’t just make deployments easier—they changed the mental model. Overnight, “it works on my machine” became less of a battle cry and more of an embarrassing punchline. Docker made packaging software feel portable and predictable. And then Kubernetes arrived and promised something even bigger: not just portability, but automation at scale.&lt;/p&gt;
&lt;p&gt;Here’s the dirty secret, though: most teams running Kubernetes don’t need Kubernetes. The orchestration layer is where complexity hides—exactly the kind of complexity that can slow down teams who are still growing, still learning, or still delivering business value faster than they’re managing infrastructure.&lt;/p&gt;
&lt;h2 id="why-docker-felt-like-magic-and-why-it-stuck"&gt;Why Docker Felt Like Magic (And Why It Stuck)&lt;/h2&gt;
&lt;p&gt;Before containers, deployments were a patchwork of scripts, SSH sessions, bespoke server images, and tribal knowledge. Docker turned that into a repeatable artifact: build once, run anywhere. That’s not just a developer convenience—it’s an operational upgrade.&lt;/p&gt;
&lt;p&gt;Consider a common workflow: you have a web app, a background worker, and a database. With Docker, you can define services, wire them together, and run the whole stack locally exactly as it behaves in production—down to environment variables, filesystem layout, and network expectations. That consistency reduces debugging time and makes CI more trustworthy.&lt;/p&gt;
&lt;p&gt;Even better, Docker made “deployment” less about what your servers look like and more about what your images contain. Your pipeline builds images, pushes them to a registry, and your runtime pulls them. Clear boundaries. Fewer surprises.&lt;/p&gt;
&lt;p&gt;This is why Docker became the default building block for modern systems. It lowered the cost of shipping.&lt;/p&gt;
&lt;h2 id="kubernetes-great-promise-heavy-machinery"&gt;Kubernetes: Great Promise, Heavy Machinery&lt;/h2&gt;
&lt;p&gt;Kubernetes is not “Docker, but better.” It’s a whole operational philosophy: declarative desired state, continuous reconciliation, and a control plane that keeps running systems aligned with your intentions.&lt;/p&gt;
&lt;p&gt;If you’re only running a couple services with straightforward scaling patterns, Kubernetes can feel like bringing a full orchestra to play a single song on a piano. The piano works. The orchestra sounds impressive. But the rehearsal schedule is brutal.&lt;/p&gt;
&lt;p&gt;The overhead isn’t theoretical. It shows up in work you didn’t plan for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cluster management&lt;/strong&gt;: upgrades, node scaling, networking quirks, security policies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational maturity&lt;/strong&gt;: observability, incident response, rollback strategy, service discovery.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow complexity&lt;/strong&gt;: YAML-heavy manifests, Helm charts (often), GitOps tooling (sometimes), and debugging reconciliation loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kubernetes requires a certain kind of muscle memory. If your team doesn’t have it yet, that’s not a moral failing—it’s just reality. For smaller teams, the operational surface area can steal attention from the engineering that actually differentiates your product.&lt;/p&gt;
&lt;p&gt;And that’s the core tension: containers won the deployment war, but orchestration is where the real complexity lives.&lt;/p&gt;
&lt;h2 id="the-reality-check-when-kubernetes-is-the-right-tool"&gt;The Reality Check: When Kubernetes Is the Right Tool&lt;/h2&gt;
&lt;p&gt;Kubernetes becomes worth it when you have &lt;em&gt;system-level&lt;/em&gt; needs that are hard to automate reliably with simpler tools. “Hard” here means operationally risky, not just inconvenient.&lt;/p&gt;
&lt;p&gt;You’re more likely in the right zone if you have several of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple environments with frequent change&lt;/strong&gt;: staging and production with different constraints, plus regular rollout patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unpredictable load&lt;/strong&gt; requiring robust autoscaling and workload isolation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dozens (or hundreds) of services&lt;/strong&gt; with consistent patterns for networking, discovery, and scaling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex availability requirements&lt;/strong&gt;: multiple failure domains, strict rollout strategies, and mature rollback behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform goals&lt;/strong&gt;: you’re building an internal ecosystem where teams deploy services without touching infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical rule of thumb: if your deployment process is already stable and your scaling needs are predictable, Kubernetes is often a tax, not a benefit. If you’re outgrowing that stability—if failures and scaling edge cases start to dominate your time—Kubernetes starts looking less like machinery and more like leverage.&lt;/p&gt;
&lt;h2 id="the-case-for-docker-compose--cicd-even-in-production"&gt;The Case for Docker Compose + CI/CD (Even in Production)&lt;/h2&gt;
&lt;p&gt;If you’re under 50 engineers—or even close—there’s a strong argument to stop romanticizing Kubernetes and focus on shipping. Docker Compose and a disciplined CI/CD pipeline can handle a surprising amount of real-world production work.&lt;/p&gt;
&lt;h3 id="a-concrete-example-a-service-with-a-web-tier-and-worker-tier"&gt;A concrete example: a service with a web tier and worker tier&lt;/h3&gt;
&lt;p&gt;You can structure this cleanly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;web&lt;/strong&gt; container (stateless)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;worker&lt;/strong&gt; container (consumes jobs)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;database&lt;/strong&gt; (managed externally, or containerized if appropriate)&lt;/li&gt;
&lt;li&gt;Shared configuration via environment variables and secrets injection&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With Docker Compose, your local environment mirrors integration behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker compose up&lt;/code&gt; for developer parity&lt;/li&gt;
&lt;li&gt;deterministic local dependency graph&lt;/li&gt;
&lt;li&gt;reproducible config for tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, your CI/CD does the serious work:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Build images for web and worker.&lt;/li&gt;
&lt;li&gt;Run tests and integration checks (at least against a disposable environment).&lt;/li&gt;
&lt;li&gt;Promote images to a staging registry/release tag.&lt;/li&gt;
&lt;li&gt;Deploy using a repeatable mechanism (often pulling the same image tags from the registry).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The result: you get container portability without inheriting Kubernetes’ orchestration tax.&lt;/p&gt;
&lt;h3 id="what-about-scaling-and-reliability"&gt;What about scaling and reliability?&lt;/h3&gt;
&lt;p&gt;If scaling is needed, you can scale at the infrastructure level (load balancer, horizontal scaling in the platform, or managed services) without implementing every orchestration concept yourself. Many teams use a managed runtime (VMs, container platforms, or platform-as-a-service) where the heavy lifting—process supervision, networking plumbing, and rolling updates—is handled by the platform.&lt;/p&gt;
&lt;p&gt;The key isn’t “no orchestration.” It’s “appropriate orchestration.”&lt;/p&gt;
&lt;p&gt;For many teams, that means you orchestrate workflows in CI/CD and let infrastructure handle the mechanics.&lt;/p&gt;
&lt;h2 id="when-you-outgrow-it-a-migration-path-that-doesnt-hurt"&gt;When You Outgrow It: A Migration Path That Doesn’t Hurt&lt;/h2&gt;
&lt;p&gt;If Kubernetes is the destination, don’t treat it like a monolithic switch. Treat it like a staged migration with clear wins.&lt;/p&gt;
&lt;p&gt;A sensible progression looks like this:&lt;/p&gt;
&lt;h3 id="step-1-standardize-your-images-and-rollout-strategy"&gt;Step 1: Standardize your images and rollout strategy&lt;/h3&gt;
&lt;p&gt;Before you touch Kubernetes, make sure your Docker images are consistent, versioned, and immutable. Your CI should already be producing the same artifacts you would deploy anywhere.&lt;/p&gt;
&lt;h3 id="step-2-introduce-kubernetes-ready-patterns"&gt;Step 2: Introduce “Kubernetes-ready” patterns&lt;/h3&gt;
&lt;p&gt;Build with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;clear health checks (liveness/readiness concepts)&lt;/li&gt;
&lt;li&gt;stateless services where possible&lt;/li&gt;
&lt;li&gt;explicit resource limits&lt;/li&gt;
&lt;li&gt;predictable configuration via environment variables&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-3-run-one-service-on-kubernetes"&gt;Step 3: Run one service on Kubernetes&lt;/h3&gt;
&lt;p&gt;Pick a low-risk service or a service with strong boundaries. Migrate it first. Evaluate operational overhead with real workloads—not fantasy.&lt;/p&gt;
&lt;h3 id="step-4-decide-if-its-platform-level-or-service-level"&gt;Step 4: Decide if it’s platform-level, or service-level&lt;/h3&gt;
&lt;p&gt;Some teams use Kubernetes as a service deployment target. Others use it as a platform. The latter is where the real complexity ramps up. Be honest: if you want a platform, you must invest in it—documentation, tooling, and operational ownership.&lt;/p&gt;
&lt;h3 id="step-5-keep-docker-yes-really"&gt;Step 5: Keep Docker (yes, really)&lt;/h3&gt;
&lt;p&gt;Kubernetes doesn’t replace the container model. It replaces the deployment and runtime mechanics. Your images still matter. Your CI/CD still matters. Kubernetes just becomes the orchestrator.&lt;/p&gt;
&lt;p&gt;The best migrations preserve what’s working: build pipelines, artifact discipline, and deployment confidence.&lt;/p&gt;
&lt;h2 id="a-sharp-opinion-optimize-for-team-velocity-not-tool-prestige"&gt;A Sharp Opinion: Optimize for Team Velocity, Not Tool Prestige&lt;/h2&gt;
&lt;p&gt;It’s fashionable to frame Kubernetes adoption as inevitable. But inevitability is a myth businesses like because it sounds like certainty. Teams don’t need inevitability—they need velocity.&lt;/p&gt;
&lt;p&gt;For many organizations, especially those that are still building, Kubernetes overhead can show up as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more time spent debugging infrastructure&lt;/li&gt;
&lt;li&gt;more time writing deployment manifests instead of improving product code&lt;/li&gt;
&lt;li&gt;harder incident response due to more moving parts&lt;/li&gt;
&lt;li&gt;operational burnout from cluster babysitting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Meanwhile, Docker Compose plus CI/CD gives you a tighter feedback loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fewer systems to learn&lt;/li&gt;
&lt;li&gt;fewer failure modes to manage&lt;/li&gt;
&lt;li&gt;clearer ownership boundaries&lt;/li&gt;
&lt;li&gt;easier onboarding for new engineers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Kubernetes is powerful. It’s also opinionated. And power has a cost.&lt;/p&gt;
&lt;p&gt;So the question isn’t “Which tool is modern?” The question is: “What complexity are we willing to own so we can ship faster?”&lt;/p&gt;
&lt;h2 id="conclusion-containers-first-orchestration-when-its-worth-the-price"&gt;Conclusion: Containers First, Orchestration When It’s Worth the Price&lt;/h2&gt;
&lt;p&gt;Docker changed software deployment by making artifacts portable and repeatable. Kubernetes can extend that story by automating orchestration and scale—but it also introduces a level of operational complexity that many teams don’t need.&lt;/p&gt;
&lt;p&gt;If you’re not at scale, start with Docker Compose and a strong CI/CD pipeline. Standardize your images, make rollouts boring, and let infrastructure handle the mechanics where possible. When your requirements outgrow the simple path, migrate deliberately—service by service, with discipline.&lt;/p&gt;
&lt;p&gt;Containers got you to “works everywhere.” Orchestration is what you add when “works reliably everywhere” becomes the real business demand.&lt;/p&gt;</content></item><item><title>The Git Workflow That Actually Scales</title><link>https://decastro.work/blog/git-workflow-that-actually-scales/</link><pubDate>Thu, 10 Mar 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/git-workflow-that-actually-scales/</guid><description>&lt;p&gt;Most Git workflows fail for the same reason: they’re optimized for the moment you tag a release, not for the reality of everyday change. If your developers spend their time shepherding long-lived branches through manual merges, you’re not scaling—you&amp;rsquo;re amortizing pain.&lt;/p&gt;
&lt;p&gt;Trunk-based development with short-lived branches isn’t a trend. It’s a response to how modern teams deliver software: continuous integration, frequent deployments, and feedback loops measured in minutes, not sprints. And yes—done properly, it beats GitFlow.&lt;/p&gt;</description><content>&lt;p&gt;Most Git workflows fail for the same reason: they’re optimized for the moment you tag a release, not for the reality of everyday change. If your developers spend their time shepherding long-lived branches through manual merges, you’re not scaling—you&amp;rsquo;re amortizing pain.&lt;/p&gt;
&lt;p&gt;Trunk-based development with short-lived branches isn’t a trend. It’s a response to how modern teams deliver software: continuous integration, frequent deployments, and feedback loops measured in minutes, not sprints. And yes—done properly, it beats GitFlow.&lt;/p&gt;
&lt;h2 id="why-gitflow-breaks-in-the-real-world"&gt;Why GitFlow Breaks in the Real World&lt;/h2&gt;
&lt;p&gt;GitFlow assumes a world where work funnels through scheduled releases and well-defined QA gates. In that world, it’s rational to have long-lived &lt;code&gt;develop&lt;/code&gt;, a &lt;code&gt;release&lt;/code&gt; branch, and supporting branches that exist long enough to “collect changes” for the next drop.&lt;/p&gt;
&lt;p&gt;But most teams don’t actually operate like that. They work in smaller increments, merge multiple times per day, run automated tests constantly, and release whenever a build passes a checklist—not when a calendar tells them to.&lt;/p&gt;
&lt;p&gt;Here’s what goes wrong when GitFlow meets that reality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Merge conflicts become inevitable&lt;/strong&gt;, not exceptional. The longer a feature branch exists, the more it diverges from &lt;code&gt;develop&lt;/code&gt;, and the more painful the eventual integration becomes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“Integration debt” accumulates.&lt;/strong&gt; Your team can work for days without truly integrating anything. Then one person spends half a day resolving conflicts while everyone else waits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release branches become confusion magnets.&lt;/strong&gt; Changes land in the wrong place, or get partially backported, or get re-labeled because “release X” now needs to include “that other thing.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core issue isn’t that GitFlow is “bad.” It’s that its model is mismatched to modern delivery systems. If your branching strategy relies on weeks of parallel work, you’ve designed for a bottleneck.&lt;/p&gt;
&lt;h2 id="trunk-based-development-the-principle-behind-the-practice"&gt;Trunk-Based Development: The Principle Behind the Practice&lt;/h2&gt;
&lt;p&gt;Trunk-based development (TBD) flips the default assumption: the source of truth lives on the trunk (usually &lt;code&gt;main&lt;/code&gt;), and changes merge into it frequently. You still use branches—but they’re tools for short-lived isolation, not storage for weeks of work.&lt;/p&gt;
&lt;p&gt;A healthy TBD workflow has three non-negotiables:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Short-lived branches&lt;/strong&gt;: merge readiness in hours, not weeks. A practical rule of thumb is &lt;strong&gt;under 24 hours&lt;/strong&gt;. If your branch regularly lives longer than a day, it’s signaling a process problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated integration&lt;/strong&gt;: every pull request (PR) runs CI, including relevant tests, linters, and builds. “Green” should mean something.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frequent deployments&lt;/strong&gt;: if you can’t deploy often, you can’t safely hide unfinished work. You’ll start treating deployment like a gate again—recreating GitFlow’s pain.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Done correctly, this produces a simple outcome: engineers get fast feedback because the trunk constantly evolves, and teams avoid the integration cliffs that long-lived branches create.&lt;/p&gt;
&lt;h2 id="the-mechanics-short-lived-branches-that-stay-mergeable"&gt;The Mechanics: Short-Lived Branches That Stay Mergeable&lt;/h2&gt;
&lt;p&gt;Let’s make this concrete. A typical TBD day might look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create a branch: &lt;code&gt;feature/payments-endpoint-refactor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Make incremental commits&lt;/li&gt;
&lt;li&gt;Open a PR early, even if the feature isn’t fully finished&lt;/li&gt;
&lt;li&gt;Let CI gate merge until the code compiles and passes the relevant automated checks&lt;/li&gt;
&lt;li&gt;Merge quickly&lt;/li&gt;
&lt;li&gt;Continue iterating as new commits and PRs land on the trunk&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The secret to making this work isn’t only “short branches.” It’s keeping them &lt;em&gt;mergeable&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slice work vertically.&lt;/strong&gt; Build a thin, working slice that compiles, passes tests, and can be safely enabled or disabled. Don’t chase “perfect completeness” before integration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avoid mega-branches.&lt;/strong&gt; If a branch requires coordinating multiple teams or waiting on external dependencies, you’re not isolating risk—you’re postponing it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep PRs small enough to review fast.&lt;/strong&gt; If you can’t review it within a reasonable time window, it won’t merge quickly, and it’ll start drifting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One practical tactic: treat the PR title like a user story boundary. If the PR can’t be described as “make X work in Y scenario,” it’s probably too broad.&lt;/p&gt;
&lt;h2 id="feature-flags-your-safety-net-for-incomplete-work"&gt;Feature Flags: Your Safety Net for Incomplete Work&lt;/h2&gt;
&lt;p&gt;Short-lived branches only solve half the problem. The other half is what happens when unfinished code lands on &lt;code&gt;main&lt;/code&gt;. That’s where feature flags come in.&lt;/p&gt;
&lt;p&gt;A feature flag lets you merge code even if you can’t—or shouldn’t—turn it on for every user yet. Your workflow becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Merge code behind a flag&lt;/li&gt;
&lt;li&gt;Deploy continuously&lt;/li&gt;
&lt;li&gt;Enable the feature for a limited audience (or 0% traffic)&lt;/li&gt;
&lt;li&gt;Gradually expand once confidence is earned&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: Suppose you’re rewriting a checkout endpoint and you want to reduce risk.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You implement the new path behind &lt;code&gt;checkout_v2_enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CI runs tests for the new code&lt;/li&gt;
&lt;li&gt;The code merges into &lt;code&gt;main&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;Deployment occurs multiple times per day&lt;/li&gt;
&lt;li&gt;You enable the flag for internal traffic first&lt;/li&gt;
&lt;li&gt;Then you expand to a small percentage of production traffic&lt;/li&gt;
&lt;li&gt;Finally, you remove the old code once the new path is stable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach prevents the classic GitFlow scenario where the branch exists “until QA is done.” Instead, QA becomes continuous and operational: monitoring, automated checks, and controlled rollout replace the big manual handoffs.&lt;/p&gt;
&lt;p&gt;If feature flags sound like extra complexity, that’s fair—but the complexity is preferable to the alternative. When you don’t use flags, you must delay merging until everything is ready, which forces the long-lived branches TBD exists to eliminate.&lt;/p&gt;
&lt;h2 id="cicd-and-guardrails-fast-feedback-without-chaos"&gt;CI/CD and Guardrails: Fast Feedback Without Chaos&lt;/h2&gt;
&lt;p&gt;Trunk-based development works only if your tooling enforces quality at speed. Otherwise, “frequent merges” turn into “frequent breakage.”&lt;/p&gt;
&lt;p&gt;A scalable TBD setup typically includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Branch protections&lt;/strong&gt; on &lt;code&gt;main&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;require PRs&lt;/li&gt;
&lt;li&gt;require CI to pass&lt;/li&gt;
&lt;li&gt;require review approvals from relevant owners&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A clear CI split&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;fast checks on every PR (lint, unit tests, compile)&lt;/li&gt;
&lt;li&gt;broader checks either on schedule or for specific changes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated deployment&lt;/strong&gt; to at least staging continuously, and production frequently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don’t need a perfect pipeline from day one. But you do need guardrails that make it difficult to merge broken code.&lt;/p&gt;
&lt;p&gt;Here’s a practical framing that works: define your pipeline as a contract. Every PR promises, “If this merge lands, the trunk remains buildable and tests remain meaningful.” When that contract holds, engineers stop fearing &lt;code&gt;main&lt;/code&gt; and start trusting it.&lt;/p&gt;
&lt;h2 id="what-to-do-if-youre-still-branching-like-gitflow"&gt;What to Do If You’re Still “Branching Like GitFlow”&lt;/h2&gt;
&lt;p&gt;If your branches currently live for weeks, don’t try to flip the switch overnight. You need a migration plan that reduces pain immediately.&lt;/p&gt;
&lt;p&gt;Start with these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Measure your current branch lifetimes.&lt;/strong&gt; If most branches last multiple weeks, expect merge conflicts and delayed integration—because that’s what your process is doing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduce branch scope.&lt;/strong&gt; Break work into vertical slices that can be reviewed and merged sooner. Even if branches still last longer than ideal, this makes them less divergence-heavy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Introduce feature flags for risky changes.&lt;/strong&gt; Begin with one area—say, customer-facing UI toggles or a single backend endpoint—then expand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift release thinking to deployment thinking.&lt;/strong&gt; Instead of “release when ready,” aim for “deploy whenever safe.” If you can’t deploy yet, the branch strategy can’t fully evolve.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set a hard expectation for branch duration.&lt;/strong&gt; Not as bureaucracy, but as a forcing function. If a branch is approaching 24 hours, either merge it behind flags or split it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Be opinionated about it: if the branch can’t be merged quickly, the work isn’t ready to be isolated—it’s ready to be re-sliced.&lt;/p&gt;
&lt;h2 id="conclusion-scaling-is-integration-not-scheduling"&gt;Conclusion: Scaling Is Integration, Not Scheduling&lt;/h2&gt;
&lt;p&gt;GitFlow is elegant for a world of scheduled releases and manual gates. Modern teams don’t live there. They live in CI, continuous deployment, and constant user feedback—and they need a Git workflow that supports that reality.&lt;/p&gt;
&lt;p&gt;Trunk-based development scales because it keeps integration continuous. Short-lived branches keep merges boring. Feature flags keep “unfinished” safe. And CI/CD keeps the trunk trustworthy.&lt;/p&gt;
&lt;p&gt;If your branches regularly last weeks, you don’t have a branching strategy—you have a merging problem. Fix that, and everything else gets easier: fewer conflicts, faster reviews, and a workflow your team actually wants to use.&lt;/p&gt;</content></item><item><title>Your REST API Is Probably Fine. Stop Reaching for GraphQL.</title><link>https://decastro.work/blog/rest-api-fine-stop-reaching-graphql/</link><pubDate>Sat, 05 Mar 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rest-api-fine-stop-reaching-graphql/</guid><description>&lt;p&gt;GraphQL gets a lot of attention because it’s clever. But clever isn’t the same thing as better for your business. In most teams, your REST API isn’t the bottleneck—your product requirements, data modeling, and API ergonomics are. If you’re considering GraphQL “because the industry does,” pause. REST with good design already solves the majority of real-world problems, and it comes with fewer moving parts when you’re trying to ship.&lt;/p&gt;
&lt;h2 id="the-graphql-pitch-fewer-round-trips-more-flexibility"&gt;The GraphQL pitch: fewer round trips, more flexibility&lt;/h2&gt;
&lt;p&gt;GraphQL’s core value is simple: clients request exactly the fields they need, in exactly the shape they want. Instead of endpoints like:&lt;/p&gt;</description><content>&lt;p&gt;GraphQL gets a lot of attention because it’s clever. But clever isn’t the same thing as better for your business. In most teams, your REST API isn’t the bottleneck—your product requirements, data modeling, and API ergonomics are. If you’re considering GraphQL “because the industry does,” pause. REST with good design already solves the majority of real-world problems, and it comes with fewer moving parts when you’re trying to ship.&lt;/p&gt;
&lt;h2 id="the-graphql-pitch-fewer-round-trips-more-flexibility"&gt;The GraphQL pitch: fewer round trips, more flexibility&lt;/h2&gt;
&lt;p&gt;GraphQL’s core value is simple: clients request exactly the fields they need, in exactly the shape they want. Instead of endpoints like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /users/123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/123/orders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/123/orders/456/items&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL lets a client request a nested graph in one call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-graphql" data-lang="graphql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;query&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;user&lt;/span&gt;(id: &lt;span style="color:#e6db74"&gt;&amp;#34;123&amp;#34;&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; orders {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; items {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sku
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; quantity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That flexibility is legitimate. It’s also seductive for teams wrestling with multiple clients—say, web, mobile, partner integrations—each wanting different subsets of the same data.&lt;/p&gt;
&lt;p&gt;But the key is not whether GraphQL is capable. The question is whether your constraints match what GraphQL optimizes.&lt;/p&gt;
&lt;h2 id="the-reality-check-most-teams-dont-need-a-query-language"&gt;The reality check: most teams don’t need a query language&lt;/h2&gt;
&lt;p&gt;Most projects don’t have deeply nested, highly variable data requirements across many clients. Most teams have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A handful of predictable views or screens&lt;/li&gt;
&lt;li&gt;A relatively stable set of resources&lt;/li&gt;
&lt;li&gt;A client-server communication pattern that looks like “fetch list, fetch details, submit mutation”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In those cases, a well-designed REST API is not “less modern.” It’s simpler, more observable, and easier to evolve without building an internal tooling stack around a query language.&lt;/p&gt;
&lt;p&gt;Here’s the important nuance: GraphQL isn’t just “REST with one endpoint.” It changes how caching works, how debugging looks, and how you think about authorization and performance. Those are solvable problems—but they’re not free.&lt;/p&gt;
&lt;p&gt;If your system is mostly CRUD-shaped, you’ll feel GraphQL overhead more than its benefits.&lt;/p&gt;
&lt;h2 id="why-rest-wins-for-day-to-day-engineering-caching-errors-debugging"&gt;Why REST wins for day-to-day engineering: caching, errors, debugging&lt;/h2&gt;
&lt;p&gt;REST’s superpower for many teams is that it fits the web’s mental model.&lt;/p&gt;
&lt;h3 id="http-caching-is-straightforward"&gt;HTTP caching is straightforward&lt;/h3&gt;
&lt;p&gt;With REST, you can lean on standard caching semantics—ETags, &lt;code&gt;Cache-Control&lt;/code&gt;, conditional requests—without inventing new rules.&lt;/p&gt;
&lt;p&gt;Example: you can set an ETag on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /products/sku-123&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then clients can revalidate efficiently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;If-None-Match: &amp;quot;abc123&amp;quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL can approximate this, but you’re building policy around a transport that no longer maps cleanly to resource identity the way HTTP does.&lt;/p&gt;
&lt;h3 id="error-handling-stays-obvious"&gt;Error handling stays obvious&lt;/h3&gt;
&lt;p&gt;REST encourages you to model failure modes clearly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;404 Not Found&lt;/code&gt; means missing resource&lt;/li&gt;
&lt;li&gt;&lt;code&gt;400 Bad Request&lt;/code&gt; means validation issues&lt;/li&gt;
&lt;li&gt;&lt;code&gt;409 Conflict&lt;/code&gt; means concurrency or state conflicts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GraphQL often returns HTTP 200 with an errors array (or other variations), which means your client and observability tooling have to work harder to correctly interpret outcomes. You can fix this, but again: more moving parts.&lt;/p&gt;
&lt;h3 id="debugging-is-easier-when-requests-are-deterministic"&gt;Debugging is easier when requests are deterministic&lt;/h3&gt;
&lt;p&gt;A REST request maps to a URI, an HTTP method, and a payload. When something goes wrong, the reproduction path is obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Call this endpoint with these parameters.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With GraphQL, the “shape” is in the query document itself. Two queries might hit the same endpoint but represent very different workloads. That makes tracing and performance analysis trickier unless you invest in mature tooling and conventions.&lt;/p&gt;
&lt;h2 id="the-real-graphql-shaped-problems-and-when-to-actually-use-it"&gt;The real “GraphQL-shaped” problems (and when to actually use it)&lt;/h2&gt;
&lt;p&gt;GraphQL becomes compelling when you truly have the conditions it’s designed for:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Multiple clients with different data needs&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Example: web needs a compact dashboard; mobile needs more context; partner APIs need a different projection.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex object graphs&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Example: permissions and nested associations where clients frequently need multi-hop data in one go.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI-driven data shaping&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Example: front-end teams iterating rapidly on screens that assemble data from many related entities.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your team is building something like a rich internal platform—where dozens of screens request overlapping, nested structures—GraphQL can reduce chattiness and improve developer productivity.&lt;/p&gt;
&lt;p&gt;But if your API is basically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /resources&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /resources/:id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /resources&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH /resources/:id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…then the “GraphQL flexibility” is mostly theoretical. You’re paying complexity tax for flexibility you don’t regularly exercise.&lt;/p&gt;
&lt;h2 id="if-you-want-to-modernize-rest-do-it-the-right-way"&gt;If you want to modernize REST, do it the right way&lt;/h2&gt;
&lt;p&gt;“Stop reaching for GraphQL” doesn’t mean “do nothing.” It means fix the things that actually make REST feel painful.&lt;/p&gt;
&lt;h3 id="1-add-a-small-set-of-purpose-built-endpoints"&gt;1) Add a small set of purpose-built endpoints&lt;/h3&gt;
&lt;p&gt;If your clients repeatedly need the same aggregation, don’t force them to stitch it together manually.&lt;/p&gt;
&lt;p&gt;Instead of requiring multiple calls, offer a dedicated projection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /users/:id/summary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/:id/permissions&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /users/:id/activity?from=...&amp;amp;to=...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn’t reinventing GraphQL. It’s acknowledging that clients want use-case-driven payloads.&lt;/p&gt;
&lt;h3 id="2-use-pagination-and-filtering-consistently"&gt;2) Use pagination and filtering consistently&lt;/h3&gt;
&lt;p&gt;A lot of REST pain comes from inconsistent list behavior.&lt;/p&gt;
&lt;p&gt;Pick a pagination strategy and stick to it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;limit/offset&lt;/code&gt; for simplicity, or&lt;/li&gt;
&lt;li&gt;cursor-based pagination when datasets are large and change often.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then enforce:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;predictable query parameter names&lt;/li&gt;
&lt;li&gt;clear semantics for filters and sorting&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-make-resource-identity-cache-friendly"&gt;3) Make resource identity cache-friendly&lt;/h3&gt;
&lt;p&gt;Resources should have stable URIs. If the “resource” is the shape itself (like a query result), think twice—caching becomes fragile. Prefer caching stable entities and letting the server compute derived representations when necessary.&lt;/p&gt;
&lt;h3 id="4-improve-client-performance-with-batching-not-query-language"&gt;4) Improve client performance with batching, not query language&lt;/h3&gt;
&lt;p&gt;If you’re fighting “N+1 requests,” consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;server-side composition endpoints (as above)&lt;/li&gt;
&lt;li&gt;request batching at the transport layer&lt;/li&gt;
&lt;li&gt;background prefetch patterns in clients&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can get the performance wins without bringing in a whole query ecosystem.&lt;/p&gt;
&lt;h3 id="5-tighten-error-contracts"&gt;5) Tighten error contracts&lt;/h3&gt;
&lt;p&gt;Define a consistent error shape, with machine-readable codes and actionable details. Make your clients resilient. When errors are predictable, debugging becomes routine rather than heroic.&lt;/p&gt;
&lt;p&gt;Example error payload:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;VALIDATION_FAILED&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;message&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Email format is invalid&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;details&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &lt;span style="color:#f92672"&gt;&amp;#34;field&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;email&amp;#34;&lt;/span&gt;, &lt;span style="color:#f92672"&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;must match RFC 5322 format&amp;#34;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s what developers actually need—not a new language.&lt;/p&gt;
&lt;h2 id="a-practical-decision-framework-keep-rest-unless-you-hit-these-walls"&gt;A practical decision framework: keep REST unless you hit these walls&lt;/h2&gt;
&lt;p&gt;Here’s a simple gut-check you can run with your team:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are multiple distinct clients requesting different projections of the same nested graphs &lt;em&gt;often&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Do you frequently see front-end workarounds caused by over-fetching/under-fetching?&lt;/li&gt;
&lt;li&gt;Are you willing to invest in GraphQL-specific infrastructure: schema governance, resolver performance monitoring, query complexity limits, and developer experience?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the answer to the first two is “not really,” you’re probably not buying the right tool. If the third is a hard “no,” then GraphQL is not a small decision—it’s a platform decision.&lt;/p&gt;
&lt;p&gt;Most teams should start by upgrading their REST design and consistency. You’ll get better outcomes sooner, with fewer hidden costs.&lt;/p&gt;
&lt;h2 id="conclusion-graphql-is-a-tool-not-a-default"&gt;Conclusion: GraphQL is a tool, not a default&lt;/h2&gt;
&lt;p&gt;GraphQL is an incredible technology for specific problems: deeply nested data, complex object graphs, and multiple clients with divergent needs. For the rest, REST—when designed with care—remains the most practical choice: better caching behavior, clearer error handling, and less debugging friction. If your REST API is “fine,” stop fixing what isn’t broken. Invest that effort into endpoints and contracts your team can understand, test, and operate confidently.&lt;/p&gt;</content></item><item><title>Rust Is the Language Your Future Self Will Thank You For</title><link>https://decastro.work/blog/rust-language-future-self-thank-you/</link><pubDate>Tue, 22 Feb 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/rust-language-future-self-thank-you/</guid><description>&lt;p&gt;You can write buggy software in any language—but you can only &lt;em&gt;prevent&lt;/em&gt; certain classes of bugs when your tools actively refuse to let bad code exist. Rust is exactly that kind of language. The hype is real, yes, but the deeper story is more practical: memory safety without garbage collection, enforced by the compiler, with a learning curve that—once you conquer it—changes how you think about correctness forever.&lt;/p&gt;
&lt;h2 id="the-loved-part-is-catching-up-to-the-used-part"&gt;The “loved” part is catching up to the “used” part&lt;/h2&gt;
&lt;p&gt;It’s easy to dismiss “most loved language” lists as popularity contests. But language love has a habit of turning into adoption once the pain becomes undeniable and the wins become consistent.&lt;/p&gt;</description><content>&lt;p&gt;You can write buggy software in any language—but you can only &lt;em&gt;prevent&lt;/em&gt; certain classes of bugs when your tools actively refuse to let bad code exist. Rust is exactly that kind of language. The hype is real, yes, but the deeper story is more practical: memory safety without garbage collection, enforced by the compiler, with a learning curve that—once you conquer it—changes how you think about correctness forever.&lt;/p&gt;
&lt;h2 id="the-loved-part-is-catching-up-to-the-used-part"&gt;The “loved” part is catching up to the “used” part&lt;/h2&gt;
&lt;p&gt;It’s easy to dismiss “most loved language” lists as popularity contests. But language love has a habit of turning into adoption once the pain becomes undeniable and the wins become consistent.&lt;/p&gt;
&lt;p&gt;Rust’s momentum isn’t just social. It’s structural. Developers don’t just like Rust; they feel the difference when they ship software:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fewer crashy edge cases during refactors&lt;/li&gt;
&lt;li&gt;Less time chasing “use-after-free” and “data race” ghosts&lt;/li&gt;
&lt;li&gt;A compiler that acts like a tireless code reviewer, even when you’re tired, rushed, or distracted&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, Rust converts admiration into engineering leverage. The gap between “this is awesome” and “we should use this” keeps shrinking because teams are increasingly seeing that the cost of Rust isn’t merely syntax or tooling—it’s a mindset shift toward making invalid programs unrepresentable.&lt;/p&gt;
&lt;h2 id="why-the-borrow-checker-feels-brutal-and-why-thats-the-point"&gt;Why the borrow checker feels brutal (and why that’s the point)&lt;/h2&gt;
&lt;p&gt;The borrow checker is the compiler’s way of answering a question that most languages leave to humans: &lt;em&gt;Who owns this memory, and who is allowed to access it?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In C and C++, you can write code that compiles while violating the rules of safe access. Those bugs can hide for years—until a particular timing window, optimization, or input triggers them. Unit tests might catch them occasionally, but tests are sampling, not proof.&lt;/p&gt;
&lt;p&gt;Rust attacks the root cause by enforcing rules about borrowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can have either &lt;strong&gt;one mutable reference&lt;/strong&gt; &lt;em&gt;or&lt;/em&gt; &lt;strong&gt;any number of immutable references&lt;/strong&gt;, but not both at the same time.&lt;/li&gt;
&lt;li&gt;References must not outlive the data they point to.&lt;/li&gt;
&lt;li&gt;Aliasing mutable state in ways that lead to races or invalid access is rejected early.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is the “brutal” experience you hear about: the compiler stops you from continuing until your intent is precise. That pressure is uncomfortable—because it’s replacing habits you didn’t realize you had. But it’s also why Rust tends to produce a distinct kind of confidence: not “I think it works,” but “this can’t go wrong in these specific ways.”&lt;/p&gt;
&lt;h3 id="a-quick-example-the-bug-rust-prevents"&gt;A quick example: the bug Rust prevents&lt;/h3&gt;
&lt;p&gt;Imagine you have a function that returns a reference to data owned by a local variable. In C++, it might compile and appear to work, until it doesn’t—classic dangling reference territory.&lt;/p&gt;
&lt;p&gt;In Rust, the compiler requires lifetimes to be coherent. If your reference would outlive the data, Rust refuses to compile the program. That’s not pedantry; it’s reliability.&lt;/p&gt;
&lt;h2 id="memory-safety-without-garbage-collection-isnt-marketingits-engineering"&gt;“Memory safety without garbage collection” isn’t marketing—it’s engineering&lt;/h2&gt;
&lt;p&gt;Garbage collection (GC) helps manage memory lifetime, but it comes with tradeoffs: pauses, runtime overhead, and sometimes design constraints. Rust takes a different approach: it uses &lt;strong&gt;ownership&lt;/strong&gt; and &lt;strong&gt;borrowing&lt;/strong&gt; to ensure memory safety at compile time, not runtime.&lt;/p&gt;
&lt;p&gt;Here’s the high-level model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each value has an &lt;strong&gt;owner&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When the owner goes out of scope, the value is dropped deterministically.&lt;/li&gt;
&lt;li&gt;Moving values transfers ownership safely.&lt;/li&gt;
&lt;li&gt;Borrowing creates references that Rust tracks with lifetimes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This doesn’t just prevent memory bugs. It changes your system design. You start to think in terms of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where state truly belongs&lt;/li&gt;
&lt;li&gt;How data should flow&lt;/li&gt;
&lt;li&gt;What should be shared (and how safely)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yes—this is why people often say they “can’t go back.” Once you’ve felt the compiler guard your invariants, writing code that can silently violate them starts to feel like borrowing trouble.&lt;/p&gt;
&lt;h2 id="the-c-to-rust-experience-same-power-fewer-landmines"&gt;The C++-to-Rust experience: same power, fewer landmines&lt;/h2&gt;
&lt;p&gt;Every C++ developer I’ve seen try Rust eventually reports the same arc:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Confusion: “Why is the compiler yelling at me?”&lt;/li&gt;
&lt;li&gt;Frustration: “Why does this need so many lifetimes?”&lt;/li&gt;
&lt;li&gt;Breakthrough: “Oh. The compiler is forcing me to state my intent.”&lt;/li&gt;
&lt;li&gt;Relief: “I’m no longer afraid to refactor this module.”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Rust doesn’t remove low-level control. It gives you control with guardrails. For teams migrating incrementally, that can be a big deal: you can adopt Rust for performance-critical components, security-sensitive services, or developer productivity hotspots without boiling the ocean.&lt;/p&gt;
&lt;p&gt;Practical migration strategies include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build a small Rust library for a well-defined task (parsing, indexing, crypto wrappers, data validation).&lt;/li&gt;
&lt;li&gt;Expose a stable API to the rest of the system.&lt;/li&gt;
&lt;li&gt;Let Rust own the tricky safety invariants internally.&lt;/li&gt;
&lt;li&gt;Gradually expand scope once the team trusts the workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust also interoperates with C and C++ through well-established approaches, so you’re not forced into “rewrite everything” thinking. The goal is to shrink the surface area of memory-unsafe code, not to torch your existing architecture.&lt;/p&gt;
&lt;h2 id="unit-tests-are-greatrust-makes-them-less-necessary-for-some-bugs"&gt;Unit tests are great—Rust makes them less necessary for some bugs&lt;/h2&gt;
&lt;p&gt;It’s tempting to treat Rust as “tests, but with fewer failures.” That’s wrong. Tests are still essential. What Rust does is eliminate entire categories of problems &lt;em&gt;before you run anything&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Consider the kinds of issues that often slip past unit tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use-after-free that only happens under a specific timing pattern&lt;/li&gt;
&lt;li&gt;Data races that appear only with thread scheduling variability&lt;/li&gt;
&lt;li&gt;Rare lifetime bugs that show up in production inputs&lt;/li&gt;
&lt;li&gt;Refactors that accidentally invalidate assumptions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust’s guarantees apply even when your tests are thin, your input space is incomplete, and your team is under pressure. That matters because real-world software rarely enjoys ideal test coverage.&lt;/p&gt;
&lt;p&gt;The real payoff is psychological and operational. When you refactor, you’re not just asking “Will tests pass?” You’re asking “Will the compiler accept the new ownership and borrowing relationships?” The compiler becomes a safety net that doesn’t depend on what you thought to test.&lt;/p&gt;
&lt;h2 id="learning-rust-how-to-avoid-getting-stuck-on-the-hardest-parts"&gt;Learning Rust: how to avoid getting stuck on the hardest parts&lt;/h2&gt;
&lt;p&gt;The borrow checker is real, and yes, it can take time. But you don’t need to “get everything” immediately. You need a path.&lt;/p&gt;
&lt;p&gt;Here are practical ways to learn without burning out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Start small and work outward.&lt;/strong&gt; Write simple functions, then gradually add references, structs, and collections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat the error messages like puzzles.&lt;/strong&gt; They’re not always perfect, but they often contain the exact rule you violated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer owned data first.&lt;/strong&gt; Use borrowing when you actually need it for performance or to avoid copies.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the compiler as a tutor.&lt;/strong&gt; Don’t fight it for hours—iterate, adjust, recompile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn the standard patterns.&lt;/strong&gt; Most Rust code you’ll write involves familiar shapes: &lt;code&gt;Option&lt;/code&gt;/&lt;code&gt;Result&lt;/code&gt;, iterator chains, structs with lifetimes where needed, and concurrency primitives that enforce safety.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One mindset shift helps more than any trick: when Rust complains, it’s not accusing you of being careless—it’s asking you to clarify your intent. If you respond by making ownership and access explicit, the language starts to feel less like a gatekeeper and more like a collaborator.&lt;/p&gt;
&lt;p&gt;And if you’re already comfortable with systems concepts—memory layout, pointers, lifetimes in principle—you’ll adapt faster. Rust rewards that mental model, just with stronger enforcement.&lt;/p&gt;
&lt;h2 id="conclusion-youre-not-choosing-a-languageyoure-choosing-fewer-regrets"&gt;Conclusion: you’re not choosing a language—you’re choosing fewer regrets&lt;/h2&gt;
&lt;p&gt;Rust is worth learning because it turns memory safety into a compile-time guarantee. It’s also worth learning because it changes how you structure software: you stop treating “correctness” as an afterthought and start treating it as something the toolchain should enforce.&lt;/p&gt;
&lt;p&gt;If you’re considering Rust, don’t ask whether you can afford the learning curve. Ask whether you can afford to keep building and maintaining code that lets entire classes of memory and concurrency bugs slip through until production proves you wrong. Your future self will notice the difference—every time you refactor without fear.&lt;/p&gt;</content></item><item><title>Why I Still Reach for Python When Speed Doesn't Matter</title><link>https://decastro.work/blog/why-i-still-reach-for-python-speed-doesnt-matter/</link><pubDate>Tue, 15 Feb 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/why-i-still-reach-for-python-speed-doesnt-matter/</guid><description>&lt;p&gt;Every time someone posts a benchmark screenshot, it turns into the same argument: “Python is slow.” Sure—sometimes it’s &lt;em&gt;very&lt;/em&gt; slow. But for most software I build and maintain, speed is not the constraint. Clarity is. Ecosystem depth is. The ability to prototype, iterate, test, and ship is what keeps projects from stalling. In that world, Python remains my default—then I optimize the few places that truly deserve it.&lt;/p&gt;
&lt;h2 id="pythons-real-reputation-slow-where-it-matters-least"&gt;Python’s real reputation: slow &lt;em&gt;where it matters least&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Let’s get something out of the way: Python can be slower than compiled languages for CPU-bound workloads. That’s not a myth; it’s a design trade-off. Python emphasizes expressiveness and a rich runtime over raw execution speed.&lt;/p&gt;</description><content>&lt;p&gt;Every time someone posts a benchmark screenshot, it turns into the same argument: “Python is slow.” Sure—sometimes it’s &lt;em&gt;very&lt;/em&gt; slow. But for most software I build and maintain, speed is not the constraint. Clarity is. Ecosystem depth is. The ability to prototype, iterate, test, and ship is what keeps projects from stalling. In that world, Python remains my default—then I optimize the few places that truly deserve it.&lt;/p&gt;
&lt;h2 id="pythons-real-reputation-slow-where-it-matters-least"&gt;Python’s real reputation: slow &lt;em&gt;where it matters least&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;Let’s get something out of the way: Python can be slower than compiled languages for CPU-bound workloads. That’s not a myth; it’s a design trade-off. Python emphasizes expressiveness and a rich runtime over raw execution speed.&lt;/p&gt;
&lt;p&gt;But the “Python is slow” argument often collapses the real question into a single metric. Your application rarely spends its entire time in Python bytecode. It might wait on a database, call an external API, stream data over the network, block on I/O, or spend most of its life in glue code—moving information between systems.&lt;/p&gt;
&lt;p&gt;Consider a common internal tool: a small service that ingests customer events, normalizes them, writes rows to a warehouse, and triggers alerts. The hot path isn’t “how fast can Python add integers?” The hot path is “how quickly can we understand the data, handle edge cases, add retries, and ship the feature without breaking production?”&lt;/p&gt;
&lt;p&gt;In that scenario, Python isn’t the bottleneck—because your biggest delays are human and architectural: unclear requirements, missing instrumentation, brittle integration logic, and slow feedback loops.&lt;/p&gt;
&lt;h2 id="readability-is-a-performance-feature-and-python-wins-that-battle"&gt;Readability is a performance feature (and Python wins that battle)&lt;/h2&gt;
&lt;p&gt;Speed isn’t just CPU cycles. It’s also the time it takes humans to understand and safely change code. Python’s biggest advantage for most teams is that it reads like an executable explanation.&lt;/p&gt;
&lt;p&gt;A few practical examples I’ve seen play out repeatedly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data transformations&lt;/strong&gt;: If you’re mapping, filtering, grouping, and reshaping data (pandas-style patterns), Python makes the intent obvious. Colleagues can review logic quickly and catch bugs earlier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automation and orchestration&lt;/strong&gt;: Scripting the workflow—fetching data, running jobs, reporting results—becomes straightforward. The code communicates “what happens” more than “how to fight the compiler.”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API handlers and glue logic&lt;/strong&gt;: The messy parts of software—validation, serialization, retries, error messages—are where clarity prevents outages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When readability improves, you get compounding benefits: fewer regressions, faster onboarding, and less time spent debugging “what this code is trying to do.” That’s real performance, measured in engineering time and reliability—not in microseconds.&lt;/p&gt;
&lt;p&gt;If you want a rule of thumb: if the code is hard to read, it will be hard to optimize later. Python’s readability keeps the path open for incremental improvements instead of forcing rewrites.&lt;/p&gt;
&lt;h2 id="the-ecosystem-depth-is-the-real-reason-python-keeps-winning"&gt;The ecosystem depth is the real reason Python keeps winning&lt;/h2&gt;
&lt;p&gt;The second reason I reach for Python is the ecosystem. Python isn’t just a language—it’s a collection of libraries that solve high-leverage problems without you having to become an expert in everything at once.&lt;/p&gt;
&lt;p&gt;When you’re building software in these categories, Python gives you an enormous head start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data pipelines and ETL&lt;/strong&gt;: You can move and transform data quickly with mature tools, and you can integrate with warehouses and streaming systems without wrestling too much boilerplate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scientific computing and data analysis&lt;/strong&gt;: If your project involves modeling, simulation, or analytics, Python’s package ecosystem is simply deep.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automation and DevOps-adjacent tooling&lt;/strong&gt;: From scripting to workflow orchestration, Python tends to be the “glue language” that keeps teams moving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APIs and backend services&lt;/strong&gt;: Modern frameworks make it easy to build consistent interfaces, validate inputs, and add observability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practice, the “time-to-value” advantage dominates. Suppose you’re building a service that predicts something based on historical features. Even if you &lt;em&gt;could&lt;/em&gt; write the math in C++ or Rust, you’ll still need data access, feature engineering, evaluation, logging, and iteration. Python tends to be where that work gets done fastest—and the model quality improves faster because you can experiment without friction.&lt;/p&gt;
&lt;p&gt;And yes: production-grade Python is common. You can scale it with the right architecture: background workers, horizontal scaling, caching, and careful I/O. Raw interpreter speed is only one variable in the system.&lt;/p&gt;
&lt;h2 id="time-to-prototype-beats-theoretical-throughput-for-most-work"&gt;Time-to-prototype beats theoretical throughput for most work&lt;/h2&gt;
&lt;p&gt;Most projects don’t fail because they’re “too slow.” They fail because they’re unclear, incomplete, or wrong.&lt;/p&gt;
&lt;p&gt;Python accelerates the most important early phase: turning ideas into working software that can be tested against reality. That includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;building a minimal end-to-end pipeline,&lt;/li&gt;
&lt;li&gt;validating inputs and assumptions,&lt;/li&gt;
&lt;li&gt;writing tests for behavior (not just for code paths),&lt;/li&gt;
&lt;li&gt;and getting feedback from users or stakeholders.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A concrete example: imagine you’re tasked with building a data pipeline that ingests logs, extracts fields, and populates a dashboard. The first iteration will probably be wrong. You’ll discover missing fields, inconsistent formats, and “special cases” that only show up in production data.&lt;/p&gt;
&lt;p&gt;If your prototype takes weeks to stand up because you chose a low-level approach, you’ll spend those weeks discovering issues more slowly. If your prototype is in Python and working in days, you can identify the real bottlenecks and refine the solution while everyone still has momentum.&lt;/p&gt;
&lt;p&gt;That velocity matters so much that I treat it as a primary performance metric. In most teams, the time saved in iteration cycles and debugging outweighs any initial execution overhead.&lt;/p&gt;
&lt;h2 id="optimize-like-a-professional-measure-isolate-rewrite-selectively"&gt;Optimize like a professional: measure, isolate, rewrite selectively&lt;/h2&gt;
&lt;p&gt;The common compromise I recommend is simple and disciplined:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Write in Python first.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Profile the actual workload.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identify the small fraction that truly costs time.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rewrite only that portion in something faster—if needed.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This approach avoids the trap of premature optimization, where you “speed up” code that doesn’t matter while the real problems—data quality, correctness, and integration—remain unaddressed.&lt;/p&gt;
&lt;p&gt;Here are practical tactics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use profiling tools to find hot spots&lt;/strong&gt;, not guesses. You’re looking for the specific functions and loops that consume the majority of runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separate compute from I/O&lt;/strong&gt;. If most time is waiting on the network or database, speeding up Python won’t help much; you’ll want better batching, caching, query tuning, or async I/O.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move inner loops to vectorized or compiled paths&lt;/strong&gt; where it fits the problem. Often you don’t need to rewrite the whole system; you just need to avoid Python-level iteration on large arrays.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Introduce caching at the edges&lt;/strong&gt;. Many systems are slow because they recompute the same results repeatedly, not because arithmetic is expensive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the interface stable&lt;/strong&gt; when rewriting. If you encapsulate a performance-critical function behind a narrow boundary, you can swap implementations without turning your project into a rewrite.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A pattern I’ve used successfully: keep business logic and orchestration in Python, but let numeric kernels live in optimized libraries (which may be written in C/C++ under the hood) or in compiled extensions when absolutely necessary. The result is usually a system that stays readable while still meeting performance requirements.&lt;/p&gt;
&lt;h2 id="when-python-isnt-the-right-tool-and-what-to-do-instead"&gt;When Python &lt;em&gt;isn’t&lt;/em&gt; the right tool (and what to do instead)&lt;/h2&gt;
&lt;p&gt;To be clear, there are cases where Python shouldn’t be your first choice—particularly when you have strict, predictable CPU-bound requirements and you know exactly where the time goes.&lt;/p&gt;
&lt;p&gt;If you’re building a latency-sensitive service with tight budgets and high request rates, you may prefer a language with deterministic performance characteristics from the start. Or you may still build in Python but design the architecture so the low-latency portion is handled by a faster component, while Python remains the orchestration layer.&lt;/p&gt;
&lt;p&gt;The key is not dogma. It’s architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Python where productivity matters most: pipelines, APIs, integrations, and control-plane logic.&lt;/li&gt;
&lt;li&gt;Use other languages or optimized runtimes where the bottleneck is truly compute-heavy and measurable.&lt;/li&gt;
&lt;li&gt;Treat “performance” as a system property, not a language brag.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words: Python is my default, not my religion.&lt;/p&gt;
&lt;h2 id="conclusion-python-optimizes-the-work-you-cant-benchmark"&gt;Conclusion: Python optimizes the work you can’t benchmark&lt;/h2&gt;
&lt;p&gt;Python’s weakness in raw execution speed is real. But in most software, speed is not the deciding factor. Readability reduces risk. The ecosystem reduces time-to-value. And selective optimization ensures you only pay complexity costs where they actually buy results.&lt;/p&gt;
&lt;p&gt;So I’ll keep reaching for Python—not because it’s the fastest option, but because it’s the fastest path to correct, maintainable software that can evolve. Then, when performance truly matters, I’ll profile first and rewrite the 5% that earns it.&lt;/p&gt;</content></item><item><title>The Absolute State of CSS in 2022</title><link>https://decastro.work/blog/absolute-state-of-css-2022/</link><pubDate>Thu, 10 Feb 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/absolute-state-of-css-2022/</guid><description>&lt;p&gt;For years, CSS felt like the language of “just make it line up.” Meanwhile, JavaScript got all the headlines: frameworks, state management, and the never-ending debate about what “the best way” is. But in 2022, CSS stopped being background music and started dropping feature-level power. Container queries, cascade layers, and &lt;code&gt;:has()&lt;/code&gt; didn’t just polish the surface—they rewired how you design responsive, predictable interfaces without reaching for hacks or heavy scripts.&lt;/p&gt;</description><content>&lt;p&gt;For years, CSS felt like the language of “just make it line up.” Meanwhile, JavaScript got all the headlines: frameworks, state management, and the never-ending debate about what “the best way” is. But in 2022, CSS stopped being background music and started dropping feature-level power. Container queries, cascade layers, and &lt;code&gt;:has()&lt;/code&gt; didn’t just polish the surface—they rewired how you design responsive, predictable interfaces without reaching for hacks or heavy scripts.&lt;/p&gt;
&lt;p&gt;If you write UI, you should care. Not someday. Now.&lt;/p&gt;
&lt;h2 id="container-queries-stop-punishing-the-viewport"&gt;Container Queries: Stop Punishing the Viewport&lt;/h2&gt;
&lt;p&gt;For as long as responsive design has existed, CSS has relied on the viewport as the source of truth. That’s an awkward choice: many components don’t live at the top level. They live in sidebars, cards, modals, tool panes, and grids—containers whose size may have nothing to do with the viewport’s width.&lt;/p&gt;
&lt;p&gt;Container queries fix that.&lt;/p&gt;
&lt;h3 id="the-old-way-viewport-based"&gt;The old way (viewport-based)&lt;/h3&gt;
&lt;p&gt;You might write:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1.25&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;max-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;600px&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This implicitly assumes your card collapses when the &lt;em&gt;viewport&lt;/em&gt; does. In real layouts, it’s the container that matters.&lt;/p&gt;
&lt;h3 id="the-modern-way-parent-based"&gt;The modern way (parent-based)&lt;/h3&gt;
&lt;p&gt;With container queries, you define a container and then query its dimensions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;container-type&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;inline&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;size&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1.25&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;container&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;max-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;420px&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-size&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the title scales based on the card’s available width—whether that card is in a wide layout, a narrow column, or a collapsed drawer.&lt;/p&gt;
&lt;h3 id="practical-advice"&gt;Practical advice&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;container-type: inline-size&lt;/code&gt; for width-driven components (most UI layout depends on inline size).&lt;/li&gt;
&lt;li&gt;Name your containers conceptually. Even if you don’t have container names everywhere, keep your mental model clean: “This component responds to its own space.”&lt;/li&gt;
&lt;li&gt;Replace brittle viewport media queries that target component internals. If your CSS targets &lt;code&gt;.sidebar .widget&lt;/code&gt; differently depending on where it’s rendered, container queries are usually the correct escape hatch.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="cascade-layers-make-specificity-boring-again"&gt;Cascade Layers: Make Specificity Boring Again&lt;/h2&gt;
&lt;p&gt;If you’ve ever said, “Why is this CSS not applying?” you already understand the pain cascade layers are designed to eliminate.&lt;/p&gt;
&lt;p&gt;Traditional CSS specificity can turn the stylesheet into a battlefield:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Later rules win when specificity ties.&lt;/li&gt;
&lt;li&gt;Higher specificity silently overrides lower specificity.&lt;/li&gt;
&lt;li&gt;“Quick fixes” accumulate until the cascade becomes unmaintainable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cascade layers let you explicitly order groups of styles, so you stop playing whack-a-mole with specificity.&lt;/p&gt;
&lt;h3 id="think-of-layers-as-priority-buckets"&gt;Think of layers as “priority buckets”&lt;/h3&gt;
&lt;p&gt;A clean pattern is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Base styles&lt;/li&gt;
&lt;li&gt;Component styles&lt;/li&gt;
&lt;li&gt;Overrides (if you must)&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;base&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt; &lt;span style="color:#f92672"&gt;components&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt; &lt;span style="color:#f92672"&gt;overrides&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;base&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;button&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;inherit&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;components&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;primary&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;background&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;#2563eb&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;color&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;white&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;overrides&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;/* Intentionally last-resort */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;primary&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;background&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;#1d4ed8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Even if &lt;code&gt;.primary&lt;/code&gt; appears in multiple places, the layer order defines the outcome—without turning everything into &lt;code&gt;!important&lt;/code&gt; or specificity gymnastics.&lt;/p&gt;
&lt;h3 id="practical-advice-1"&gt;Practical advice&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Put your framework-ish styles into one layer and your app overrides into another. You want the override layer to always beat the base layer, regardless of selector complexity.&lt;/li&gt;
&lt;li&gt;Use layers to control “CSS ownership.” For teams, this is huge: a component author can ship styles without fear that random app-level selectors will accidentally bulldoze them.&lt;/li&gt;
&lt;li&gt;Don’t treat layers as a magic wand. If everything goes into one layer, you’ve gained little. The value is in partitioning.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="has--the-parent-selector-you-always-wanted"&gt;&lt;code&gt;:has()&lt;/code&gt; : The Parent Selector You Always Wanted&lt;/h2&gt;
&lt;p&gt;&lt;code&gt; :has()&lt;/code&gt; is the most developer-empathic feature in recent CSS memory. It lets you style an element based on its descendants—&lt;em&gt;and&lt;/em&gt; it does so without JavaScript.&lt;/p&gt;
&lt;p&gt;Yes, it’s effectively a parent selector.&lt;/p&gt;
&lt;h3 id="the-old-way-javascript-or-extra-markup"&gt;The old way (JavaScript or extra markup)&lt;/h3&gt;
&lt;p&gt;Common pattern: “If a wrapper contains something that matches X, change the wrapper’s style.”&lt;/p&gt;
&lt;p&gt;With JS you might:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query the DOM&lt;/li&gt;
&lt;li&gt;Check conditions&lt;/li&gt;
&lt;li&gt;Toggle classes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Or you might add “state” classes manually.&lt;/p&gt;
&lt;h3 id="the-css-way"&gt;The CSS way&lt;/h3&gt;
&lt;p&gt;Now you can write:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;has&lt;/span&gt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;status-badge&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;padding-top&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1.5&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;input-group&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;has&lt;/span&gt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;input&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;focus&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;border-color&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;#2563eb&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a big deal because the UI state is already present in the DOM; &lt;code&gt;:has()&lt;/code&gt; lets CSS react to it.&lt;/p&gt;
&lt;h3 id="practical-advice-2"&gt;Practical advice&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;:has()&lt;/code&gt; to reduce “state class” boilerplate. Your markup already expresses the relationship—let CSS leverage it.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;:has()&lt;/code&gt; for styling changes, not for heavy behavioral logic. CSS isn’t a state machine; it’s a renderer. Keep it focused.&lt;/li&gt;
&lt;li&gt;Be mindful of performance in deeply nested or frequently changing DOMs. If you’re using it on large lists with constant mutation, measure. In most typical interface patterns, it’s a clean win.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="together-they-change-how-you-build-components"&gt;Together, They Change How You Build Components&lt;/h2&gt;
&lt;p&gt;Individually, these features are excellent. Together, they feel like a tectonic shift in component design philosophy.&lt;/p&gt;
&lt;p&gt;Consider a common UI card:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It should adjust internal layout based on its own width.&lt;/li&gt;
&lt;li&gt;It should style itself differently depending on whether it contains certain elements.&lt;/li&gt;
&lt;li&gt;It should not fight with global styles or third-party CSS overrides.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Viewport media queries determine structure.&lt;/li&gt;
&lt;li&gt;JS toggles classes to represent internal conditions.&lt;/li&gt;
&lt;li&gt;Specificity wars decide what wins.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Container queries handle sizing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:has()&lt;/code&gt; handles structural conditions.&lt;/li&gt;
&lt;li&gt;Cascade layers handle stylesheet priority.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-cohesive-example"&gt;A cohesive example&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;base&lt;/span&gt;&lt;span style="color:#f92672"&gt;,&lt;/span&gt; &lt;span style="color:#f92672"&gt;components&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;layer&lt;/span&gt; &lt;span style="color:#f92672"&gt;components&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;container-type&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;inline&lt;/span&gt;&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;size&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;padding&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;has&lt;/span&gt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;warning&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;border-color&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;#f59e0b&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt;:&lt;span style="color:#a6e22e"&gt;has&lt;/span&gt;&lt;span style="color:#f92672"&gt;(&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;warning&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;font-weight&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;700&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @&lt;span style="color:#66d9ef"&gt;container&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;max-width&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;420px&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;card&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;meta&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;display&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;none&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No viewport assumptions. No manual state classes. No “why is my selector losing?” drama. Just declarative rules that match how the component behaves.&lt;/p&gt;
&lt;h2 id="stop-sleeping-on-native-css-replace-javascript-with-rendering-logic"&gt;Stop Sleeping on Native CSS: Replace JavaScript With Rendering Logic&lt;/h2&gt;
&lt;p&gt;Here’s the uncomfortable truth: a lot of JavaScript UI logic exists because CSS couldn’t express certain relationships. With these features, some of that logic can move back to CSS, where it belongs.&lt;/p&gt;
&lt;h3 id="where-css-shines-now"&gt;Where CSS shines now&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Layout adaptation based on parent sizing (container queries)&lt;/li&gt;
&lt;li&gt;Conditional styling based on DOM structure (&lt;code&gt;:has()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Predictable styling order across teams and dependencies (cascade layers)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="where-you-still-need-javascript"&gt;Where you still need JavaScript&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Real event-driven behavior (timers, network calls, complex interactions)&lt;/li&gt;
&lt;li&gt;State that isn’t representable in the DOM structure (or isn’t stable enough)&lt;/li&gt;
&lt;li&gt;Animations that require imperative control, not just style transitions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="a-practical-migration-mindset"&gt;A practical migration mindset&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Identify one UI component with:
&lt;ul&gt;
&lt;li&gt;fragile media queries,&lt;/li&gt;
&lt;li&gt;class toggling based on DOM content,&lt;/li&gt;
&lt;li&gt;and “override by selector power” problems.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Replace viewport rules with container queries.&lt;/li&gt;
&lt;li&gt;Replace “if it contains X, add class” logic with &lt;code&gt;:has()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Put component styles into a dedicated cascade layer and reserve an override layer for app-level exceptions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do this incrementally. You don’t need to rewrite the entire frontend in a weekend. You just need to stop accepting that “CSS can’t do that” as a permanent limitation.&lt;/p&gt;
&lt;h2 id="conclusion-css-is-finally-playing-the-same-game"&gt;Conclusion: CSS Is Finally Playing the Same Game&lt;/h2&gt;
&lt;p&gt;2022 wasn’t just “nice-to-have” CSS. It was structural progress: container queries let components reason about their real space, cascade layers let styles stop fighting for dominance, and &lt;code&gt;:has()&lt;/code&gt; gives CSS the missing parent-level awareness that developers have wanted for years.&lt;/p&gt;
&lt;p&gt;If you’re still sprinkling JavaScript for styling conditions, betting your layout on viewport breakpoints, or relying on specificity tricks, you’re paying a tax that CSS no longer demands. Native CSS isn’t catching up—it’s already capable of a lot of what your frontend codebase does today. The real win is simple: let CSS render, and let JavaScript handle what only JavaScript can do.&lt;/p&gt;</content></item><item><title>TypeScript Is Not Optional Anymore</title><link>https://decastro.work/blog/typescript-is-not-optional-anymore/</link><pubDate>Tue, 01 Feb 2022 00:00:00 +0000</pubDate><author>abdecastro@protonmail.com (Antonio B De Castro)</author><guid>https://decastro.work/blog/typescript-is-not-optional-anymore/</guid><description>&lt;p&gt;If you’re starting a new JavaScript project in 2022 without TypeScript, you’re not being “flexible.” You’re deliberately buying a future problem with interest. TypeScript has moved from preference to infrastructure: it’s how modern teams prevent avoidable bugs, document intent, and scale codebases without turning every refactor into a gamble.&lt;/p&gt;
&lt;h2 id="javascript-without-types-is-a-design-decisionnot-a-default"&gt;JavaScript Without Types Is a Design Decision—Not a Default&lt;/h2&gt;
&lt;p&gt;Vanilla JavaScript isn’t “wrong.” It’s just ambiguous by default. When everything is dynamically typed, the language cannot help you catch mistakes that are obvious to humans: passing the wrong shape of data, calling a function with the wrong arguments, forgetting a field that another part of the app expects, or misunderstanding what a value can be.&lt;/p&gt;</description><content>&lt;p&gt;If you’re starting a new JavaScript project in 2022 without TypeScript, you’re not being “flexible.” You’re deliberately buying a future problem with interest. TypeScript has moved from preference to infrastructure: it’s how modern teams prevent avoidable bugs, document intent, and scale codebases without turning every refactor into a gamble.&lt;/p&gt;
&lt;h2 id="javascript-without-types-is-a-design-decisionnot-a-default"&gt;JavaScript Without Types Is a Design Decision—Not a Default&lt;/h2&gt;
&lt;p&gt;Vanilla JavaScript isn’t “wrong.” It’s just ambiguous by default. When everything is dynamically typed, the language cannot help you catch mistakes that are obvious to humans: passing the wrong shape of data, calling a function with the wrong arguments, forgetting a field that another part of the app expects, or misunderstanding what a value can be.&lt;/p&gt;
&lt;p&gt;That ambiguity might feel productive at the start. The first week is fast because you’re not fighting the compiler. But the cost shows up later when the project grows beyond a small script. The code becomes self-contradictory: naming suggests one contract, runtime reality enforces another. You end up relying on tests, runtime checks, and tribal knowledge to enforce what the type system could have enforced immediately.&lt;/p&gt;
&lt;p&gt;My position is simple: if you’re building anything that will change—features, APIs, UI flows, integrations—then skipping types is not neutral. It’s a design decision that knowingly increases risk.&lt;/p&gt;
&lt;h2 id="framework-support-isnt-the-pointtooling-feedback-is"&gt;“Framework Support” Isn’t the Point—Tooling Feedback Is&lt;/h2&gt;
&lt;p&gt;Yes, most major frameworks now provide strong TypeScript support. That matters, but it’s not the whole story. The real advantage is feedback speed.&lt;/p&gt;
&lt;p&gt;TypeScript turns the editor into a co-pilot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Autocomplete becomes reliable because the IDE understands your data shapes.&lt;/li&gt;
&lt;li&gt;Refactors become safer because renames and signature changes propagate through the codebase.&lt;/li&gt;
&lt;li&gt;You can model domain concepts directly: &lt;code&gt;UserId&lt;/code&gt;, &lt;code&gt;OrderStatus&lt;/code&gt;, &lt;code&gt;Email&lt;/code&gt;, &lt;code&gt;Money&lt;/code&gt;—not just strings that “probably” mean the right thing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Consider a typical bug pattern in JavaScript:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// JavaScript
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;formatPrice&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`$&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toFixed&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/api/order&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;res&lt;/span&gt; =&amp;gt; &lt;span style="color:#a6e22e"&gt;res&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;then&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;order&lt;/span&gt; =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// order.total might be a string, or null, or missing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; document.&lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;textContent&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;formatPrice&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;order&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;total&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This can fail at runtime in multiple ways—too late, often in production, and usually with vague error messages. With TypeScript you can encode the contract:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Order&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;total&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;formatPrice&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;number&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`$&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;price&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;toFixed&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;)&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// If order.total can be null or a string, TypeScript forces you to handle it.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the compiler becomes your first line of defense. That’s not “extra.” It’s a measurable reduction in avoidable bugs because you catch mismatches before you ship.&lt;/p&gt;
&lt;h2 id="typescript-changes-how-you-think-about-apis"&gt;TypeScript Changes How You Think About APIs&lt;/h2&gt;
&lt;p&gt;A major reason TypeScript is worth it: it forces you to define interfaces. In other words, it makes your contracts explicit.&lt;/p&gt;
&lt;p&gt;In JavaScript, API shapes often emerge indirectly: you infer them from usage. That works until it doesn’t. When multiple parts of the system (frontend, backend, worker processes) evolve independently, the “shape” of a thing becomes a rumor.&lt;/p&gt;
&lt;p&gt;TypeScript helps you replace rumors with definitions. For example, if you have an API response for users:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;type&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;email&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;displayName?&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;; &lt;span style="color:#75715e"&gt;// optional means not always present
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That single type becomes a shared source of truth. When someone adds &lt;code&gt;displayName&lt;/code&gt;, removes it, or changes semantics, you’ll see the impact immediately. Even better: your IDE can guide developers to the right usage patterns because the type tells you what’s safe.&lt;/p&gt;
&lt;p&gt;And yes, you still need runtime validation at boundaries (APIs, user input). TypeScript doesn’t eliminate runtime concerns; it prevents most internal confusion before it gets to the boundary. You stop treating “undefined behavior” as normal.&lt;/p&gt;
&lt;h2 id="the-real-cost-refactoring-without-confidence"&gt;The Real Cost: Refactoring Without Confidence&lt;/h2&gt;
&lt;p&gt;Every team eventually hits the same wall: you want to refactor, but you can’t trust the codebase. The lack of types doesn’t just allow bugs—it blocks velocity.&lt;/p&gt;
&lt;p&gt;Here’s what that looks like in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Renaming a property becomes risky because there’s no reliable way to find all usages.&lt;/li&gt;
&lt;li&gt;Changing a function signature leads to silent breakage when call sites aren’t updated correctly.&lt;/li&gt;
&lt;li&gt;“We’ll fix it when it breaks” becomes the workflow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TypeScript doesn’t magically make refactors perfect, but it makes them &lt;em&gt;searchable&lt;/em&gt;. The compiler acts like a map of your code’s expectations. If your refactor changes a contract, TypeScript tells you where that contract is assumed.&lt;/p&gt;
&lt;p&gt;In a larger system, that means fewer late surprises and less time spent hunting issues that should have been caught at development time. The payoff is compounding: the more you type, the easier it becomes to safely evolve the code.&lt;/p&gt;
&lt;h2 id="counterargument-were-too-busy-to-add-typescript"&gt;Counterargument: “We’re Too Busy to Add TypeScript”&lt;/h2&gt;
&lt;p&gt;This is the most common pushback, and it sounds reasonable until you compare it to the cost of not doing it.&lt;/p&gt;
&lt;p&gt;Adding TypeScript is usually incremental. You don’t need to “rewrite everything” before you benefit. In many projects, you can start by:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Converting the most critical modules first (data fetching, domain logic, API clients).&lt;/li&gt;
&lt;li&gt;Enabling strictness gradually (or starting with a moderate level, then tightening over time).&lt;/li&gt;
&lt;li&gt;Using JSDoc types temporarily in legacy files to establish contracts while you migrate.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You also don’t have to aim for perfection overnight. The goal is to start building type-aware confidence in the areas that change most frequently.&lt;/p&gt;
&lt;p&gt;More importantly: if you’re “too busy” to add TypeScript to a new project, you’re also too busy to maintain a codebase where bugs are discovered later and refactors take longer. TypeScript is not a luxury feature—it’s an accelerant that reduces the time you spend cleaning up avoidable problems.&lt;/p&gt;
&lt;h2 id="practical-rule-type-the-edges-then-type-the-core"&gt;Practical Rule: Type the Edges, Then Type the Core&lt;/h2&gt;
&lt;p&gt;The easiest way to get value without boiling the ocean is to apply a simple strategy:&lt;/p&gt;
&lt;h3 id="type-the-edges"&gt;Type the edges&lt;/h3&gt;
&lt;p&gt;Anything entering or leaving your system is a boundary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP requests and responses&lt;/li&gt;
&lt;li&gt;WebSocket messages&lt;/li&gt;
&lt;li&gt;Worker events&lt;/li&gt;
&lt;li&gt;Form inputs and user-generated content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Define the types for those payloads. Then enforce them at runtime if needed. This prevents the “undefined becomes a string becomes a crash” class of bugs.&lt;/p&gt;
&lt;h3 id="type-the-core"&gt;Type the core&lt;/h3&gt;
&lt;p&gt;After the edges, focus on the code that represents your domain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;business rules&lt;/li&gt;
&lt;li&gt;calculations&lt;/li&gt;
&lt;li&gt;state transitions&lt;/li&gt;
&lt;li&gt;permission checks&lt;/li&gt;
&lt;li&gt;mapping from API data to UI models&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When your core is typed, the rest of the system becomes easier to understand and harder to misuse. That’s where TypeScript pays off most.&lt;/p&gt;
&lt;p&gt;A small example: if you have a reducer managing UI state, types make illegal states unrepresentable. In JavaScript, “impossible” states still happen constantly because nothing stops them. In TypeScript, you can force the code to reflect reality—because the compiler won’t let you lie about it.&lt;/p&gt;
&lt;h2 id="conclusion-new-projects-shouldnt-start-with-hidden-risk"&gt;Conclusion: New Projects Shouldn’t Start With Hidden Risk&lt;/h2&gt;
&lt;p&gt;TypeScript isn’t optional anymore because modern development isn’t just about running code—it’s about building systems that evolve. If you start a new JavaScript project without TypeScript, you’re accepting a workflow where correctness depends on memory, tests, and luck.&lt;/p&gt;
&lt;p&gt;TypeScript gives you earlier feedback, clearer contracts, safer refactors, and a codebase that scales without becoming haunted. If you’re building in 2022—and you want to move fast without breaking things—TypeScript isn’t an add-on.&lt;/p&gt;
&lt;p&gt;It’s the baseline.&lt;/p&gt;</content></item></channel></rss>