<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chris Roy</title>
    <description>The latest articles on DEV Community by Chris Roy (@thechrisin).</description>
    <link>https://dev.to/thechrisin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F999241%2F1d6b30f0-a4b0-4fab-8289-7d8c5c32abb0.png</url>
      <title>DEV Community: Chris Roy</title>
      <link>https://dev.to/thechrisin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thechrisin"/>
    <language>en</language>
    <item>
      <title>Why Verification Asks You to Leave the Language You Ship</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:53:58 +0000</pubDate>
      <link>https://dev.to/thechrisin/why-verification-asks-you-to-leave-the-language-you-ship-1h32</link>
      <guid>https://dev.to/thechrisin/why-verification-asks-you-to-leave-the-language-you-ship-1h32</guid>
      <description>&lt;p&gt;A test lives in the file you already have open. You write it in the language you ship, you run it, and when it fails you get an input you can look at. The first week with a verifier doesn't work like that. You pick a function you already understand, you write down what it should guarantee, and you wait. What comes back is often &lt;code&gt;unknown&lt;/code&gt;, a timeout, or an error pointing at a formula you never typed. The function was ten lines. Getting the tool to accept those ten lines is the rest of the week.&lt;/p&gt;

&lt;p&gt;The proof is asking about every run that meets the spec, and that claim needs more than an example. Bertrand Meyer's version of this, in &lt;a href="https://se.inf.ethz.ch/~meyer/publications/computer/contract.pdf" rel="noopener noreferrer"&gt;Design by Contract&lt;/a&gt;, is the one that still makes sense to people who ship code: the caller promises the preconditions, the callee promises the postconditions if those promises are kept. You already do a sloppy version of that in your head. The tool wants it written down, in a form it can check, and it won't take "obviously" for an answer.&lt;/p&gt;

&lt;p&gt;Preconditions, postconditions, framing, permissions, ownership: none of those are hard on their own, and none of them are what most people were hired to think about. You learn them while you learn the verifier, while you learn which of its errors mean the program is wrong and which mean the solver got bored. That's a lot to hold at once. It would be a lot even if the spec lived in the same language as the code. In the systems we've actually built, it usually doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof obligations and disappointment
&lt;/h2&gt;

&lt;p&gt;The loop is where this stops being a vocabulary problem. Five lines of code. The invariant that makes those five lines provable can take days, because it has to hold on entry, survive every iteration, and imply the postcondition when the loop exits. Dijkstra knew that, and so did the people who designed the tools. You find out by watching a verifier reject a program you're sure is correct.&lt;/p&gt;

&lt;p&gt;When the proof fails, three things could be wrong: the code, the specification, or the tool's ability to see why the specification holds. A unit test fails with a counterexample you can run. A verifier often fails with a shrug. Rustan Leino has spent years making that interaction less miserable, in what he and Michał Moskal named &lt;a href="https://leino.science/papers/krml212.pdf" rel="noopener noreferrer"&gt;auto-active verification&lt;/a&gt;: you stay in the editor, the tool tries to prove what it can, and you add assertions, lemmas, and ghost code until the solver goes through. That's better than a thousand-line Coq script, and it's still a second job. The ghost code is a program that exists only to convince a solver, written in a dialect whose diagnostics are about Z3's search that afternoon.&lt;/p&gt;

&lt;p&gt;In 2009, Gerwin Klein and colleagues published the functional correctness proof of the seL4 microkernel. The kernel was about &lt;a href="https://trustworthy.systems/publications/papers/Klein_EHACDEEKNSTW_09.pdf" rel="noopener noreferrer"&gt;8,700 lines of C and 600 lines of assembly&lt;/a&gt;. The proof, in Isabelle/HOL, was about 200,000 lines of script. They weren't padding. A machine-checked claim that a kernel always matches its spec is a large claim, and large claims produce large artifacts. By the time the same group wrote up nearly a decade of keeping that proof alive as the kernel grew, the Isabelle corpus was around &lt;a href="https://sel4.systems/Research/pdfs/comprehensive-formal-verification-os-microkernel.pdf" rel="noopener noreferrer"&gt;480,000 lines&lt;/a&gt;. Xavier Leroy's &lt;a href="https://xavierleroy.org/publi/compcert-CACM.pdf" rel="noopener noreferrer"&gt;CompCert&lt;/a&gt; is the same shape on the compiler side: a Coq development that lets you believe a property of the C still holds of the assembly. A team with a 200-line service and a sprint deadline doesn't start there. The proof is a second codebase, in a second language, maintained by people who can read both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two programs, one product
&lt;/h2&gt;

&lt;p&gt;The other thing you notice, once you've picked a tool, is that you've also picked a language you have to keep. A Dafny spec doesn't become a JML spec, and a Coq development doesn't become a SPARK annotation. You don't get to change your mind in year three without rewriting the argument.&lt;/p&gt;

&lt;p&gt;The field tried to share the painful part. Boogie and WhyML exist so a frontend can lower "real" code into a smaller logic and hand the rest to an SMT solver. Jean-Christophe Filliâtre and Andrei Paskevich's &lt;a href="https://why3.org/" rel="noopener noreferrer"&gt;Why3&lt;/a&gt; will send the same problem toward Alt-Ergo, CVC, Z3, or Coq, depending on what's installed. Microsoft's &lt;a href="https://www.microsoft.com/en-us/research/publication/boogie-a-modular-reusable-verifier-for-object-oriented-programs/" rel="noopener noreferrer"&gt;Boogie&lt;/a&gt; sits under Dafny. Verifier authors get to avoid rewriting SMT encodings every time. Someone trying to verify a program they already have now has three languages in play: the one they ship, the one they wrote the spec in, and the one the error actually happened in. Debugging a failed proof means debugging an encoding.&lt;/p&gt;

&lt;p&gt;AWS paid that bill in public. Cedar is the authorization language behind Amazon Verified Permissions. The team wanted to prove things about the authorizer that you actually care about in access control: a request is denied unless a permit says otherwise, and a forbid still wins if both apply. They proved those properties against a model written in Dafny. The thing they shipped was Rust. To believe the Rust, they ran &lt;a href="https://www.amazon.science/blog/how-we-built-cedar-with-automated-reasoning-and-differential-testing" rel="noopener noreferrer"&gt;differential random testing&lt;/a&gt;: generate policies, data, and requests, feed them to both programs, and see if they agree. Mike Hicks's writeup from May 2023 is worth reading for the texture. They used &lt;code&gt;cargo fuzz&lt;/code&gt;. They wrote extra generators because pure random policies almost never mention the same groups as the random requests, so you spend all your time in error-handling code and never touch the authorizer. By the time he published, a nightly run was on the order of a hundred million tests. That process found real bugs, including a bad IP-address parse in a Rust package they depended on, and mismatches in how missing application data got handled. Hicks also noted that the Dafny authorizer was about a sixth the size of the production one, which is why they could stand to read it.&lt;/p&gt;

&lt;p&gt;Hicks walks through a version of &lt;code&gt;isAuthorized&lt;/code&gt; that looks right on a first read: if any forbid policy applies, deny, otherwise allow. That gets "forbids override permits" for free. It also allows a request when &lt;em&gt;no&lt;/em&gt; policy applied at all, which is the opposite of the default-deny rule they thought they had. Dafny refused the lemma. The fix is checking that a permit actually applied, the kind of thing a code review can miss because the function still "looks like" access control. They found more of those while proving the validator sound: policies that type-checked and then blew up at evaluation in ways the design hadn't named yet.&lt;/p&gt;

&lt;p&gt;The language you can prove things about and the language you can ship were different languages, so a lot of the assurance budget went to showing they still meant the same thing. Most teams don't have that budget. They pick one side of the split and hope. The people who can keep a Dafny model and a Rust implementation aligned through a year of feature work are a research group. Testing never needed that group. JUnit, pytest, and &lt;code&gt;cargo test&lt;/code&gt; live in the language the code is already in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who the room is for
&lt;/h2&gt;

&lt;p&gt;Some of what's left is social, and pretending otherwise doesn't help.&lt;/p&gt;

&lt;p&gt;Formal methods grew up in universities and national labs. The currency is papers. The prestige objects are complete proofs of kernels and compilers. Those proofs matter. They also set the temperature of the field. A newcomer who asks for a tutorial that starts from a web handler is left with the impression they showed up to the wrong conference. Diagnostics assume you already know the logic. Examples are sorted lists and binary search, which is honest about the state of automation and dishonest about the programs people actually ship. If the price of entry is sequent calculus, the room stays small, and the smallness gets misread as seriousness. I don't think most of the researchers involved intend that. I do think the tools encode it. A system that requires a new language, a new IDE, and a new way of reading error messages will be used by people who already wanted to be there.&lt;/p&gt;

&lt;p&gt;The industry work that exists, Cedar included, is still specialist teams around components whose failure is expensive enough to justify the second language. They don't make the ordinary case cheaper.&lt;/p&gt;

&lt;p&gt;Testing won the ordinary case because it lived where the code already was. The closest thing verification has to that story is property-based testing, and the interesting part is how little language it needed.&lt;/p&gt;

&lt;p&gt;Koen Claessen and John Hughes published &lt;a href="https://doi.org/10.1145/351240.351266" rel="noopener noreferrer"&gt;QuickCheck&lt;/a&gt; at ICFP in 2000. The trick wasn't a new logic. Properties were Haskell functions. Generators were Haskell functions. You imported a module and you wrote &lt;code&gt;prop_&lt;/code&gt; names in the same files as the code. When a property failed, you got a counterexample, often a small one, because the library would shrink the input. You could do this on a lunch break, without asking anyone for a license or a seminar. Hypothesis later did the same thing for Python, and the idea spread into languages that will never run Isabelle, because you didn't have to leave. "Write a property, get a counterexample" became a normal skill, even though a property isn't a proof. The adoption still tells you where the notation has to sit: in the file that's already open, in the language the compiler already understands.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the spec isn't the program
&lt;/h2&gt;

&lt;p&gt;Most verification systems ask you to leave the language you're shipping. Dafny is the clean version of that bet: you write the program in Dafny, specify it in Dafny, and compile to C# or Java after the proof goes through. The translation between that language and the one you ship is now part of the trusted computing base. If the verifier's idea of what a loop means and the compiler's idea of what a loop means ever drift, you can prove a property of a program you didn't ship. CompCert exists partly because that drift is real for C. Cedar's Dafny-plus-Rust split is the same crack, accepted as an engineering process. Joshua Cohen's &lt;a href="https://joscoh.github.io/docs/thesis.pdf" rel="noopener noreferrer"&gt;foundationally verified Why3&lt;/a&gt; exists because the community already knows those mappings are large, trusted, and mostly unchecked.&lt;/p&gt;

&lt;p&gt;Specs rot the way comments rot, only with more confidence attached. A specification that can't be type-checked, refactored, and reviewed with the same tools as the program will be maintained like a comment: carefully at first, then not at all. Two syntaxes means two formatters, two jump-to-definitions, two code reviews. In practice the spec loses, because the compiler is the one that ships.&lt;/p&gt;

&lt;p&gt;A separate specification language also has to have a meaning, and that meaning is almost never the meaning of the host language. It's a logic chosen to sit well with SMT: restricted quantification, a particular heap model, a particular story about frames and permissions. You have to learn that logic and the mapping from your language into it. The mapping is where the unsoundness bugs live.&lt;/p&gt;

&lt;p&gt;I don't think this split was a mistake in 2005. Boogie and Why3 made it possible to build verifiers without rebuilding SMT encodings every time. An intermediate language is a good backend, the way LLVM is a good backend. LLVM isn't a language you ask application developers to think in. WhyML and Boogie have become that, even when the README says they're intermediate. Error messages leak into the mental model. Eventually you're debugging the encoding.&lt;/p&gt;

&lt;p&gt;I don't want anyone thinking in WhyML in order to ship a Rust program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification as a library
&lt;/h2&gt;

&lt;p&gt;I'd put the contracts in a module you can import: preconditions as ordinary expressions, ghost state as data the compiler erases, lemmas as functions whose bodies are proofs, checked and then discarded. The verifier is a checker for that library. The solver stays internal.&lt;/p&gt;

&lt;p&gt;QuickCheck already showed that a spec in a library gets used. The next step is to keep that shape and strengthen the checker: same notation, a static proof where the library can see enough, tests where it can't.&lt;/p&gt;

&lt;p&gt;In 2023, &lt;a href="https://www.andrew.cmu.edu/user/bparno/papers/verus-ghost.pdf" rel="noopener noreferrer"&gt;Verus&lt;/a&gt; showed you can write the specs and the proofs in Rust. There's a mode system that distinguishes executable code, spec, and proof, and ghost permissions go through Rust's own borrow checker, which is the whole reason you'd bother: the type system you already fight with is also the type system the proof can use. Ghost code is erased. The SMT solver is still there. The programmer-facing language is Rust. That's as close as I've seen to something a systems team might actually open on a Tuesday. It still isn't a crate you add to &lt;code&gt;Cargo.toml&lt;/code&gt; and forget about. Modes and ghost annotations are language extensions. The fragment the solver can handle is baked into the tool, not into an API you could version.&lt;/p&gt;

&lt;p&gt;I'd go further. Make even those ordinary: types and attributes the compiler already has, plus a checker that understands a documented fragment. SMT solvers like restricted logics. A full modern language is a bad input to Z3. The library can draw that line. It exports the predicates, ghost types, and proof combinators the checker knows how to handle, and it refuses the rest at the import boundary. That's a &lt;a href="https://thechris.in/articles/on-restraint-boundaries-and-systems-thinking/" rel="noopener noreferrer"&gt;boundary in the ordinary engineering sense&lt;/a&gt;: named interactions, everything else excluded by default. The restriction lives in an API, which can grow.&lt;/p&gt;

&lt;p&gt;Ghost code has to erase, so proofs don't ship. Spec-only functions can't be called from executable code by accident. Error messages have to point at the source you wrote, not at a Boogie file. And the expression in a &lt;code&gt;requires&lt;/code&gt; clause has to mean what it means in an &lt;code&gt;if&lt;/code&gt;. If the checker and the compiler ever disagree about that expression, that's a compiler bug.&lt;/p&gt;

&lt;p&gt;You inherit the host language's mess. If the language has undefined behavior, a library can't wish it away. SPARK's answer was a subset of Ada, encoded as types and banned constructs, without a new parser. Automation will be worse, at first, than in a language designed around the solver. Some properties will stay out of reach until the library grows a lemma collection the way Lean grew mathlib: slowly, in public, by people solving their own problems and leaving the lemma behind. A lemma about maps, written against a verification library, can be depended on by the next program in the same language. A lemma about maps, written in WhyML, helps the next WhyML program. There aren't many of those.&lt;/p&gt;

&lt;p&gt;I've been turning this over while working on &lt;a href="https://github.com/bigtalk-org" rel="noopener noreferrer"&gt;BigTalk&lt;/a&gt;, which is supposed to have verification without asking people to leave the language to get it. Once you're already deciding what a type is, what an effect is, and what a region is, adding a second meaning for "the same" program starts to look like the kind of &lt;a href="https://thechris.in/articles/on-programming-language-design-and-the-problem-of-forgotten-decisions/" rel="noopener noreferrer"&gt;forgotten decision&lt;/a&gt; you'll spend years apologizing for. The library is the decision I'd rather defend: verification as code, in the program, with a checker that shares the compiler's meaning of that code.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bertrand Meyer, &lt;a href="https://se.inf.ethz.ch/~meyer/publications/computer/contract.pdf" rel="noopener noreferrer"&gt;Applying Design by Contract&lt;/a&gt;, IEEE Computer, 1992.&lt;/li&gt;
&lt;li&gt;K. Rustan M. Leino, &lt;a href="https://www.microsoft.com/en-us/research/publication/dafny-automatic-program-verifier-functional-correctness/" rel="noopener noreferrer"&gt;"Dafny: An Automatic Program Verifier for Functional Correctness,"&lt;/a&gt; LPAR, 2010. See also &lt;a href="https://leino.science/papers/krml221.pdf" rel="noopener noreferrer"&gt;Using Dafny, an Automatic Program Verifier&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;K. Rustan M. Leino and Michał Moskal, &lt;a href="https://leino.science/papers/krml212.pdf" rel="noopener noreferrer"&gt;"Usable Auto-Active Verification,"&lt;/a&gt; UV, 2010.&lt;/li&gt;
&lt;li&gt;Gerwin Klein et al., &lt;a href="https://trustworthy.systems/publications/papers/Klein_EHACDEEKNSTW_09.pdf" rel="noopener noreferrer"&gt;"seL4: Formal Verification of an OS Kernel,"&lt;/a&gt; SOSP, 2009.&lt;/li&gt;
&lt;li&gt;Gerwin Klein et al., &lt;a href="https://sel4.systems/Research/pdfs/comprehensive-formal-verification-os-microkernel.pdf" rel="noopener noreferrer"&gt;"Comprehensive Formal Verification of an OS Microkernel,"&lt;/a&gt; ACM Transactions on Computer Systems, 2014.&lt;/li&gt;
&lt;li&gt;Xavier Leroy, &lt;a href="https://xavierleroy.org/publi/compcert-CACM.pdf" rel="noopener noreferrer"&gt;"Formal Verification of a Realistic Compiler,"&lt;/a&gt; Communications of the ACM, 2009.&lt;/li&gt;
&lt;li&gt;Jean-Christophe Filliâtre and Andrei Paskevich, &lt;a href="https://doi.org/10.1007/978-3-642-37036-6_8" rel="noopener noreferrer"&gt;"Why3: Where Programs Meet Provers,"&lt;/a&gt; ESOP, 2013. See also &lt;a href="https://why3.org/" rel="noopener noreferrer"&gt;Why3&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Mike Barnett, Bor-Yuh Evan Chang, Robert DeLine, Bart Jacobs, and K. Rustan M. Leino, &lt;a href="https://www.microsoft.com/en-us/research/publication/boogie-a-modular-reusable-verifier-for-object-oriented-programs/" rel="noopener noreferrer"&gt;"Boogie: A Modular Reusable Verifier for Object-Oriented Programs,"&lt;/a&gt; FMCO, 2005.&lt;/li&gt;
&lt;li&gt;Mike Hicks, &lt;a href="https://www.amazon.science/blog/how-we-built-cedar-with-automated-reasoning-and-differential-testing" rel="noopener noreferrer"&gt;"How We Built Cedar with Automated Reasoning and Differential Testing,"&lt;/a&gt; Amazon Science, May 2023.&lt;/li&gt;
&lt;li&gt;Koen Claessen and John Hughes, &lt;a href="https://doi.org/10.1145/351240.351266" rel="noopener noreferrer"&gt;"QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs,"&lt;/a&gt; ICFP, 2000.&lt;/li&gt;
&lt;li&gt;Andrea Lattuada et al., &lt;a href="https://www.andrew.cmu.edu/user/bparno/papers/verus-ghost.pdf" rel="noopener noreferrer"&gt;"Verus: Verifying Rust Programs using Linear Ghost Types,"&lt;/a&gt; OOPSLA, 2023.&lt;/li&gt;
&lt;li&gt;Joshua M. Cohen, &lt;a href="https://joscoh.github.io/docs/thesis.pdf" rel="noopener noreferrer"&gt;&lt;em&gt;A Foundationally Verified Intermediate Verification Language&lt;/em&gt;&lt;/a&gt;, PhD thesis, Princeton, 2025.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>languagedesign</category>
      <category>formalverification</category>
      <category>rust</category>
    </item>
    <item>
      <title>Why Plugin Systems Keep Running After the Contract Is Gone</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:56:29 +0000</pubDate>
      <link>https://dev.to/thechrisin/why-plugin-systems-fail-quietly-weak-boundaries-explained-4lhl</link>
      <guid>https://dev.to/thechrisin/why-plugin-systems-fail-quietly-weak-boundaries-explained-4lhl</guid>
      <description>&lt;p&gt;Most plugin systems continue to work just well enough, making them dangerous.&lt;/p&gt;

&lt;p&gt;A typical plugin system starts from a reasonable goal of letting people extend functionality without touching the core. The core stays stable. Optional functionality lives at the edges. Responsibility is distributed cleanly, at least on the whiteboard. In the early stages this holds up fine in practice. Plugins are few. Contracts are narrow enough that a plugin author can read the documented interface once and know the whole shape of what they're allowed to touch. A new contributor can look at the system and reason about it locally, without holding the whole thing in their head. Nobody has had to break a promise to anyone yet, but that equilibrium doesn't last, and it rarely announces its own ending.&lt;/p&gt;

&lt;p&gt;The failure builds through accommodation: a plugin needs access to a little more state than the contract allows, or another needs to intercept a request earlier in the lifecycle than anyone originally envisioned. Each request, taken on its own, is defensible. Nobody who approved any single one of them was wrong to do it.&lt;/p&gt;

&lt;p&gt;Read any one of those changes in a code review and it looks fine. A reviewer sees a small diff, a plausible justification, a deadline attached to it, and approves it. What a reviewer almost never sees is the fifteenth version of that same request, made by a different plugin, for a different reason, landing on a boundary that's already been widened fourteen times before. Each decision is local. The damage is cumulative, and cumulative damage is exactly the kind that doesn't show up in any single code review.&lt;/p&gt;

&lt;p&gt;Somewhere in that process it becomes a second system running alongside the first, wired to it at dozens of points nobody wrote down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The erosion of boundaries
&lt;/h2&gt;

&lt;p&gt;A plugin system only works if there's a hard line between what the core owns and what plugins are allowed to touch. Almost every real system compromises that line early, because compromising it is the path of least resistance. Internal data structures get exposed. Lifecycle hooks get added for one plugin's sake. Mutable globals stay reachable because locking them down would mean telling three plugin authors their code is about to break. The justification is always some version of "trust the plugin author to use it responsibly."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Trust doesn't scale the way access does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once a plugin depends on an internal detail, that detail stops being internal, whatever the documentation says. It becomes a &lt;a href="https://thechris.in/articles/on-restraint-boundaries-and-systems-thinking/" rel="noopener noreferrer"&gt;contract by accident&lt;/a&gt;, and accidental contracts are the ones nobody can safely break, because nobody agreed to them on purpose in the first place.&lt;/p&gt;

&lt;p&gt;Firefox lived this for over a decade. Its original extension model, built on XUL and XPCOM, gave add-ons direct access to the browser's internal component model, i.e., the same libraries that handled networking, rendering, and security, wide open to any extension that wanted them. It made Firefox's extension ecosystem one of the richest on the web for years. It also meant that by the time Mozilla needed to rebuild Firefox around a multiprocess architecture, in pursuit of the same stability and performance gains Chrome had already shipped, &lt;a href="https://lwn.net/Articles/668956/" rel="noopener noreferrer"&gt;legacy XUL extensions were incompatible with it by construction&lt;/a&gt;. They reached into places a multiprocess browser couldn't safely expose anymore. Mozilla's own reasoning was direct: reliance on XUL and XPCOM had become an obstacle to work the browser needed to do, specifically the Electrolysis multiprocess project and the Servo rendering engine, both of which required a much cleaner separation between the browser's internals and whatever code an extension was running. You can't safely split a browser across multiple processes if any extension might be reaching directly into a data structure that now lives in a different process entirely. The fix arrived as a new extension API, WebExtensions, deliberately narrower and modeled closely on Chrome's, paired with a hard cutoff: as of &lt;a href="https://blog.mozilla.org/addons/2017/10/26/helping-find-compatible-extensions/" rel="noopener noreferrer"&gt;Firefox 57 in November 2017&lt;/a&gt;, the old add-ons simply stopped loading. Years of accumulated extensions broke at once. The access those extensions depended on had never been closed off, and closing it years later took a browser-wide breaking release that a narrower API would have made unnecessary from the start.&lt;/p&gt;

&lt;p&gt;The break, when it finally comes, looks catastrophic, because years of small, reasonable accommodations were never priced as debt until the bill came due all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared responsibility, no ownership
&lt;/h2&gt;

&lt;p&gt;In a plugin system that's still healthy, you can always answer one question cleanly: who owns this invariant. Either the core enforces it, or the invariant doesn't really exist. Once a system starts failing, that question stops having a clean answer. Invariants get enforced collectively, informally, by convention. One plugin assumes another has already run. The core assumes plugins will clean up their own state. Execution order becomes "load-bearing," to borrow the phrase everyone reaches for these days, and nobody wrote it down because nobody designed it on purpose.&lt;/p&gt;

&lt;p&gt;At that point behavior is being negotiated, extension by extension, mostly by accident.&lt;/p&gt;

&lt;p&gt;Atom's package system got there. Packages ran with full access to the DOM and to Node's runtime, which was also the thing that made Atom feel so hackable. You could install a package, and it could reshape almost anything about how the editor behaved, down to intercepting keystrokes before the editor's own core ever saw them. That same openness meant packages could and did collide in ways nobody upstream could fully predict, each one reasonable in isolation, none of them designed against each other. Two packages might both assume they were the first to touch a given buffer. Neither was wrong on its own. Together, loaded in whatever order the user happened to install them, they'd produce a bug that only existed because of the combination, and no one owned the combination.&lt;/p&gt;

&lt;p&gt;GitHub &lt;a href="https://www.ghacks.net/2022/06/09/githubs-atom-text-editor-will-be-retired-in-december/" rel="noopener noreferrer"&gt;announced Atom's sunset in June 2022&lt;/a&gt;, archiving the project that December. The stated reason was that community involvement and feature development had stalled. The editor Microsoft chose to keep investing in instead, Visual Studio Code, had been built from day one on a much more restrained model for what an extension was allowed to do. Atom's own performance problems, widely attributed at the time to its Electron foundation, were at least partly a symptom of the same openness. An editor that lets every package touch everything has a much harder time optimizing anything, because it can never be sure what depends on the thing it wants to change.&lt;/p&gt;

&lt;p&gt;Failures don't map to a single component. They emerge from interaction, from two pieces of code that were each individually correct and jointly wrong. Removing one plugin "fixes" the bug, and nobody on the team can explain why beyond a shrug. The system still runs. Nobody quite trusts it anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Power dressed up as flexibility
&lt;/h2&gt;

&lt;p&gt;A lot of plugin systems expose power where they should be exposing interfaces. They hand out a hook into some broad execution context and call it flexibility. When a plugin can do anything, the core can guarantee nothing.&lt;/p&gt;

&lt;p&gt;ActiveX is the clean historical case. Introduced by Microsoft in 1996, it let a web page embed a control that ran with essentially the same privileges as any other program on the machine, no sandbox, no meaningfully scoped permission model. That an ActiveX control could do anything a desktop application could do, right inside the browser, was the appeal. It was also, as one retrospective later put it bluntly, &lt;a href="https://www.howtogeek.com/717016/remembering-activex-controls-the-webs-biggest-mistake/" rel="noopener noreferrer"&gt;the web's biggest mistake&lt;/a&gt;. A long, ugly stream of &lt;a href="https://www.cisa.gov/news-events/alerts/2006/09/15/microsoft-internet-explorer-activex-vulnerability" rel="noopener noreferrer"&gt;vulnerabilities&lt;/a&gt; followed, most of them boiling down to the same root cause where a hostile web page could get an ActiveX control to run arbitrary code as the user, no exploit chain required beyond convincing someone to visit the wrong site. Microsoft spent years bolting on warnings and kill bits before finally &lt;a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-office-2024-to-disable-activex-controls-by-default/" rel="noopener noreferrer"&gt;disabling ActiveX by default&lt;/a&gt; across Office and Microsoft 365 in 2024, nearly three decades after it shipped, rather than ever retrofitting real boundaries into a system built without any.&lt;/p&gt;

&lt;p&gt;ActiveX wasn't alone in this. The broader family of browser plugin technologies built around NPAPI, the interface that let Flash, Java applets, and Silverlight run inside a browser tab, made a similar bet: give the plugin a wide-open door into the host process in exchange for whatever capability the plugin wanted to offer. Chrome and Firefox spent the 2010s walking that bet back, first with sandboxing, then with click-to-play defaults, then by dropping NPAPI support entirely. &lt;a href="https://blog.chromium.org/2014/11/the-final-countdown-for-npapi.html" rel="noopener noreferrer"&gt;Chrome removed it in 2015&lt;/a&gt;, &lt;a href="https://www.bleepingcomputer.com/news/software/mozilla-to-drop-support-for-all-npapi-plugins-in-firefox-52-except-flash/" rel="noopener noreferrer"&gt;Firefox followed in 2017&lt;/a&gt;. The technology wasn't uniquely badly built; it ran on the same premise as ActiveX, that broad access is a reasonable price for broad capability. Browsers eventually decided it wasn't, and every plugin depending on that access had to be rebuilt or abandoned to match.&lt;/p&gt;

&lt;p&gt;Defensive code creeps into the core because nobody trusts what a plugin might have already touched. Validation gets duplicated at every boundary because there's no boundary anyone's confident is real.&lt;/p&gt;

&lt;p&gt;This never arrives as a single moment. There's no line the system visibly crosses. It just accumulates until the honest answer to "where does this system end" is that nobody's sure anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The systems that age better
&lt;/h2&gt;

&lt;p&gt;Eclipse is one of the oldest working examples. Its plugin model, formalized on top of OSGi, is built around what Eclipse's own architecture documentation calls the &lt;a href="https://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html" rel="noopener noreferrer"&gt;plug-in contract&lt;/a&gt;, a small, explicit set of extension points, each one declared up front, each one owned by whichever plugin defines it. A plugin can't reach into another plugin's internals just because both happen to be loaded in the same process. It has to go through a declared extension point or it doesn't get in at all. OSGi's bundle model reinforces this at the runtime level, with each bundle getting its own classloader and explicit version boundaries between dependencies. That discipline is a large part of why OSGi has stayed a working foundation for enterprise software for close to two decades, hot-swapping components in running systems without the whole platform destabilizing every time somebody ships an update.&lt;/p&gt;

&lt;p&gt;Visual Studio Code took a related but distinct approach to the same discipline. Instead of trusting extensions with the same process as the editor, it doesn't give them the choice. Every extension runs in a separate process, &lt;a href="https://code.visualstudio.com/blogs/2022/11/28/vscode-sandbox" rel="noopener noreferrer"&gt;the extension host&lt;/a&gt;, talking to the main application only through a defined API surface and an RPC layer. An extension can be slow, buggy, even hostile, and the editor's own responsiveness is largely insulated from it, because the extension was never given the kind of direct access that would let it take the rest of the application down with it.&lt;/p&gt;

&lt;p&gt;That's the same problem Atom's package model ran into, solved by refusing the access in the first place instead of hoping plugin authors would behave. Where Eclipse enforces its boundary through a declared contract that extensions have to go through, VS Code enforces a version of the same idea through a process boundary that extensions can't get around even if they wanted to. Different mechanism, same underlying refusal.&lt;/p&gt;

&lt;p&gt;Eclipse and VS Code both made their refusal at the architecture level, before any extension author ever showed up wanting more. Chrome's Manifest V3 shows what the same refusal looks like made in public, years into an established ecosystem, against developers who had built real businesses on the access being taken away. Chrome's older extension API let extensions intercept and modify every network request a browser made, through the webRequest API, the kind of broad, ambient power that makes it impossible to reason about what any given extension might be doing to your traffic. Google's replacement, &lt;code&gt;declarativeNetRequest&lt;/code&gt;, asks extensions to declare their filtering rules up front instead of intercepting requests live, which is a real reduction in what an extension can express. Ad blocker developers noticed immediately. Raymond Hill, who maintains uBlock Origin, &lt;a href="https://www.bleepingcomputer.com/news/security/chrome-extension-manifest-v3-may-break-ublock-origin-content-blocker/" rel="noopener noreferrer"&gt;warned as early as January 2019&lt;/a&gt; that the change threatened to break his tool's core functionality, and the backlash from extension developers has continued through every stage of the rollout since. Google shipped the restriction anyway. The ecosystem got smaller and louder about it in roughly equal measure, and Chrome's extension platform got more predictable in exchange, which was the actual trade being made.&lt;/p&gt;

&lt;p&gt;Every one of those systems paid a visible cost for the refusal. Eclipse's plugin authors write against a stricter model than a looser platform would demand. VS Code extensions can't reach into the editor the way an Atom package once could. Chrome's ad blockers lost capability they'd relied on for years. None of it showed up as a headline the way a launch does. It showed up as a smaller ecosystem that people complained about and kept building on anyway.&lt;/p&gt;

&lt;p&gt;This is the ordinary, predictable outcome of extension without boundaries, produced by people who were right about every single decision except the sum of all of them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Aside:&lt;/strong&gt; &lt;em&gt;I don't like VSCode personally. I like what Zed is doing, and building an IDE of my own is a long-term goal. For now that's a few years away.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://lwn.net/Articles/668956/" rel="noopener noreferrer"&gt;"Deprecating XUL for WebExtensions,"&lt;/a&gt; LWN.net.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.mozilla.org/addons/2017/10/26/helping-find-compatible-extensions/" rel="noopener noreferrer"&gt;"Helping You Find Compatible Extensions,"&lt;/a&gt; Mozilla Add-ons Community Blog, October 2017.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.ghacks.net/2022/06/09/githubs-atom-text-editor-will-be-retired-in-december/" rel="noopener noreferrer"&gt;"GitHub's Atom Text Editor Will Be Retired in December,"&lt;/a&gt; gHacks Tech News, June 2022.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.howtogeek.com/717016/remembering-activex-controls-the-webs-biggest-mistake/" rel="noopener noreferrer"&gt;"Remembering ActiveX Controls, the Web's Biggest Mistake,"&lt;/a&gt; How-To Geek.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cisa.gov/news-events/alerts/2006/09/15/microsoft-internet-explorer-activex-vulnerability" rel="noopener noreferrer"&gt;"Microsoft Internet Explorer ActiveX Vulnerability,"&lt;/a&gt; CISA, 2006.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-office-2024-to-disable-activex-controls-by-default/" rel="noopener noreferrer"&gt;"Microsoft Office 2024 to disable ActiveX controls by default,"&lt;/a&gt; BleepingComputer, 2024.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html" rel="noopener noreferrer"&gt;"Notes on the Eclipse Plug-in Architecture,"&lt;/a&gt; Eclipse Foundation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.osgi.org/resources/what-is-osgi/" rel="noopener noreferrer"&gt;"What Is OSGi?,"&lt;/a&gt; OSGi Alliance / Eclipse Foundation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/blogs/2022/11/28/vscode-sandbox" rel="noopener noreferrer"&gt;"Migrating VS Code to Process Sandboxing,"&lt;/a&gt; Visual Studio Code Blog, November 2022.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.bleepingcomputer.com/news/security/chrome-extension-manifest-v3-may-break-ublock-origin-content-blocker/" rel="noopener noreferrer"&gt;"Chrome Extension Manifest V3 May Break uBlock Origin Content Blocker,"&lt;/a&gt; BleepingComputer, January 2019.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.chromium.org/2014/11/the-final-countdown-for-npapi.html" rel="noopener noreferrer"&gt;"The Final Countdown for NPAPI,"&lt;/a&gt; Chromium Blog, November 2014.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.bleepingcomputer.com/news/software/mozilla-to-drop-support-for-all-npapi-plugins-in-firefox-52-except-flash/" rel="noopener noreferrer"&gt;"Mozilla to Drop Support for All NPAPI Plugins in Firefox 52, Except Flash,"&lt;/a&gt; BleepingComputer, 2017.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>softwareengineering</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Why Systems That Last Keep Their Boundaries Honest</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:55:03 +0000</pubDate>
      <link>https://dev.to/thechrisin/systems-thinking-how-restraint-prevents-complexity-4jno</link>
      <guid>https://dev.to/thechrisin/systems-thinking-how-restraint-prevents-complexity-4jno</guid>
      <description>&lt;p&gt;Most system failures trace back to a constraint nobody wrote down. By the time something breaks, the architecture is usually still fine. What gave way is an assumption nobody agreed to keep.&lt;/p&gt;

&lt;p&gt;Early on, most systems are tractable. The architecture is coherent, the abstractions are few, and the number of invariants in play is small enough that one person can hold all of them in their head at once. At this stage the system rewards local reasoning. You can change a component and predict the effect of that change without opening five other files to check what else might notice.&lt;/p&gt;

&lt;p&gt;The system's behavior can still be approximated as:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal sizing reset-size6 size7"&gt;S&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel sizing reset-size6 size7"&gt;≈&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-limits sizing reset-size6 size7"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size7 size4 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size7 size4 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sizing reset-size6 size7"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size7 size4 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;where each component 
&lt;span class="katex-element"&gt;
  &lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/span&gt;
 contributes independently, and interactions between components stay limited and explicit. Read a function, understand its inputs and outputs, and you understand what it does to the system. Nothing else needs checking.&lt;/p&gt;

&lt;p&gt;That property doesn't survive growth, and it rarely gets killed off by recklessness. It erodes through accommodation. A new requirement adds a conditional path to a function that used to have one job. An exception bypasses an invariant because enforcing it that week would have blown a deadline. A shortcut defers a constraint under pressure, with every intention of coming back to fix it later, and the fix never gets scheduled because nothing visibly broke. Each of these changes looks locally defensible at the moment it's made. The system quietly stops being additive, one reasonable decision at a time, without anyone ever sitting down and deciding to compromise the architecture.&lt;/p&gt;

&lt;p&gt;Interaction terms accumulate:&lt;/p&gt;


&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal sizing reset-size6 size5"&gt;S&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel sizing reset-size6 size5"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-limits sizing reset-size6 size5"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size5 size2 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;span class="mord mtight"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size5 size2 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sizing reset-size6 size5"&gt;&lt;span class="mord mathnormal"&gt;C&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size5 size2 mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin sizing reset-size6 size5"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mop op-limits sizing reset-size6 size5"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size5 size2 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;i&lt;/span&gt;&lt;span class="mrel mtight"&gt;&lt;span class="mrel mtight"&gt;&lt;span class="mord vbox mtight"&gt;&lt;span class="thinbox mtight"&gt;&lt;span class="rlap mtight"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="inner"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mrel mtight"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="fix"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mrel mtight"&gt;=&lt;/span&gt;&lt;/span&gt;&lt;span class="mord mathnormal mtight"&gt;j&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="mop op-symbol large-op"&gt;∑&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mord sizing reset-size6 size5"&gt;&lt;span class="mord mathnormal"&gt;I&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size5 size2 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mord mathnormal mtight"&gt;ij&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;Once interactions grow faster than components, local reasoning stops working. The second term takes over. Behavior starts to look emergent instead of designed, and the instinct is usually to blame the people writing the code, but that's rarely where the fault lies. Coupled systems behave this way as a structural matter. It happens on well-run teams with disciplined engineers just as often as it happens anywhere else, because the mathematics doesn't care how careful anyone was being.&lt;/p&gt;

&lt;p&gt;A validation function gets written for one form, on one page. Six months later it's being called from four other places, because it already does most of what those callers need and writing a second function feels wasteful. None of the four callers needs quite the same validation, so each one adds a flag, a special case, an early return for its own use. The function now has to know about four different contexts to do its job in any of them. A helper function turns into a load-bearing wall without any single change making it one, because reuse looked free every single time someone reached for it, long before anyone consciously chose to build it that way.&lt;/p&gt;

&lt;p&gt;The four callers didn't need a shared function. They needed four small functions that happened to overlap in places. Leave the overlap. Writing four functions feels like waste in the moment, measured against the immediate task. Each of those four could change, break, and get deleted independently when its caller goes away. The shared version can do none of those things without a meeting first, and the meeting rarely happens until the function is already too tangled to split back apart.&lt;/p&gt;

&lt;p&gt;Reuse is one of the few instincts in software engineering that's taught early and rewarded in code review, usually before anyone's had the chance to watch it fail. Writing the same ten lines twice doesn't get anyone promoted, but spotting the duplication and collapsing it into one place does. The complimented version is the one that, three years later, four teams are afraid to touch, and by then nobody remembers who to ask about why it's shaped the way it is.&lt;/p&gt;

&lt;p&gt;Restraint, in this context, is what keeps that growth in check: not adding an abstraction before there's evidence it earns its keep. The same instinct holds off on generalizing a function until two call sites need it, and refuses to promise behavior the system can't keep enforcing five years from now, after everyone who understood the original trade-off has moved to a different team.&lt;/p&gt;

&lt;p&gt;Boundaries are how restraint gets enforced day to day. Naming them in a design review once, then forgetting them, doesn't count. A boundary is a contract that names the allowed interactions, excludes everything else by default, and enforces the invariant at the interface instead of relying on the discipline of every later caller, most of whom will never read the comment explaining why the invariant exists. When a boundary holds, interaction terms stay contained near it. When it doesn't, complexity diffuses outward to wherever the boundary used to be, and it rarely announces that it's doing so.&lt;/p&gt;

&lt;p&gt;A module with no interface at least advertises its own recklessness; everyone touching it knows they're on their own. A leaky one looks safe from the outside while failing, without any noise, to protect anything. Once internal details leak across a boundary, that boundary is a boundary in name only. The invariant it was supposed to protect is no longer enforced in one place. It's now distributed, implicitly, across every caller that has come to depend on the leak, usually without anyone on the team keeping a list of who's depending on what.&lt;/p&gt;

&lt;p&gt;Someone returned the internal list because it was three lines shorter than writing a proper accessor. For a while, nothing goes wrong. Then a caller three modules away starts mutating that list directly, because it's right there and it works. A year later, someone tries to change how that internal list is stored, maybe to fix a performance problem, and discovers the change breaks code in a part of the system they've never had reason to open. The boundary failed silently, letting something invisible grow behind it.&lt;/p&gt;

&lt;p&gt;At that point the cost of a small change stops being small. A one-line fix requires tracing every caller that might be relying on the old behavior, half of which were never documented as dependents in the first place. Understanding gives way to fear. Teams stop touching that part of the code because nobody can say with confidence what else is wired to it, and finding out is expensive enough that it's easier to build around the problem than through it.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://en.wikipedia.org/wiki/The_Mythical_Man-Month" rel="noopener noreferrer"&gt;The Mythical Man-Month&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Fred_Brooks" rel="noopener noreferrer"&gt;Fred Brooks&lt;/a&gt; wrote that conceptual integrity erodes gradually, under a steady accumulation of individually reasonable additions. &lt;a href="https://en.wikipedia.org/wiki/John_Ousterhout" rel="noopener noreferrer"&gt;John Ousterhout&lt;/a&gt;, in &lt;a href="https://dl.acm.org/doi/10.5555/3288797" rel="noopener noreferrer"&gt;A Philosophy of Software Design&lt;/a&gt;, describes the shallow module: an interface nearly as complicated as its implementation. It doesn't reduce complexity. It relocates it onto whoever has to call it, and hides the relocation behind the appearance of a clean function signature.&lt;/p&gt;

&lt;p&gt;Ousterhout's counterpart to the shallow module is the deep one: a simple interface sitting in front of substantial internal machinery. The caller gets a narrow surface to learn. The complexity stays in the module. A file system is the example he reaches for. Opening, reading, and writing a file is a handful of operations to learn, and underneath that handful sits allocation, caching, and recovery from partial writes. None of it is the caller's problem. Most interfaces people call "clean" are shallow in this sense, and most people can't tell the difference until they're the one stuck maintaining what's on the other side of it.&lt;/p&gt;

&lt;p&gt;Systems thinking, in practice, is arranging things so that most components need to know as little as possible about the rest of the system. Duplicating ten lines of logic across two modules can be cheaper, over the life of the system, than sharing one function that couples them together for years.&lt;/p&gt;

&lt;p&gt;There's an old, unglamorous illustration of this in how Unix pipes were designed: small programs, each doing one narrow thing, connected through a plain text interface that none of them needed to understand beyond "read a stream, write a stream." No program in that chain needs to know how the others are implemented. The boundary between them is the text stream itself, and it's inconvenient in exactly the ways that keep it honest: no shared memory, and no shortcut for one program to reach into another's internals because it's under a deadline.&lt;/p&gt;

&lt;p&gt;Not everything from that era aged the same way. The shells wrapping those pipes have accumulated more flags than most people using them could name, and plenty of small Unix tools grew their own tangle of special cases the moment someone needed one badly enough. The pipe itself mostly avoided that fate, though it's worth admitting that some of the credit belongs to the text stream being too dumb to extend rather than to anyone's restraint.&lt;/p&gt;

&lt;p&gt;A system that preserves locality lets reasoning scale roughly linearly with size. Doubling the codebase doubles the amount you need to understand to make a safe change, not the odds that the change ripples somewhere unrelated and unrelated-looking. A system that loses locality forces reasoning to become global well before it doubles in size, and past that point it stops scaling in any useful sense, no matter how much hardware or headcount gets thrown at the problem afterward. More engineers on a system with no locality doesn't parallelize the work. It just adds more people who each need the same global context before they can safely touch anything.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://curtclifton.net/papers/MoseleyMarks06a.pdf" rel="noopener noreferrer"&gt;Out of the Tar Pit&lt;/a&gt;, &lt;em&gt;Ben Moseley&lt;/em&gt; and &lt;em&gt;Peter Marks&lt;/em&gt; argue that essential complexity, the complexity a system can't avoid because it's inherent to the problem being solved, is usually smaller than it looks from inside a struggling codebase. Most of what makes large systems hard to work with is accidental complexity, generated by uncontrolled state and interaction. That's what teams end up fighting for years without naming it that way.&lt;/p&gt;

&lt;p&gt;A system that grants unrestricted access in the name of flexibility has already given up the guarantees that flexibility was supposed to make possible. A setting changes mid-request and nobody can say why. Six subsystems all have write access to the same configuration object, because passing it around explicitly felt like extra ceremony, and none of them coordinated with the others. Early on that looked like convenience. The flexibility that made the object easy to build with is what makes the bug impossible to trace.&lt;/p&gt;

&lt;p&gt;Fixing that particular problem rarely means adding more logging or more code review. It means taking write access away from five of the six subsystems and forcing them to ask the sixth one for a change instead of making it themselves, which is a smaller ask than it sounds like until you're the one on the subsystem that lost its shortcut.&lt;/p&gt;

&lt;p&gt;A component can only be given real freedom to change its own internals once the rest of the system knows exactly what it will and won't do at its boundary. Freedom without a known boundary reads as flexibility right up until someone needs to rely on it, at which point it's just risk nobody priced in.&lt;/p&gt;

&lt;p&gt;Trust, inside a codebase, tends to come from limits. A component that's explicit about what it will do, and that fails loudly when it can't, gives engineers something solid to build on top of. It lets a team revisit an old decision months later without treating the whole surrounding area as radioactive, the way people learn to avoid the one file nobody fully understands anymore, the one everyone routes around instead of opening.&lt;/p&gt;

&lt;p&gt;The same pattern shows up outside software entirely. &lt;a href="https://en.wikipedia.org/wiki/Charles_Perrow" rel="noopener noreferrer"&gt;Charles Perrow&lt;/a&gt;'s &lt;a href="https://en.wikipedia.org/wiki/Normal_Accidents" rel="noopener noreferrer"&gt;Normal Accidents&lt;/a&gt; looks at failures across nuclear plants, chemical refineries, and aviation, and finds that catastrophic failure in tightly coupled systems is usually a property of how densely the parts interact, not a property of any single part failing on its own. In systems that are already tightly coupled, adding more cross-cutting paths, more ways for one part to reach into another under unusual conditions, tends to increase risk. This holds even when every individual addition looked, at the time, like it was making the system more capable or more resilient to some specific failure someone had just seen happen.&lt;/p&gt;

&lt;p&gt;Time tests a design more honestly than any review ever will. Systems built to anticipate every future requirement tend to accumulate complexity they never use, carrying the weight of speculative features on the chance someone eventually asks for them. &lt;a href="https://en.wikipedia.org/wiki/Manny_Lehman_(computer_scientist)" rel="noopener noreferrer"&gt;Manny Lehman&lt;/a&gt;'s &lt;a href="https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution" rel="noopener noreferrer"&gt;Laws of Software Evolution&lt;/a&gt; get at part of why: a system that keeps growing without a disciplined structure to contain that growth gets progressively harder to modify, on a schedule that belongs to the system no matter who is maintaining it that quarter. Good engineers can slow the decline, but none of them seem to stop it outright.&lt;/p&gt;

&lt;p&gt;Past a certain size, discipline at the boundary stops being optional, whether or not anyone on the team decided to call it that. What remains, after the person who drew the boundary has left the room, is whether anyone kept enforcing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/The_Mythical_Man-Month" rel="noopener noreferrer"&gt;The Mythical Man-Month&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.5555/3288797" rel="noopener noreferrer"&gt;A Philosophy of Software Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curtclifton.net/papers/MoseleyMarks06a.pdf" rel="noopener noreferrer"&gt;Out of the Tar Pit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Normal_Accidents" rel="noopener noreferrer"&gt;Normal Accidents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution" rel="noopener noreferrer"&gt;Laws of Software Evolution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>foundational</category>
      <category>programming</category>
      <category>architecture</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Why the Reasoning Behind a Keyword Disappears</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:55:02 +0000</pubDate>
      <link>https://dev.to/thechrisin/programming-language-design-why-decisions-get-forgotten-3gj8</link>
      <guid>https://dev.to/thechrisin/programming-language-design-why-decisions-get-forgotten-3gj8</guid>
      <description>&lt;p&gt;Most programming languages are remembered for their syntax. Almost none are remembered for the decisions that produced it.&lt;/p&gt;

&lt;p&gt;Every keyword exists because someone decided one approach was better than another, usually after weeks of debate. Writing it took an afternoon. The reasoning behind it lasted about as long as it took everyone to move on.&lt;/p&gt;

&lt;p&gt;I've been building &lt;a href="https://github.com/bigtalk-org" rel="noopener noreferrer"&gt;&lt;strong&gt;BigTalk&lt;/strong&gt;&lt;/a&gt; from the ground up: memory model, effects, regions, verification. &lt;code&gt;git&lt;/code&gt; can find the line that introduced a bug. What it won't tell you is why that line was written that way instead of three other ways that were discussed and discarded, or never written down because they seemed obvious at the time.&lt;/p&gt;

&lt;p&gt;Those conversations live in chat threads, calls, whiteboard photos nobody keeps.&lt;/p&gt;

&lt;p&gt;Adding a keyword is easy, and rewriting a parser is easy enough. Choosing the keyword is the part that stays difficult after the merge.&lt;/p&gt;

&lt;p&gt;Should this feature exist at all? Does explicitness win over convenience here? An inconsistency might have to stay, too, if fixing it breaks something that was never supposed to depend on it.&lt;/p&gt;

&lt;p&gt;Those questions don't have single correct answers. They have a context, and context is usually the first thing a project loses. Without it, the next argument is often the old argument again, held by people who don't know it already happened.&lt;/p&gt;

&lt;p&gt;This matters more the longer a project is meant to live. The people deciding today are unlikely to be the people maintaining it in ten years. Even if they are the same people, memory is a poor dependency.&lt;/p&gt;

&lt;p&gt;An undocumented decision eventually becomes indistinguishable from an arbitrary one. Nobody trusts an arbitrary decision, so sooner or later someone changes it.&lt;/p&gt;

&lt;p&gt;RFCs get treated as a large-project affectation. Something Rust does. Something a standards body does. Overkill for anything smaller. The value has never had much to do with size.&lt;/p&gt;

&lt;p&gt;An RFC captures intent before that intent gets flattened into a diff. A useful one records the problem, the constraints, the paths not taken, and why those paths lost. Implementation detail can live in the code. The logic the implementation is downstream of usually can't.&lt;/p&gt;

&lt;p&gt;Writing RFCs for BigTalk was never the friction. Managing a growing pile of them consistently was: assigning numbers, tracking status, keeping the fortieth document under the same conventions as the first. That kind of upkeep never rivaled the difficulty of a type checker, but it made the collection worse every time it slipped, until the workflow itself became the thing I was fighting.&lt;/p&gt;

&lt;p&gt;So I built a small CLI to stop fighting it: &lt;a href="https://pypi.org/project/rfcman/" rel="noopener noreferrer"&gt;&lt;strong&gt;rfcman&lt;/strong&gt;&lt;/a&gt;. It shipped under the BigTalk name ahead of the compiler. It doesn't do anything clever. It manages RFC lifecycles and keeps their relationships stable through UUIDs, so a document can change shape without losing the reasoning attached to it.&lt;/p&gt;

&lt;p&gt;It exists to remove friction, the same reason a build system exists. Nothing about it was meant to be the interesting part of the project. It just turned out to be the part that let the interesting parts stay honest.&lt;/p&gt;

&lt;p&gt;The code usually stays readable just fine. What disappears is the reasoning that used to hold it together.&lt;/p&gt;

&lt;p&gt;Someone finds an odd abstraction, decides it's unnecessary, and removes it. Months later the problem it was preventing comes back. The system had forgotten why the abstraction was there.&lt;/p&gt;

&lt;p&gt;Software gets evaluated on correctness and performance. Continuity rarely gets the same weight, though it should. A system worth keeping has to preserve why its behavior looks the way it does.&lt;/p&gt;

&lt;p&gt;Source alone doesn't carry that, and neither does documentation once it drifts. What has to survive is a checkable link between decision and implementation, so that in ten years, someone can still find out why.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>languagedesign</category>
      <category>programming</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Why Software That Lasts Is Written for a Future Reader</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:43:55 +0000</pubDate>
      <link>https://dev.to/thechrisin/software-longevity-how-to-design-systems-that-age-well-2kda</link>
      <guid>https://dev.to/thechrisin/software-longevity-how-to-design-systems-that-age-well-2kda</guid>
      <description>&lt;p&gt;Correctness is a low bar. Most systems that reach production clear it. Plenty of them still turn difficult to touch within a few years, because nobody treated time as a design constraint while there was still time to.&lt;/p&gt;

&lt;p&gt;A system lasts when conservative choices made early are kept. What it promises has to stay visible, and so does what it will not promise.&lt;/p&gt;

&lt;p&gt;Every system eventually gets read more than it gets written, by people who weren't in the room for the original decisions. That future reader is the software's real audience. Code that leans on shared context asks that reader for a trust it hasn't earned.&lt;/p&gt;

&lt;p&gt;The code can keep running on explanations that live outside it, and on invariants nobody wrote a boundary for. Every change still turns into a negotiation with its own past.&lt;/p&gt;

&lt;p&gt;Someone opened a helper that computes totals, found two lines in the wrong order, swapped them, and swapped them back, because three callers already depended on the wrong order. The tests still pass. Opening it again means becoming the person who owns the totals.&lt;/p&gt;

&lt;p&gt;Embarrassment is a useful metric for catching this early. Once a file can only be explained by apologizing for leftover urgency, or for context nobody wrote down, avoiding it starts to feel safer than touching it.&lt;/p&gt;

&lt;p&gt;This kind of embarrassment almost always traces back to overreach. Someone stretched a promise too wide, or baked more certainty into a default than the problem actually had.&lt;/p&gt;

&lt;p&gt;Software built to last stays conservative about what it claims to do. It doesn't try to anticipate every future use case, and it won't generalize a function before two real call sites demand it. Repetition is often cheaper than unifying too early. A problem can wait until it's unavoidable. Those are decisions you can still undo.&lt;/p&gt;

&lt;p&gt;A default that stays safe under partial understanding gets used correctly far more often than a powerful one that demands real expertise to avoid misusing. Strong assumptions baked into a default expand what the whole system has promised without anyone recording the expansion, and every one of those promises eventually comes due.&lt;/p&gt;

&lt;p&gt;Exposing internal detail invites coupling even from well-intentioned callers, and that coupling hardens into dependency soon enough. What was once an implementation choice becomes a &lt;a href="https://thechris.in/articles/why-most-plugin-systems-fail-quietly/" rel="noopener noreferrer"&gt;contract by accident&lt;/a&gt;, and changing it later starts to feel like betrayal.&lt;/p&gt;

&lt;p&gt;The systems that survive tend to be the ones that chose, early and often, what not to build. Saying plainly what a system supports and what it puts out of scope is what keeps it legible once other people start changing it.&lt;/p&gt;

&lt;p&gt;Once implicit contracts have piled up, no refactor fully restores a system's &lt;a href="https://thechris.in/articles/on-restraint-boundaries-and-systems-thinking/" rel="noopener noreferrer"&gt;original tractability&lt;/a&gt;. Understanding gets expensive enough that progress just slows to a crawl.&lt;/p&gt;

&lt;p&gt;Every other review a system gets happens once, before it ships. Time keeps reviewing it for years afterward, and it never grades on a curve.&lt;/p&gt;

</description>
      <category>foundational</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Greenfield Rewrites: Why You Lose Institutional Knowledge</title>
      <dc:creator>Chris Roy</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:43:53 +0000</pubDate>
      <link>https://dev.to/thechrisin/greenfield-rewrites-why-you-lose-institutional-knowledge-5h1</link>
      <guid>https://dev.to/thechrisin/greenfield-rewrites-why-you-lose-institutional-knowledge-5h1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every system eventually invites a rewrite.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost never because it broke. Usually because nobody left on the team can change it without holding their breath.&lt;/p&gt;

&lt;p&gt;The structure feels rigid. The decisions feel outdated. The system resists modification in ways nobody can quite explain, and nobody particularly wants to be the one to justify. A new feature means touching six files that shouldn't be related to it. A fix in one corner breaks something two teams over, for reasons that take a week to trace. At some point somebody says the quiet part out loud in a planning meeting, and a clean slate starts to look reasonable. Sensible, even. Overdue.&lt;/p&gt;

&lt;p&gt;Greenfield work makes a good pitch. No history to work around. Every decision made on purpose this time, by people who finally understand the problem the old system was only ever groping toward. I've made this pitch myself, in rooms where everyone was nodding before I finished the sentence. What it delivers, most of the time, is amnesia with a deadline attached.&lt;/p&gt;

&lt;p&gt;A rewrite never starts from zero, whatever the kickoff deck says. It starts from assumptions about what the old system did and which of those things mattered, made by people standing outside the system looking in. That's exactly the vantage point from which those assumptions are hardest to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a mature system is made of
&lt;/h2&gt;

&lt;p&gt;A mature system is more than its code. It's accumulated truth about an environment that never cooperated.&lt;/p&gt;

&lt;p&gt;A workaround here, a strange boundary check there, a comment that just says "don't touch this, see the incident from three years ago." Each one is a lesson the system learned the hard way, usually at somebody's expense. Some of that knowledge is written down somewhere findable. Most of it isn't. It exists as shape: the specific, ugly contour of the code, not something you could hand to a new hire and call documentation.&lt;/p&gt;

&lt;p&gt;When a system gets rewritten, that shape doesn't transfer. It gets rediscovered, piece by piece, the hard way: in production, by whoever's on call the week some corner case the old system absorbed without comment shows up unhandled instead. The new system looks simpler on launch day because it's forgotten what made the old one complicated. Nobody's told it yet.&lt;/p&gt;

&lt;p&gt;In April 2000, &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;Joel Spolsky wrote up&lt;/a&gt; what happened after Netscape decided its browser codebase had become too messy to keep extending, and rewrote it from scratch instead. Version 4.0 shipped in 1997. The rewrite, which became Netscape 6, didn't ship until late 2000: three years with no serious competitive release, in a market where Internet Explorer was being bundled into every copy of Windows for free. Spolsky's point wasn't really about elegance. The ugliest parts of a mature codebase are usually ugly because they encode a fix for a real bug, a workaround for a browser inconsistency nobody remembers reporting, sometimes just a piece of knowledge paid for with a support ticket or an outage. Thrown out in a single motion, in the name of starting clean, because greenfield feels like progress in a way that patching an old system never does. Netscape never recovered the ground it lost in that gap: three years didn't buy the team clarity, it bought Microsoft a market.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of amnesia
&lt;/h2&gt;

&lt;p&gt;By the time the FBI's Virtual Case File project collapsed, the cost of amnesia had an actual dollar figure attached to it, and a congressional report to go with it.&lt;/p&gt;

&lt;p&gt;In 2001, the FBI contracted with Science Applications International Corporation to replace its paper-heavy, mainframe-backed case-management process with a modern application that would let agents and analysts share investigative information electronically across field offices. On paper, this was overdue and obviously correct. The bureau was still moving physical case files by hand in some offices, years after most private-sector organizations had digitized equivalent workflows.&lt;/p&gt;

&lt;p&gt;But the system being replaced held decades of procedural knowledge about how casework moved, how evidence chains had to be documented to hold up in court, and how different units needed different, sometimes conflicting views into the same case. Some of that compartmentalization between field offices existed for reasons that had nothing to do with software and everything to do with the nature of investigations. None of it lived in a specification. It lived in how people used the old system and worked around its limits, and it had to be rediscovered by a team building the replacement from outside, lacking any contact with that experience. The FBI's own understanding of what it needed kept shifting mid-build, because nobody had fully externalized what "what it needed" meant.&lt;/p&gt;

&lt;p&gt;By April 2005, the bureau scrapped the project outright. SAIC had delivered roughly 700,000 lines of code, and &lt;a href="https://www.computerworld.com/article/1719110/it-s-official-fbi-scraps-170m-virtual-case-file-project.html" rel="noopener noreferrer"&gt;most of it turned out to be unusable&lt;/a&gt;: an estimated $105 million of the total $170 million spent produced nothing the FBI could put into service. IEEE Spectrum's &lt;a href="https://spectrum.ieee.org/who-killed-the-virtual-case-file" rel="noopener noreferrer"&gt;postmortem&lt;/a&gt; is worth reading in full for the granular version. It speaks of continuously shifting requirements, no strong technical leadership on the customer's side of the table, and a build that tried to leap directly to a finished system instead of growing into one through anything resembling incremental delivery.&lt;/p&gt;

&lt;p&gt;The FBI eventually did get a working case-management system, from a follow-up effort called Sentinel, delivered in 2012 and built in phases with far tighter scope discipline and closer government oversight of the contractor relationship. The lesson wasn't that government can't build software.&lt;/p&gt;

&lt;p&gt;Nobody, inside government or out, can rewrite a system whose real specification exists nowhere but inside the system being replaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  When greenfield discards the product, not just the code
&lt;/h2&gt;

&lt;p&gt;Digg relaunched as a total rebuild on August 25, 2010, losing the reason people used the product along with the technical knowledge behind it. It had a new design, a new codebase, and new infrastructure. The entire greenfield landed in one release, top to bottom, presented as a clean and overdue modernization. In the process, the team dropped or buried features that had nothing to do with performance and everything to do with what made Digg feel like Digg to the people who used it daily: the "bury" button that let the community collectively downvote content, the "upcoming" page where unproven stories could rise on their own before reaching the front page, the personal history and friend-tracking features longtime users had built habits around. None of them registered as a necessity, or even a consideration, during the rewrite. It also changed who got distribution, giving major publishers a mechanism to auto-submit content directly to the site, which undercut the community-driven ranking that had made Digg worth visiting over any other aggregator.&lt;/p&gt;

&lt;p&gt;Within about a month, &lt;a href="https://www.startupbooted.com/what-happened-to-digg" rel="noopener noreferrer"&gt;Digg had lost close to 30% of its global traffic&lt;/a&gt;, with US traffic down by 26% and UK traffic down by 34% according to some contemporary estimates. A large share of that departing audience went straight to a much younger competitor called Reddit, which had spent years copying the parts of Digg's original mechanic that the rewrite had just thrown away. Digg laid off about 40% of its staff in the aftermath, and Kevin Rose, the founder, resigned not long after. Search Engine Land's &lt;a href="https://searchengineland.com/digg-v4-how-to-successfully-kill-a-community-50450" rel="noopener noreferrer"&gt;contemporary account&lt;/a&gt; of the relaunch reads, in hindsight, like a checklist of nearly everything a community product should never do to itself in a single release.&lt;/p&gt;

&lt;p&gt;The Digg team mistook the product's implementation for the product itself, and underestimated the migration because of it. They rewrote the former with real technical skill, and discovered six brutal weeks later that the latter had been quietly living inside the parts they'd thrown away.&lt;/p&gt;

&lt;p&gt;Longhorn, the internal codename for what eventually shipped as Windows Vista, played out the same failure in slow motion, without a public collapse but with a comparable cost in time. It accumulated features and ambitions for years without anyone drawing a hard boundary around scope, until by mid-2004 it was so far behind that Microsoft &lt;a href="https://www.theregister.com/2025/03/27/looking_back_at_windows_longhorn/" rel="noopener noreferrer"&gt;reset the project outright&lt;/a&gt;, discarding most of what had been built and restarting from the more conservative Windows Server 2003 codebase with a narrower, more defensible target. Vista finally shipped in November 2006, more than five years after Windows XP, at the time the longest gap between major Windows releases in the company's history. The reset was closer to a correction than a rewrite in the Netscape sense. Nobody at Microsoft was disgusted with the old code, exactly; it just arrived years too late, with the company paying twice: once for the original effort, once to partially undo it before building the real thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reversibility is the missing piece
&lt;/h2&gt;

&lt;p&gt;Reversibility is what actually matters here: a property built into a system deliberately, long before anyone needs it, not something a young codebase gets for free.&lt;/p&gt;

&lt;p&gt;A reversible system absorbs change instead of avoiding it. A decision can be revisited without the whole structure coming down around it. A constraint can be loosened without triggering a reconstruction project that eats a year of the roadmap. The system keeps its history instead of severing it every few years out of frustration.&lt;/p&gt;

&lt;p&gt;That takes restraint, and restraint is unglamorous, because it rarely produces a demo anyone gets excited about. Reversible decisions are frequently less expressive than the alternative. They avoid locking in an abstraction before anyone's sure it's the right one, and they deliberately leave gaps where certainty doesn't exist yet. From the outside, that can look like indecision, or a team that can't commit to an architecture. More often it's just a refusal to encode assumptions the team can't defend today, and almost certainly won't be able to defend in two years either.&lt;/p&gt;

&lt;p&gt;Twitter's original backend, written in Ruby on Rails, became infamous for outages severe enough that the fail whale error page turned into a cultural mascot for the whole Web 2.0 era. The company's Ruby-based message queue "hit a wall" as early as 2008, in the words of one Twitter developer at the time, unable to keep up with long-running, memory-intensive work at scale, under exactly the kind of pressure that tends to produce a Netscape-style decision. The obvious, tempting move was the Netscape move: declare Rails the problem, freeze feature work, rewrite the whole backend from scratch. Twitter didn't do that. Engineers migrated pieces of the backend onto a new stack built on the JVM, largely in Scala, component by component, over several years, eventually reconfiguring the system so that all mobile traffic bypassed the Ruby stack entirely. The proof came on election night in November 2012, historically one of Twitter's highest-traffic events. Users posted at a sustained average of nearly 10,000 tweets per second for a full hour, peaking at 874,560 tweets in a single minute. There was no clean launch event, no dramatic relaunch post. &lt;a href="https://www.theregister.com/2012/11/08/twitter_epic_traffic_saved_by_java/" rel="noopener noreferrer"&gt;The service just held&lt;/a&gt;, without the fail whale making an appearance even once, the entire time it was being rebuilt underneath its own users.&lt;/p&gt;

&lt;p&gt;Stripe's internal implementation changes constantly. What doesn't move is the promise made to everyone who's already integrated against an older version of the API: engineers write against the current internal version only, and a compatibility layer transforms each outgoing response into whatever version a given client originally requested, sometimes years out of date.&lt;/p&gt;

&lt;p&gt;Stripe has &lt;a href="https://stripe.com/blog/api-versioning" rel="noopener noreferrer"&gt;described this&lt;/a&gt; as treating the API itself as infrastructure, something that has to keep working for people who built on it years ago no matter how much has changed underneath since. The public API hasn't needed a breaking rewrite in more than a decade.&lt;/p&gt;

&lt;p&gt;Around 2015, Segment split a core piece of its product into more than 140 separate services, following the fairly common belief at the time that this was simply how serious companies build at scale. The promised velocity never showed up. Instead, three full-time engineers ended up spending most of their working hours just keeping the constellation of services alive, and a single change to a shared library meant coordinating redeployment across all 140-plus of them. In 2018 the team &lt;a href="https://www.twilio.com/en-us/blog/developers/best-practices/goodbye-microservices" rel="noopener noreferrer"&gt;wrote publicly&lt;/a&gt; about collapsing that architecture back into a single service. Deploys that used to require coordinating dozens of independently versioned services now took one engineer a few minutes, alone. Nobody had designed reversibility into the original split. Segment simply treated the decision itself as something that could still be undone once it was clearly a mistake, and corrected it once enough people were willing to say so plainly.&lt;/p&gt;

&lt;p&gt;Shopify never made the split in the first place. Facing the same pressure every sufficiently large monolith eventually faces (in Shopify's case, a Ruby on Rails codebase worked on by more than a thousand engineers across more than a decade), the company seriously considered breaking the monolith into microservices and concluded the operational cost wasn't worth what it would buy them. Instead, it built &lt;a href="https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity" rel="noopener noreferrer"&gt;an internal tool called Packwerk&lt;/a&gt; to enforce hard boundaries between modules living inside the same codebase, giving individual teams the isolation benefits normally associated with separate services: clear ownership, enforced interfaces, the ability to reason about one module without loading the entire system into your head at once, without paying for separate deploys, separate on-call rotations, or the failure modes that come standard with a distributed system. Shopify's own engineering team has described this as ongoing, unglamorous maintenance rather than a finished migration with a launch date; their &lt;a href="https://shopify.engineering/shopify-monolith" rel="noopener noreferrer"&gt;account of the effort&lt;/a&gt; reads less like a triumphant rewrite story and more like a maintenance log that never quite ends, which is closer to how large systems stay healthy in practice. Nobody had to freeze feature work for a year to make any of it happen.&lt;/p&gt;

&lt;p&gt;This is close to what Martin Fowler described back in 2004 as the strangler application, and retitled years later as the &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;strangler fig pattern&lt;/a&gt;. It's named for a vine that germinates in the canopy of a host tree and gradually grows down and around it, eventually taking the host's place without the tree ever being felled outright in one motion. Route traffic to new code piece by piece. Let the old system keep running underneath while it's dismantled from the edges inward, module by module, service by service. Every step stays small enough to reverse if it turns out to be the wrong step, and the system never once stops being live while the work happens around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a rewrite is earned
&lt;/h2&gt;

&lt;p&gt;Starting over isn't always the wrong call, either. Greenfield becomes necessary the moment reversibility is already gone. Once decisions can't be undone without triggering cascading failure, or constraints have grown so entangled that no single one can be pulled loose without dragging the others with it, a greenfield rewrite is earned. It's an admission that the system stopped being changeable some time ago, and the rewrite is just the moment everyone in the room finally says so out loud instead of stalling for one more quarter.&lt;/p&gt;

&lt;p&gt;The Netscapes and the Diggs and the Virtual Case Files of the world had already lost the ability to change safely, long before anyone rewrote anything. The rewrite just finished the job of discarding what still worked along with what didn't, because from inside a system that's stopped being legible, it's hard to tell which is which until it's too late.&lt;/p&gt;

&lt;p&gt;In the end, a greenfield rewrite shouldn't be a choice made on impulse. It should follow the swan song of a system that held onto its ability to change for as long as it could.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://thechris.in/articles/on-building-software-that-deserves-to-last/" rel="noopener noreferrer"&gt;systems that deserve to last&lt;/a&gt; are the ones that treated rebuilding as a last resort instead of a first instinct, and that's a property you have to design for, deliberately, long before the day you'd need it.&lt;/p&gt;

&lt;p&gt;An edited version of this article is on &lt;a href="https://hackernoon.com/the-hidden-cost-of-software-rewrites" rel="noopener noreferrer"&gt;Hackernoon&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Joel Spolsky, &lt;a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/" rel="noopener noreferrer"&gt;"Things You Should Never Do, Part I,"&lt;/a&gt; Joel on Software, April 2000.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.computerworld.com/article/1719110/it-s-official-fbi-scraps-170m-virtual-case-file-project.html" rel="noopener noreferrer"&gt;"It's official: FBI scraps $170M Virtual Case File project,"&lt;/a&gt; Computerworld.&lt;/li&gt;
&lt;li&gt;Harry Goldstein, &lt;a href="https://spectrum.ieee.org/who-killed-the-virtual-case-file" rel="noopener noreferrer"&gt;"Who Killed the Virtual Case File?,"&lt;/a&gt; IEEE Spectrum.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.startupbooted.com/what-happened-to-digg" rel="noopener noreferrer"&gt;"What Happened to Digg? The Rise, Fall, and Surprising Comeback Story,"&lt;/a&gt; Startup Booted.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://searchengineland.com/digg-v4-how-to-successfully-kill-a-community-50450" rel="noopener noreferrer"&gt;"Digg v4: How To Successfully Kill A Community,"&lt;/a&gt; Search Engine Land, 2010.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.theregister.com/2025/03/27/looking_back_at_windows_longhorn/" rel="noopener noreferrer"&gt;"Looking back at Windows Longhorn,"&lt;/a&gt; The Register, 2025.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.theregister.com/2012/11/08/twitter_epic_traffic_saved_by_java/" rel="noopener noreferrer"&gt;"Twitter survives election after Ruby-to-Java move,"&lt;/a&gt; The Register, November 2012.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stripe.com/blog/api-versioning" rel="noopener noreferrer"&gt;"APIs as infrastructure: future-proofing Stripe with versioning,"&lt;/a&gt; Stripe.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.twilio.com/en-us/blog/developers/best-practices/goodbye-microservices" rel="noopener noreferrer"&gt;"Goodbye Microservices,"&lt;/a&gt; Twilio/Segment Engineering.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.engineering/deconstructing-monolith-designing-software-maximizes-developer-productivity" rel="noopener noreferrer"&gt;"Deconstructing the Monolith: Designing Software that Maximizes Developer Productivity,"&lt;/a&gt; Shopify Engineering.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shopify.engineering/shopify-monolith" rel="noopener noreferrer"&gt;"Under Deconstruction: The State of Shopify's Monolith,"&lt;/a&gt; Shopify Engineering, 2020.&lt;/li&gt;
&lt;li&gt;Martin Fowler, &lt;a href="https://martinfowler.com/bliki/StranglerFigApplication.html" rel="noopener noreferrer"&gt;"StranglerFigApplication,"&lt;/a&gt; 2004, retitled 2019.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>programming</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
