[{"data":1,"prerenderedAt":3828},["ShallowReactive",2],{"navigation":3,"blog-list":141},[4,36,55,80,101,116,128],{"title":5,"path":6,"stem":7,"children":8,"icon":35},"ASP.NET Core","/aspnet-core","1.aspnet-core/1.index",[9,11,15,19,23,27,31],{"title":10,"path":6,"stem":7},"Cursor Pagination",{"title":12,"path":13,"stem":14},"Dapper - Micro ORM","/aspnet-core/dapper","1.aspnet-core/2.dapper",{"title":16,"path":17,"stem":18},"DbUp - Database Migrations","/aspnet-core/dbup","1.aspnet-core/3.dbup",{"title":20,"path":21,"stem":22},"Serilog - Structured Logging for .NET","/aspnet-core/serilog","1.aspnet-core/4.serilog",{"title":24,"path":25,"stem":26},"Class Inheritance in C#","/aspnet-core/inheritance","1.aspnet-core/5.inheritance",{"title":28,"path":29,"stem":30},"C# Collections","/aspnet-core/collections","1.aspnet-core/6.collections",{"title":32,"path":33,"stem":34},".NET gitignore Command","/aspnet-core/gitignore","1.aspnet-core/7.gitignore",false,{"title":37,"path":38,"stem":39,"children":40,"icon":35},"Oauth2.0 and OpenIdConnect","/oauth2andopenidconnect","10.oauth2andopenidconnect/1.index",[41,43,47,51],{"title":42,"path":38,"stem":39},"RFC 6749 - OAuth 2.0 Authorization Framework",{"title":44,"path":45,"stem":46},"Authentication (AuthN) vs Authorization (AuthZ)","/oauth2andopenidconnect/authnandauthz","10.oauth2andopenidconnect/2.authnandauthz",{"title":48,"path":49,"stem":50},"TOTP Authentication","/oauth2andopenidconnect/totp","10.oauth2andopenidconnect/3.totp",{"title":52,"path":53,"stem":54},"OAuth 2.0 Resource Indicators - RFC 8707","/oauth2andopenidconnect/oauth2resourceindicators","10.oauth2andopenidconnect/4.OAuth2ResourceIndicators",{"title":56,"path":57,"stem":58,"children":59,"page":35},"Azure","/azure","2.azure",[60,64,68,72,76],{"title":61,"path":62,"stem":63},"Azure Cost Management","/azure/azuremanagementandgovernance","2.azure/1.Azuremanagementandgovernance",{"title":65,"path":66,"stem":67},"Azure Policy","/azure/azurepolicy","2.azure/2.azurepolicy",{"title":69,"path":70,"stem":71},"Code Blocks","/azure/code-blocks","2.azure/3.code-blocks",{"title":73,"path":74,"stem":75},"Prose Components","/azure/prose-components","2.azure/4.prose-components",{"title":77,"path":78,"stem":79},"Images and Embeds","/azure/images-embeds","2.azure/5.images-embeds",{"title":81,"path":82,"stem":83,"children":84,"page":35},"Git","/git","3.git",[85,89,93,97],{"title":86,"path":87,"stem":88},"Git Rebase","/git/git-rebase","3.git/1.git-rebase",{"title":90,"path":91,"stem":92},"Git Stash","/git/git-stash","3.git/2.git-stash",{"title":94,"path":95,"stem":96},"SemVer","/git/semver","3.git/3.semver",{"title":98,"path":99,"stem":100},"Conventional Commits","/git/conventional-commits","3.git/4.conventional-commits",{"title":102,"path":103,"stem":104,"children":105,"icon":35},"Design Patterns","/design-patterns","7.design-patterns/1.index",[106,108,112],{"title":107,"path":103,"stem":104},"Introduction",{"title":109,"path":110,"stem":111},"Installation","/design-patterns/installation","7.design-patterns/2.installation",{"title":113,"path":114,"stem":115},"Usage","/design-patterns/usage","7.design-patterns/3.usage",{"title":117,"path":118,"stem":119,"children":120,"icon":35},"Software Principles","/principles","8.principles/1.index",[121,122,125],{"title":107,"path":118,"stem":119},{"title":109,"path":123,"stem":124},"/principles/installation","8.principles/2.installation",{"title":113,"path":126,"stem":127},"/principles/usage","8.principles/3.usage",{"title":129,"path":130,"stem":131,"children":132,"icon":35},"Software Architecture","/architecture","9.architecture/1.index",[133,135,138],{"title":134,"path":130,"stem":131},"CQRS Pattern",{"title":109,"path":136,"stem":137},"/architecture/installation","9.architecture/2.installation",{"title":113,"path":139,"stem":140},"/architecture/usage","9.architecture/3.usage",[142,319,464,629,781,920,1150,1294,1423,1561,1738,1863,1997,2166,2358,2501,2803,2967,3126,3260,3627],{"id":143,"title":144,"body":145,"date":309,"description":310,"extension":311,"featured":35,"meta":312,"navigation":313,"path":314,"readTime":315,"seo":316,"stem":317,"tag":117,"__hash__":318},"blog/blog/21.merchant-of-dependencies.md","You Are Hired to Build Software, Not to Become a Merchant of Dependencies",{"type":146,"value":147,"toc":292},"minimark",[148,152,157,160,164,167,171,174,178,181,185,188,214,218,221,225,228,232,235,239,242,246,249,253,256,259,263],[149,150,151],"p",{},"A dependency is never free. Every package added to a codebase becomes part of the software that team now owns — not in a legal sense, but in the sense that matters at 2 a.m. when something breaks: nobody's users care that the failure originated three layers down in someone else's code.",[153,154,156],"h2",{"id":155},"the-ecosystem-is-built-to-make-adding-easy","The Ecosystem Is Built to Make Adding Easy",[149,158,159],{},"Need a form? Add a package. Need validation? Add a package. Need a small utility? Add a package. Every modern package ecosystem is engineered to make this frictionless — a search, an install command, and the feature works. That's a genuine strength of modern tooling. It's also exactly why the cost of a dependency doesn't show up when it's added. It shows up later, and by then it's spread across a dependency tree nobody fully mapped at the time.",[153,161,163],{"id":162},"what-actually-gets-inherited","What Actually Gets Inherited",[149,165,166],{},"Adding a package doesn't just add functionality — it adds another maintainer whose priorities aren't yours, another release cycle to track, another surface for vulnerabilities to enter through, another set of licensing terms to be aware of, and an entire transitive dependency tree that's usually far larger than the one direct import suggests. The question worth asking isn't just \"can this package solve my problem.\" It's \"is this package worth owning\" — because functionally, once it's in the tree, it is owned, whether or not anyone signed up for that explicitly.",[153,168,170],{"id":169},"the-convenience-trap-and-why-its-invisible-at-the-time","The Convenience Trap, and Why It's Invisible at the Time",[149,172,173],{},"The pattern is familiar: need a feature, search the registry, install the package, problem solved — and for a while, everything genuinely does feel more productive. Six months later, that single dependency has pulled in three or four more, one of them is carrying a known vulnerability, and the team is spending its time resolving version conflicts instead of shipping anything new. The convenience didn't disappear. It moved into the future, compounding the whole time nobody was looking at it.",[153,175,177],{"id":176},"transitive-dependencies-turn-one-choice-into-an-ecosystem","Transitive Dependencies Turn One Choice Into an Ecosystem",[149,179,180],{},"Most developers who add a single package believe they've added one thing. They haven't — they've added everything that package depends on, and everything those dependencies depend on in turn. A direct dependency can be actively maintained and well-audited while something four layers beneath it is abandoned, unmaintained, or quietly carrying a disclosed CVE nobody upstream has patched yet. This is precisely why dependency management gets harder as a system grows: the risk isn't concentrated in the choices a team made directly. It's distributed across choices made by people that team has never heard of, several layers removed from any decision they actually made.",[153,182,184],{"id":183},"this-isnt-a-framework-specific-problem","This Isn't a Framework-Specific Problem",[149,186,187],{},"It's tempting to think of dependency sprawl as a JavaScript ecosystem quirk, or something specific to whichever framework happens to be top of mind — but the pattern repeats identically across every ecosystem that makes installation easy, which today is essentially all of them. A Flutter developer reaching for a package to avoid writing a form manually inherits state-management assumptions and upgrade constraints along with it. A .NET team adding NuGet packages without discipline ends up fighting version conflicts and security-scanning noise that has nothing to do with the actual feature those packages were meant to deliver. Python's pip, Rust's Cargo, Go's module system, Ruby's gems — every one of them optimizes for the same frictionless install experience, and every one of them produces the same long-term liability when that ease isn't paired with actual judgment about what's being pulled in.",[149,189,190,191,195,196,203,204,207,208,213],{},"Two real incidents make the abstract risk concrete. In 2016, a developer unpublished a package called ",[192,193,194],"code",{},"left-pad"," from npm — eleven lines of code, no dependencies of its own — after a dispute over an unrelated package name. Thousands of projects, including foundational tools like Babel and React, ",[197,198,202],"a",{"href":199,"rel":200},"https://en.wikipedia.org/wiki/Npm_left-pad_incident",[201],"nofollow","failed to build within minutes",", because a trivial utility had quietly become part of an enormous number of dependency trees without anyone auditing how deep the reliance actually went. In 2024, a contributor who had spent nearly two years patiently building trust within the ",[192,205,206],{},"xz-utils"," project was handed maintainer access and used it to insert a ",[197,209,212],{"href":210,"rel":211},"https://www.akamai.com/blog/security-research/critical-linux-backdoor-xz-utils-discovered-what-to-know",[201],"deliberate backdoor"," into a compression library present on nearly every major Linux distribution — caught only because a Microsoft engineer happened to notice SSH logins running half a second slower than expected during routine benchmarking. Neither incident required a team to write bad code. Both required only that a team depend on something they didn't fully own the risk profile of.",[153,215,217],{"id":216},"not-every-problem-deserves-a-package","Not Every Problem Deserves a Package",[149,219,220],{},"A useful test: could this be implemented clearly, in a few lines of code the team can actually read and maintain? A small validation helper, a simple mapping function, a basic date utility, a thin wrapper around one API call — these are frequently faster to write directly than to evaluate, install, and then maintain as an external dependency for years. Reaching for a package to solve a problem that takes minutes to solve directly is a trade: minutes saved today against years of upgrade cycles, security advisories, and version conflicts accepted on behalf of a future team that never got a say in the decision.",[153,222,224],{"id":223},"someone-already-solved-this-isnt-the-same-as-this-is-the-right-solution-for-us","\"Someone Already Solved This\" Isn't the Same as \"This Is the Right Solution for Us\"",[149,226,227],{},"It's often true that a problem has already been solved by someone else. That's not, on its own, a reason to adopt their solution. A mature decision weighs the complexity actually being added, who on the team will own understanding it long-term, how familiar the team already is with it, what it does to the security surface, and how often it needs attention — not just whether a working package exists somewhere in a registry.",[153,229,231],{"id":230},"libraries-should-have-to-earn-their-place","Libraries Should Have to Earn Their Place",[149,233,234],{},"Good reasons to add a dependency: complex cryptography that shouldn't be reimplemented by hand, standards implementations, database drivers, mature frameworks, genuinely difficult algorithms, anything requiring specialist expertise a team doesn't have and doesn't need to build in-house. Weak reasons: replacing ten lines of straightforward code, avoiding the work of actually understanding a problem, following whatever's popular this year, or copying another project's dependency list without asking whether the reasoning still applies.",[153,236,238],{"id":237},"ownership-doesnt-stop-at-the-import-statement","Ownership Doesn't Stop at the Import Statement",[149,240,241],{},"When an application depends on a package and that package fails, users experience exactly one thing: the system they were using stopped working. They don't see, and don't care, that the root cause traces back to code the team never wrote. Depending on external code doesn't transfer responsibility for what happens when it breaks — it just changes who has the least visibility into why.",[153,243,245],{"id":244},"engineering-judgment-isnt-measured-by-installation-count","Engineering Judgment Isn't Measured by Installation Count",[149,247,248],{},"Anyone can run an install command. The actual skill is knowing when not to, when to remove a dependency that's stopped earning its place, when to build something small internally instead, and when accepting external ownership is genuinely the right trade. The best engineers aren't the ones who know the most libraries by name. They're the ones who understand what each dependency actually costs before it's added, not after it's already load-bearing.",[153,250,252],{"id":251},"the-takeaway","The Takeaway",[149,254,255],{},"A codebase full of dependencies isn't automatically sophisticated — sometimes it's just a large, distributed collection of future maintenance work, waiting for the day one of those hundred packages stops being maintained, gets compromised, or breaks in a way nobody on the team is positioned to fix quickly. Use libraries where they provide real, specific value. Skip them where a clear, small, owned solution is genuinely enough. Every package is a decision made on behalf of whoever maintains this system next — including, often, the same team that added it, several years and several context-switches later.",[257,258],"hr",{},[153,260,262],{"id":261},"references-and-further-reading","References and Further Reading",[264,265,266,275,283],"ul",{},[267,268,269,270,274],"li",{},"Wikipedia, ",[197,271,273],{"href":199,"rel":272},[201],"\"Npm Left-Pad Incident\""," — how an eleven-line utility package broke builds across the JavaScript ecosystem when it was unpublished in 2016.",[267,276,277,278,282],{},"Akamai, ",[197,279,281],{"href":210,"rel":280},[201],"\"XZ Utils Backdoor — Everything You Need to Know\""," — a detailed account of the 2024 supply-chain attack in which a trusted maintainer position was used to insert a backdoor into a foundational compression library.",[267,284,285,286,291],{},"arXiv, ",[197,287,290],{"href":288,"rel":289},"https://arxiv.org/html/2504.17473v1",[201],"\"Wolves in the Repository: A Software Engineering Analysis of the XZ Utils Supply Chain Attack\""," — an academic breakdown of how the attack exploited the open-source maintenance process itself, not just the code.",{"title":293,"searchDepth":294,"depth":295,"links":296},"",1,2,[297,298,299,300,301,302,303,304,305,306,307,308],{"id":155,"depth":295,"text":156},{"id":162,"depth":295,"text":163},{"id":169,"depth":295,"text":170},{"id":176,"depth":295,"text":177},{"id":183,"depth":295,"text":184},{"id":216,"depth":295,"text":217},{"id":223,"depth":295,"text":224},{"id":230,"depth":295,"text":231},{"id":237,"depth":295,"text":238},{"id":244,"depth":295,"text":245},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-14","Adding a package doesn't just add functionality — it adds another maintainer, another release cycle, another transitive tree nobody fully mapped. left-pad and xz-utils are what that cost looks like when it finally comes due.","md",{},true,"/blog/merchant-of-dependencies","8 min",{"title":144,"description":310},"blog/21.merchant-of-dependencies","2yxmTdP9KE6L9VuNftTBm8XEWw11Ehx4UaQmNvMyJ8Y",{"id":320,"title":321,"body":322,"date":455,"description":456,"extension":311,"featured":35,"meta":457,"navigation":313,"path":458,"readTime":459,"seo":460,"stem":461,"tag":462,"__hash__":463},"blog/blog/20.too-many-security-options.md","Too Many Security Options Can Become a Security Problem",{"type":146,"value":323,"toc":443},[324,327,331,343,347,350,359,363,372,376,379,383,386,390,393,397,400,404,407,409,412,414,416],[149,325,326],{},"A security standard that offers unlimited choices without strong defaults can unintentionally build weaker systems than one that offers fewer, better-recommended ones. That sounds counterintuitive — more options should mean more ways to get it right — but it consistently doesn't work out that way in practice, and there's good reason both from security engineering and from decision-making research to explain why.",[153,328,330],{"id":329},"you-can-use-whichever-works-best-sounds-flexible-it-isnt-free","\"You Can Use Whichever Works Best\" Sounds Flexible. It Isn't Free.",[149,332,333,334,338,339,342],{},"Security guidance often reads like a menu: API keys, shared secrets, mTLS, Private Key JWT, DPoP — pick whichever fits your situation. Technically, that's flexibility. Practically, most organizations will gravitate toward whichever option is easiest to implement, cheapest to operate, most familiar to their developers, and simplest to explain internally — not necessarily whichever option provides the strongest guarantees. That's not a criticism of the engineers making the call. It's what happens by default when a standard describes what's ",[335,336,337],"em",{},"possible"," without also being clear about what's ",[335,340,341],{},"recommended",".",[153,344,346],{"id":345},"the-secure-choice-is-rarely-the-easy-choice","The Secure Choice Is Rarely the Easy Choice",[149,348,349],{},"Engineering teams operate under real constraints — deadlines, budgets, existing systems, and limited in-house security expertise, on top of ordinary pressure to ship features. When a standard presents five valid approaches side by side, the path with the least friction usually wins, even when a harder path offers meaningfully better guarantees. A configuration change beats standing up certificate lifecycle management, which beats building out cryptographic key infrastructure from scratch — not because teams are careless, but because humans reliably optimize for reducing complexity when a decision doesn't come with a clear default telling them not to.",[149,351,352,353,358],{},"This isn't a uniquely engineering phenomenon. Psychologist Barry Schwartz's research on the ",[197,354,357],{"href":355,"rel":356},"https://thedecisionlab.com/reference-guide/economics/the-paradox-of-choice",[201],"paradox of choice"," found that beyond a certain point, more options don't produce better decisions — they produce more cognitive effort, more decision fatigue, and a stronger pull toward whichever option requires the least deliberation to select. Security standards that present a long list of equally \"supported\" mechanisms are asking every implementing team to do the comparative risk analysis the standard itself could have done once, centrally, and didn't.",[153,360,362],{"id":361},"good-security-design-assumes-defaults-get-chosen-not-every-option","Good Security Design Assumes Defaults Get Chosen, Not Every Option",[149,364,365,366,371],{},"This is exactly why \"secure by default\" has become a load-bearing principle in modern security engineering rather than a nice-to-have. The UK's National Cyber Security Centre frames it directly: technology that's ",[197,367,370],{"href":368,"rel":369},"https://www.ncsc.gov.uk/information/secure-default",[201],"secure by default"," has the best security it can offer without the user needing to know it's there or having to turn it on — security shouldn't require extensive configuration to work, and it shouldn't depend on every implementer independently discovering and applying the same recommendation. Good defaults look like secure cookie settings enabled out of the box, modern TLS preferred automatically, strong password hashing as the unconfigured baseline, and least privilege as the starting assumption rather than something bolted on later. Poor defaults look like insecure options left enabled, deprecated algorithms still accepted, and meaningful controls requiring someone to remember to switch them on. A system shouldn't depend on every developer independently remembering every best practice — that's not a security architecture, that's a hope.",[153,373,375],{"id":374},"flexibility-without-guidance-fragments-an-entire-ecosystem","Flexibility Without Guidance Fragments an Entire Ecosystem",[149,377,378],{},"The cost compounds once multiple organizations are implementing against the same standard independently. Tell a set of financial institutions \"you may use shared secrets, mTLS, Private Key JWT, or DPoP\" without a strong recommendation, and the predictable result is that different banks land on different answers — not because one chose wrong and the others chose right, but because the standard never told any of them which choice it actually preferred. Every integration between those institutions now carries different security assumptions, and the ecosystem's overall strength gets pulled down toward whichever choice was easiest, not up toward whichever choice was strongest. Complexity that could have been resolved once, in the standard itself, gets pushed out into every individual implementation instead.",[153,380,382],{"id":381},"supported-is-not-the-same-word-as-recommended","\"Supported\" Is Not the Same Word as \"Recommended\"",[149,384,385],{},"One of the more dangerous phrases in security documentation is simply \"this is supported.\" Supported doesn't mean equally secure, equally appropriate for new systems, or equally likely to hold up under audit. A legacy mechanism can remain supported for compatibility reasons for years after it's stopped being the right default for anything new — and a standard that doesn't clearly separate \"still works for existing integrations\" from \"what you should build today\" leaves that judgment call to whoever's implementing it, at exactly the moment they have the least context to make it well.",[153,387,389],{"id":388},"a-standard-is-only-as-strong-as-what-gets-actually-deployed","A Standard Is Only as Strong as What Gets Actually Deployed",[149,391,392],{},"A technically pristine design nobody implements correctly isn't effective security — it's a specification. The useful question for anyone writing a security standard isn't just \"what does this make possible,\" it's \"what will most organizations actually deploy, given real constraints,\" because the weakest widely-adopted implementation quietly becomes the real security boundary the whole ecosystem is operating under, regardless of what the strongest possible implementation could have achieved.",[153,394,396],{"id":395},"why-financial-grade-apis-went-the-opinionated-route","Why Financial-Grade APIs Went the Opinionated Route",[149,398,399],{},"This is a large part of why FAPI didn't just add more options to OAuth — it narrowed them. The industry had already learned that bearer tokens can be stolen, shared secrets leak, and authentication alone doesn't prove enough about a given request. The response wasn't a longer menu. It was a smaller, stronger one: sender-constrained tokens, cryptographic client authentication, transaction binding, and stronger authorization flows, presented as what a compliant implementation should actually do rather than one item on a long list of acceptable options. The goal was never more choice. It was less ambiguity about what actually protects a high-value transaction.",[153,401,403],{"id":402},"security-should-be-opinionated","Security Should Be Opinionated",[149,405,406],{},"In most of software engineering, flexibility is a virtue. In security, unmanaged flexibility is closer to a liability, because the cost of a bad default doesn't show up as a minor inconvenience — it shows up as a breach. A strong security recommendation states its default plainly — use this approach, because it provides these specific guarantees — and then explains where alternatives exist and for which real constraints they're actually justified. It doesn't hand over ten equally-weighted options and leave the risk analysis as an exercise for the reader.",[153,408,252],{"id":251},[149,410,411],{},"Security isn't improved by adding more choices to a standard — sometimes it's actively weakened by it, because more options mean more opportunities to select convenience over protection, and every one of those individual choices is invisible until something goes wrong. The strongest systems are built when the secure option is clearly recommended, well documented, reasonably easy to implement, and secure without anyone having to opt in. Flexibility still has its place — real constraints are real, and no single mechanism fits every situation. But when the thing being protected is financial data, identity, or a system the rest of an ecosystem depends on, a clear default beats an open menu every time.",[257,413],{},[153,415,262],{"id":261},[264,417,418,426,434],{},[267,419,420,421,425],{},"National Cyber Security Centre (UK), ",[197,422,424],{"href":368,"rel":423},[201],"\"Secure by Default\""," — the principle that security should require minimal configuration and hold by default rather than depend on manual activation.",[267,427,428,429,433],{},"The Decision Lab, ",[197,430,432],{"href":355,"rel":431},[201],"\"The Paradox of Choice\""," — Barry Schwartz's research on why more options increase decision fatigue and push people toward the path of least resistance.",[267,435,436,437,442],{},"Google Cloud, ",[197,438,441],{"href":439,"rel":440},"https://docs.cloud.google.com/architecture/framework/security/implement-security-by-design",[201],"\"Implement Security by Design\""," — a practical distinction between \"secure by design\" and \"secure by default,\" and how the two work together.",{"title":293,"searchDepth":294,"depth":295,"links":444},[445,446,447,448,449,450,451,452,453,454],{"id":329,"depth":295,"text":330},{"id":345,"depth":295,"text":346},{"id":361,"depth":295,"text":362},{"id":374,"depth":295,"text":375},{"id":381,"depth":295,"text":382},{"id":388,"depth":295,"text":389},{"id":395,"depth":295,"text":396},{"id":402,"depth":295,"text":403},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-13","A standard offering unlimited choices without strong defaults can build weaker systems than one offering fewer, better-recommended ones. Why FAPI narrowed OAuth's options instead of adding more, and what the paradox of choice has to do with it.",{},"/blog/too-many-security-options","7 min",{"title":321,"description":456},"blog/20.too-many-security-options","Security","zZzzmcTKdkTKBY9K928_QOECgE4Z80KCwOCi4zKTmeU",{"id":465,"title":466,"body":467,"date":620,"description":621,"extension":311,"featured":35,"meta":622,"navigation":313,"path":623,"readTime":624,"seo":625,"stem":626,"tag":627,"__hash__":628},"blog/blog/19.fapi-transaction-binding.md","Why FAPI Moved From \"Authenticate the Client\" to \"Bind the Transaction to the Client\"",{"type":146,"value":468,"toc":608},[469,472,476,479,486,490,497,501,510,514,522,530,541,557,561,564,568,571,573,580,582,584],[149,470,471],{},"Knowing who's calling an API used to be enough. In financial systems, it isn't anymore — what's needed instead is confidence that a specific action is tied to the legitimate client, the legitimate user, and the exact context it was actually approved in. That shift, from authenticating a caller to binding a transaction, is the throughline connecting most of what the Financial-grade API (FAPI) profiles actually require.",[153,473,475],{"id":474},"the-old-question-was-is-this-client-authenticated","The Old Question Was \"Is This Client Authenticated?\"",[149,477,478],{},"Traditional API security starts simple: authenticate the client, issue a token, let the client call the API with that token. For a huge share of applications, that's genuinely sufficient — reading a public feed or updating a user's display name doesn't need much more than proof that a legitimate, recognized client is making the request.",[149,480,481,482,485],{},"A bank transfer isn't that kind of request. A payment isn't just an HTTP call with a JSON body attached, and a customer identity change isn't a routine field update — each is a high-impact, often irreversible action, and \"the client that called this endpoint was authenticated at some point\" doesn't actually answer the question that matters: was ",[335,483,484],{},"this specific action"," legitimately authorized, by the right user, in the right context, right now?",[153,487,489],{"id":488},"private-key-jwt-proving-ownership-not-just-presenting-a-password","Private Key JWT: Proving Ownership, Not Just Presenting a Password",[149,491,492,493,496],{},"The first piece of this shift replaces a shared secret with cryptographic proof of client ownership. Instead of a ",[192,494,495],{},"client_secret"," both sides hold, the client signs a JWT with a private key only it possesses, and the authorization server verifies that signature against the client's registered public key. The server isn't trusting a password anyone with access to a config file could copy — it's verifying that whoever's making this specific request actually controls the private key tied to this specific client's registered identity.",[153,498,500],{"id":499},"mtls-binding-identity-at-the-connection-level","mTLS: Binding Identity at the Connection Level",[149,502,503,504,509],{},"Mutual TLS pushes proof-of-possession down a layer, binding identity to the TLS connection itself rather than to an individual signed token. The client presents a certificate; the server verifies its validity, its ownership, and its chain of trust, and the resulting connection is cryptographically tied to that authenticated client for its duration. ",[197,505,508],{"href":506,"rel":507},"https://datatracker.ietf.org/doc/html/rfc8705",[201],"RFC 8705"," formalizes this for OAuth specifically — certificate-bound access tokens that a resource server can confirm were actually issued to, and are being presented by, the same client that established the mTLS connection. It's a natural fit for bank-to-bank integrations and other controlled enterprise environments where certificate infrastructure is already part of how systems talk to each other.",[153,511,513],{"id":512},"dpop-binding-the-token-to-a-key-at-the-request-level","DPoP: Binding the Token to a Key at the Request Level",[149,515,516,521],{},[197,517,520],{"href":518,"rel":519},"https://datatracker.ietf.org/doc/html/rfc9449",[201],"DPoP (RFC 9449)"," takes the same underlying idea — prove possession of a key, not just presentation of a credential — and applies it per HTTP request. The client attaches a signed proof to each call, covering the method, the URI, a timestamp, and a hash of the access token itself, all signed with a private key bound to that token at issuance. A stolen token, on its own, is worthless to an attacker who doesn't also hold the key that has to sign every request made with it.",[153,523,525,526,529],{"id":524},"rich-authorization-requests-binding-the-transaction-not-just-the-session","Rich Authorization Requests: Binding the ",[335,527,528],{},"Transaction",", Not Just the Session",[149,531,532,533,536,537,540],{},"This is where FAPI's shift becomes most explicit, and it's the piece easy to miss if the story stops at \"who's the client.\" Client authentication and proof-of-possession answer ",[335,534,535],{},"who's calling",". They say nothing about ",[335,538,539],{},"what, exactly, was approved",". Traditional OAuth scopes are coarse — \"payments:write\" authorizes payment-initiation in general, but says nothing about which account, which amount, or which recipient a user actually consented to.",[149,542,543,548,549,552,553,556],{},[197,544,547],{"href":545,"rel":546},"https://datatracker.ietf.org/doc/html/rfc9396",[201],"RFC 9396, Rich Authorization Requests (RAR)",", closes exactly that gap. It introduces a structured ",[192,550,551],{},"authorization_details"," parameter that lets a client specify precisely what's being authorized — a payment of a specific amount, to a specific account, in a specific currency — as part of the authorization request itself, rather than relying on a generic scope string. A payment request under RAR isn't \"this client has payment-initiation access.\" It's \"this exact transaction, for this exact amount, to this exact recipient, is what the user is being asked to approve,\" and the token that's ultimately issued carries that specific detail rather than a broad grant that happens to cover it. That's the mechanism that actually lets a system claim it's binding the ",[335,554,555],{},"transaction"," to the client — not just the session, and not just the connection.",[153,558,560],{"id":559},"ciba-binding-the-approval-to-the-right-user-not-just-the-right-session","CIBA: Binding the Approval to the Right User, Not Just the Right Session",[149,562,563],{},"The remaining piece is making sure a human, specifically the right human, actually approved the action — which is where flows like Client-Initiated Backchannel Authentication earn their place in financial contexts. A customer approving a payment isn't just accessing an API endpoint. They're authorizing an irreversible business action, on a device the bank already recognizes as theirs, and a push-based approval model built on the same possession-based logic as the rest of this stack — approve this specific transfer, on this specific registered device, right now — closes the last gap between \"a valid token was presented\" and \"the actual account holder said yes to this specific thing.\"",[153,565,567],{"id":566},"the-pattern-across-all-of-it","The Pattern Across All of It",[149,569,570],{},"Private Key JWT proves client ownership. mTLS proves connection identity. DPoP proves possession of the signing key on every request. RAR proves the specifics of what's actually being authorized, not just that authorization exists in some general sense. CIBA strengthens the link between the approval and the actual human behind it. None of these mechanisms exist in isolation — together, they move financial APIs away from a single upfront credential check and toward continuous, specific, cryptographic proof that a particular client, a particular user, and a particular transaction genuinely belong together.",[153,572,252],{"id":251},[149,574,575,576,579],{},"For high-value systems, authenticating the client was never the finish line — it was the floor. The real target is trusted ",[335,577,578],{},"authorization",": not just confidence that a recognized caller is on the other end of a request, but confidence that this specific action, at this specific moment, for this specific amount, was legitimately approved by the person it claims to represent. That's a meaningfully higher bar than \"did we check the token,\" and it's exactly the bar FAPI was built to enforce.",[257,581],{},[153,583,262],{"id":261},[264,585,586,594,601],{},[267,587,588,589,593],{},"IETF, ",[197,590,592],{"href":545,"rel":591},[201],"RFC 9396: OAuth 2.0 Rich Authorization Requests"," — the standard letting clients specify fine-grained, transaction-level authorization details instead of coarse scopes.",[267,595,588,596,600],{},[197,597,599],{"href":518,"rel":598},[201],"RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)"," — binding access tokens to a client-held key at the individual request level.",[267,602,588,603,607],{},[197,604,606],{"href":506,"rel":605},[201],"RFC 8705: OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens"," — binding tokens to a client's TLS certificate for connection-level proof of possession.",{"title":293,"searchDepth":294,"depth":295,"links":609},[610,611,612,613,614,616,617,618,619],{"id":474,"depth":295,"text":475},{"id":488,"depth":295,"text":489},{"id":499,"depth":295,"text":500},{"id":512,"depth":295,"text":513},{"id":524,"depth":295,"text":615},"Rich Authorization Requests: Binding the Transaction, Not Just the Session",{"id":559,"depth":295,"text":560},{"id":566,"depth":295,"text":567},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-12","Knowing who's calling an API used to be enough. Private Key JWT, mTLS, DPoP, Rich Authorization Requests, and CIBA are the mechanisms that let FAPI bind a specific transaction to a specific client and user, not just a session.",{},"/blog/fapi-transaction-binding","6 min",{"title":466,"description":621},"blog/19.fapi-transaction-binding","OAuth2 / OpenID Connect","bw0TrMX8bIAidoLyxiDBK8VNvGi22Ssee5y7AwRBMBU",{"id":630,"title":631,"body":632,"date":774,"description":775,"extension":311,"featured":35,"meta":776,"navigation":313,"path":777,"readTime":315,"seo":778,"stem":779,"tag":627,"__hash__":780},"blog/blog/18.beyond-bearer-tokens-proof-of-possession.md","Your Secure APIs May Need More Than Bearer Tokens",{"type":146,"value":633,"toc":761},[634,637,641,648,651,655,658,662,669,672,676,679,683,689,693,701,705,712,715,719,722,726,729,731,734,736,738],[149,635,636],{},"If possession of a stolen token is enough to access your API, the token has become the password — and it inherited every weakness a password has, without most of the mitigations a password gets. That's the uncomfortable truth sitting underneath a lot of API designs protecting genuinely valuable resources: banking transactions, healthcare records, payment operations, systems where the cost of a wrong answer isn't hypothetical.",[153,638,640],{"id":639},"bearer-tokens-work-like-cash","Bearer Tokens Work Like Cash",[149,642,643,644,647],{},"A bearer token's entire security model is baked into its name: whoever bears it, spends it. The API checks whether the token is valid, whether it's expired, whether it carries the right scopes — and if all three check out, access is granted. What the API fundamentally doesn't know is ",[335,645,646],{},"who"," is presenting the token. It only knows someone has it, the same way a cashier accepting a twenty-dollar bill has no way of knowing whether the person handing it over is the person it was originally given to.",[149,649,650],{},"That's a fine model for a lot of low-stakes situations. It becomes a liability the moment token theft is a realistic threat, because an attacker who obtains a valid access token — through malware, a compromised device, browser storage theft, a logging mistake, or a breached piece of infrastructure — doesn't need the user's password, the original device, or the original application. They need the token, full stop, and the API has no mechanism to tell the difference between the legitimate holder and the thief.",[153,652,654],{"id":653},"proof-of-possession-asks-a-different-question","Proof of Possession Asks a Different Question",[149,656,657],{},"The fix isn't a better token. It's a different question entirely. Instead of \"do you have the token,\" proof-of-possession mechanisms ask \"can you prove you're the entity this token was actually issued to.\" That reframing is the whole security improvement: the token alone stops being sufficient, because the requester also has to demonstrate control of a cryptographic key that never travels with the token itself.",[153,659,661],{"id":660},"dpop-binding-a-token-to-a-key-the-client-actually-holds","DPoP: Binding a Token to a Key the Client Actually Holds",[149,663,664,668],{},[197,665,667],{"href":518,"rel":666},[201],"DPoP (Demonstrating Proof of Possession)",", standardized as RFC 9449 in 2023, is the OAuth mechanism built specifically for this. Instead of a bare bearer token, the client sends a signed JWT — the DPoP proof — alongside the request. That proof is a signature over specifics of the actual HTTP request: the method, the URI, a timestamp, a unique identifier, and a hash of the associated access token, all signed with a private key the client holds. The authorization server binds the original access token to the public part of that key pair, and the resource server, on every subsequent request, verifies three things: that the proof's signature is valid, that the key matches what the token was bound to, and that the request details in the proof actually match the request it's attached to.",[149,670,671],{},"The practical consequence is direct. An attacker who steals the access token — through any of the theft vectors above — doesn't get the private key alongside it, because the private key was never sent anywhere; it only ever produced signatures. Without that key, the stolen token has no value on its own. The attacker needs both the token and the key, and stealing the first no longer gets them the second.",[153,673,675],{"id":674},"public-keys-still-arent-the-vulnerability","Public Keys Still Aren't the Vulnerability",[149,677,678],{},"The same misconception that shows up around Private Key JWT shows up here too: if an attacker gets the public key, isn't the whole scheme compromised? No — the public key was designed to be shared from the start. Security comes entirely from protecting the private key, and an attacker who knows the public key, the algorithm, and the full structure of the token still can't produce a valid signature without the one piece they don't have.",[153,680,682],{"id":681},"shared-secrets-are-familiar-which-is-part-of-the-problem","Shared Secrets Are Familiar, Which Is Part of the Problem",[149,684,685,686,688],{},"Plenty of systems still authenticate clients with a ",[192,687,495],{}," — a shared-secret model where both the client and the authorization server know the same value. It's familiar and easy to implement, and it comes with the same structural weaknesses shared secrets always have: they leak, get copied into configuration files, complicate rotation, and multiply every time a new environment needs its own copy. For high-value systems, asymmetric cryptography — where only one side ever holds the private key — closes exactly that gap.",[153,690,692],{"id":691},"different-environments-different-proof-of-possession-mechanisms","Different Environments, Different Proof-of-Possession Mechanisms",[149,694,695,696,700],{},"DPoP isn't the only option, and the right choice depends on the client, not a blanket preference. DPoP fits public clients, mobile apps, and browser-based applications, where certificate management is genuinely difficult but binding a token to an application-managed key is straightforward. ",[197,697,699],{"href":506,"rel":698},[201],"Mutual TLS (RFC 8705)"," — which binds tokens to a client's TLS certificate rather than a separate signed proof — tends to fit machine-to-machine communication and controlled enterprise environments, including financial institution integrations where certificate infrastructure already exists. Private Key JWT, covered on its own terms elsewhere, fits confidential clients able to securely hold and protect a private key long-term. The governing principle isn't \"pick one mechanism and use it everywhere.\" It's \"don't lean on a weaker mechanism once the risk in front of you justifies a stronger one.\"",[153,702,704],{"id":703},"security-should-scale-with-what-the-api-actually-does","Security Should Scale With What the API Actually Does",[149,706,707,708,711],{},"It's worth pushing this a step further than \"payments and health records get the strong stuff, everything else gets bearer tokens.\" Any API that requires authentication at all has already declared itself worth protecting — that's what authentication ",[335,709,710],{},"is for",". A public product catalog doesn't need a login. The moment an API sits behind an auth check, it's guarding something: a user's data, an internal system, an action that shouldn't be available to just anyone. Treating that whole category as \"medium risk, bearer tokens are fine\" undersells what authentication is signaling in the first place.",[149,713,714],{},"That doesn't collapse every authenticated API into the same bucket — a read-only endpoint returning a user's own display name and an endpoint that transfers their money still don't carry equal stakes, and pretending otherwise would justify the wrong kind of blanket answer in the opposite direction. But the baseline assumption is worth resetting: the question isn't \"is this API important enough to deserve proof-of-possession.\" It's \"why would this specific, already-authenticated API be the exception that doesn't need it.\" The higher the real-world cost of misuse, the stronger the authorization model should be — and the floor for \"real-world cost\" starts the moment a token grants access to anything a user or a business would notice losing, not just the handful of endpoints that touch money or medical records directly.",[153,716,718],{"id":717},"convenience-isnt-a-security-architecture","Convenience Isn't a Security Architecture",[149,720,721],{},"\"Shared secrets are easier to implement\" is true and also not the question that matters. The question that matters is what happens if the credential gets stolen. If the honest answer is \"the attacker can act as the client until someone notices,\" that design deserves a second look — not because convenience is worthless, but because it was never actually weighed against the cost of the failure mode it's accepting.",[153,723,725],{"id":724},"the-direction-this-is-heading","The Direction This Is Heading",[149,727,728],{},"The broader trend across identity and authentication is moving the same direction DPoP moves: away from \"whoever holds the credential is trusted\" and toward \"prove you're authorized to use this credential.\" It's the same underlying logic behind signed JWTs, hardware-backed keys, passkeys, certificate-based authentication, and transaction signing more generally — identity is increasingly something proven at the moment of use, not something simply presented and taken at face value.",[153,730,252],{"id":251},[149,732,733],{},"The line isn't between \"high-value APIs\" and everything else — it's between APIs that require authentication and APIs that don't, and the first group is bigger than the \"payments and medical records\" framing suggests. If an API needed a login to begin with, something behind it was worth protecting, and that's the point at which \"a stolen token is enough to get in\" deserves scrutiny rather than a pass because the endpoint doesn't sound dramatic on its own. DPoP and its relatives don't stop tokens from ever being stolen — they make sure that theft alone isn't enough. The attacker still needs the key, and without it, the token they took is worth exactly nothing.",[257,735],{},[153,737,262],{"id":261},[264,739,740,746,752],{},[267,741,588,742,745],{},[197,743,599],{"href":518,"rel":744},[201]," — the standard defining DPoP proofs and how access tokens get bound to a client-held key.",[267,747,588,748,751],{},[197,749,606],{"href":506,"rel":750},[201]," — the mTLS-based alternative for sender-constraining tokens in enterprise and machine-to-machine contexts.",[267,753,754,755,760],{},"Authlete, ",[197,756,759],{"href":757,"rel":758},"https://www.authlete.com/kb/oauth-and-openid-connect/proof-of-possession-pop-tokens/certificate-bound-access-tokens/",[201],"\"Issuing Mutual-TLS Certificate-Bound Access Tokens\""," — a practical look at how certificate-bound tokens work in an implementation, distinct from mTLS client authentication itself.",{"title":293,"searchDepth":294,"depth":295,"links":762},[763,764,765,766,767,768,769,770,771,772,773],{"id":639,"depth":295,"text":640},{"id":653,"depth":295,"text":654},{"id":660,"depth":295,"text":661},{"id":674,"depth":295,"text":675},{"id":681,"depth":295,"text":682},{"id":691,"depth":295,"text":692},{"id":703,"depth":295,"text":704},{"id":717,"depth":295,"text":718},{"id":724,"depth":295,"text":725},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-11","If possession of a stolen token is enough to access your API, the token has become the password. DPoP, mTLS, and why proof-of-possession asks a fundamentally different question than a bearer token ever could.",{},"/blog/beyond-bearer-tokens-proof-of-possession",{"title":631,"description":775},"blog/18.beyond-bearer-tokens-proof-of-possession","sPJk-EwjU-9ffwd_cya4f6sRz6xEe-nFnRu4t1jbUWc",{"id":782,"title":783,"body":784,"date":913,"description":914,"extension":311,"featured":35,"meta":915,"navigation":313,"path":916,"readTime":624,"seo":917,"stem":918,"tag":462,"__hash__":919},"blog/blog/17.fips-compliance-beyond-kubernetes.md","FIPS Compliance: It's More Than Running Your Application on Kubernetes",{"type":146,"value":785,"toc":903},[786,789,793,808,815,819,822,825,829,846,849,853,856,860,863,867,870,872,875,877,879],[149,787,788],{},"Your application can be genuinely secure and still not be FIPS compliant. That's a hard sentence for a lot of engineering teams to sit with, because it means all the usual signals — HTTPS everywhere, strong algorithms, a hardened container image — don't actually answer the question FIPS is asking. FIPS isn't a property of your application. It's a property of the specific cryptographic modules your entire environment depends on, and that distinction is where most \"we deployed to Kubernetes, so we're compliant\" conversations go wrong.",[153,790,792],{"id":791},"what-fips-actually-certifies","What FIPS Actually Certifies",[149,794,795,796,801,802,807],{},"FIPS 140-3, the current ",[197,797,800],{"href":798,"rel":799},"https://csrc.nist.gov/pubs/fips/140-3/final",[201],"NIST standard"," governing cryptographic modules, doesn't certify applications, orchestrators, or clouds. It certifies specific, versioned cryptographic modules — a particular build of OpenSSL, a specific crypto library shipped with an operating system — tested and validated by an accredited lab under NIST's ",[197,803,806],{"href":804,"rel":805},"https://www.nist.gov/programs-projects/cryptographic-module-validation-program-cmvp",[201],"Cryptographic Module Validation Program (CMVP)",", run jointly with Canada's Centre for Cyber Security. CMVP maintains a searchable registry of every module that's actually earned a certificate, and it's explicit on a point worth repeating: a product doesn't meet FIPS 140 requirements just because it implements an approved algorithm and holds an algorithm validation certificate. The module itself — its implementation, not just its output — has to be the thing that was tested.",[149,809,810,811,814],{},"That's the core misunderstanding behind \"we use AES-256, therefore we're compliant.\" AES-256 is an approved algorithm. Whether a given piece of software is ",[335,812,813],{},"using a validated implementation"," of it is a completely separate question, and it's the one that actually determines compliance.",[153,816,818],{"id":817},"every-layer-between-your-code-and-the-hardware-is-part-of-the-boundary","Every Layer Between Your Code and the Hardware Is Part of the Boundary",[149,820,821],{},"Picture a typical deployment: application code, running on a language runtime, on an operating system, calling a cryptographic library, packaged in a container image, scheduled by a container runtime, sitting on a Linux kernel, on a VM or physical server. Every one of those layers can be where cryptographic operations actually happen — key generation, signing, hashing, TLS negotiation — and FIPS compliance depends on which specific modules are doing that work at each layer, not on the layer that happens to be easiest to point to in an architecture diagram.",[149,823,824],{},"Kubernetes sits entirely outside this question. It orchestrates, schedules, scales, and handles service discovery — genuinely valuable capabilities, none of which touch whether the cryptographic operations underneath a workload are FIPS-validated. A single cluster can host fully compliant workloads sitting right next to completely non-compliant ones, because the orchestrator was never the thing being certified in the first place.",[153,826,828],{"id":827},"the-base-image-and-operating-system-actually-matter","The Base Image and Operating System Actually Matter",[149,830,831,832,835,836,841,842,845],{},"This is where the practical work lives. A container built from a generic Ubuntu or a stock ",[192,833,834],{},"mcr.microsoft.com/dotnet/aspnet"," image is calling whatever OpenSSL build happens to ship in that image — and unless that specific build has been configured to use FIPS-validated modules, the application running on top of it isn't compliant no matter how good its own code is. Enterprise Linux distributions generally offer a real path here: Red Hat Enterprise Linux, for instance, documents a specific procedure for ",[197,837,840],{"href":838,"rel":839},"https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/security_hardening/switching-rhel-to-fips-mode_security-hardening",[201],"switching RHEL into FIPS mode"," — setting the ",[192,843,844],{},"fips=1"," kernel boot argument, applying the FIPS system-wide cryptographic policy, and regenerating the initial RAM disk so the switch actually takes effect at boot. Once that's done, RHEL's core cryptographic components restrict themselves to FIPS-approved implementations system-wide — which also means, notably, that an application's own FIPS-related configuration flags get ignored in favor of the OS-level policy, since the OS is the thing actually enforcing the boundary at that point.",[149,847,848],{},"Enabling FIPS mode isn't a checkbox flipped after the fact, either — it's meaningfully easier to build into a base image from the start than to retrofit into a running fleet, which is exactly why this decision belongs in early architecture conversations rather than a pre-launch compliance sweep.",[153,850,852],{"id":851},"the-runtime-adds-its-own-layer-of-complexity","The Runtime Adds Its Own Layer of Complexity",[149,854,855],{},".NET on Linux typically defers to the operating system's cryptographic libraries for many operations, which means its FIPS posture is largely inherited from whatever's underneath it. Java maintains its own provider architecture, where the specific crypto provider configured matters as much as the JDK version. Go often bundles its own cryptographic implementations directly into the standard library rather than calling out to the OS, which changes the compliance conversation entirely — a FIPS-compliant OS underneath a Go binary doesn't automatically make the Go binary's own crypto operations compliant. None of these are interchangeable, and assuming one runtime's FIPS story applies to another is a common way compliance work quietly breaks.",[153,857,859],{"id":858},"infrastructure-extends-the-boundary-further-than-most-teams-expect","Infrastructure Extends the Boundary Further Than Most Teams Expect",[149,861,862],{},"Compliance doesn't stop at the container. Disk encryption, TLS certificate management, where private keys actually live, whether an HSM is required, whether the load balancer terminating TLS is using validated cryptography, how secrets are stored, how backups are encrypted — every one of these sits inside the same cryptographic boundary as the application code, and a gap in any of them affects the overall posture the same way a gap in the application would.",[153,864,866],{"id":865},"compliance-is-a-system-property-not-a-sentence-you-can-say-once","Compliance Is a System Property, Not a Sentence You Can Say Once",[149,868,869],{},"\"We deployed to Kubernetes.\" \"We use HTTPS.\" \"We use OpenSSL.\" Each of these describes a technology choice, not a compliance outcome. FIPS compliance emerges from how every layer — application, runtime, OS, crypto library, infrastructure, hardware — actually works together, using validated modules where the standard requires them. That's why it has to be an architectural decision made early: which OS, which base image, which crypto libraries, which cloud services, where keys are stored, whether HSM integration is needed. Reversing any of those choices after a system is already running in production is a materially more expensive exercise than deciding correctly at design time.",[153,871,252],{"id":251},[149,873,874],{},"FIPS was never about Kubernetes, containers, or any single algorithm. It's about ensuring the actual cryptographic operations your system performs — at every layer between your application code and the physical hardware — are carried out by modules that have been independently validated against a defined standard, not just modules that happen to implement an approved algorithm somewhere in their code. Architects who understand the full boundary, rather than treating the orchestrator or the algorithm choice as the finish line, are the ones who end up building systems that actually meet the requirement instead of just resembling it.",[257,876],{},[153,878,262],{"id":261},[264,880,881,889,895],{},[267,882,883,884,888],{},"NIST, ",[197,885,887],{"href":798,"rel":886},[201],"FIPS 140-3: Security Requirements for Cryptographic Modules"," — the current standard governing cryptographic module validation.",[267,890,883,891,894],{},[197,892,806],{"href":804,"rel":893},[201]," — the joint NIST/CCCS program that actually tests and certifies cryptographic modules, and its searchable registry of validated modules.",[267,896,897,898,902],{},"Red Hat, ",[197,899,901],{"href":838,"rel":900},[201],"\"Switching RHEL to FIPS Mode\""," — a concrete example of what enabling FIPS mode actually requires at the operating-system level.",{"title":293,"searchDepth":294,"depth":295,"links":904},[905,906,907,908,909,910,911,912],{"id":791,"depth":295,"text":792},{"id":817,"depth":295,"text":818},{"id":827,"depth":295,"text":828},{"id":851,"depth":295,"text":852},{"id":858,"depth":295,"text":859},{"id":865,"depth":295,"text":866},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-10","FIPS doesn't certify applications, orchestrators, or clouds — it certifies specific, versioned cryptographic modules. \"We deployed to Kubernetes\" was never an answer to the question FIPS is actually asking.",{},"/blog/fips-compliance-beyond-kubernetes",{"title":783,"description":914},"blog/17.fips-compliance-beyond-kubernetes","m-pGfhKb0SSbwGRdntahs9AmZznEDyKxCASbRNMZFmg",{"id":921,"title":922,"body":923,"date":1142,"description":1143,"extension":311,"featured":35,"meta":1144,"navigation":313,"path":1145,"readTime":1146,"seo":1147,"stem":1148,"tag":627,"__hash__":1149},"blog/blog/16.why-fapi-recommends-private-key-jwt.md","Why FAPI Recommends Private Key JWT: Your Public Key Is Meant to Be Public",{"type":146,"value":924,"toc":1128},[925,932,940,944,954,958,965,969,976,980,997,1006,1010,1049,1053,1060,1064,1067,1071,1074,1078,1081,1085,1088,1090,1097,1099,1101],[149,926,927,928,931],{},"A public key is an identity. A private key is the proof. Owning one doesn't give you the other — and understanding exactly ",[335,929,930],{},"why"," that's true, mathematically, is what makes the rest of this make sense rather than just sound reassuring.",[149,933,934,935,939],{},"Financial APIs operate in an environment where \"trust me, I know the password\" isn't a strong enough claim. Banks, payment providers, and other regulated platforms need something closer to cryptographic proof than a shared secret can offer, which is a big part of why the Financial-grade API (FAPI) profiles recommend ",[936,937,938],"strong",{},"Private Key JWT"," as a client authentication method. The goal isn't just stronger authentication in a vague sense. It's a specific guarantee: proof that the client making a request actually possesses a private key, without that key ever having to travel anywhere or be shared with anyone who needs to verify it.",[153,941,943],{"id":942},"the-problem-with-shared-secrets","The Problem With Shared Secrets",[149,945,946,947,950,951,953],{},"Traditional OAuth client authentication often comes down to a ",[192,948,949],{},"client_id"," and ",[192,952,495],{}," — a password, essentially, known by both the client and the authorization server. That works, but it has a structural weakness baked in: both parties hold the same credential. If the secret leaks — copied into a config file, committed to a repo, exposed in a log — an attacker who finds it can authenticate as the client with no further effort. Rotating it requires coordinating both sides. The entire model depends on a shared piece of knowledge staying secret, and shared secrets have a well-documented habit of not staying secret forever.",[153,955,957],{"id":956},"private-key-jwt-changes-whats-being-proven","Private Key JWT Changes What's Being Proven",[149,959,960,961,964],{},"Instead of proving identity by revealing something both sides know, the client proves identity by digitally signing a JWT with a private key that only it possesses. The authorization server never holds that private key — it holds the corresponding ",[335,962,963],{},"public"," key, and verification is a fundamentally different operation from signing. The private key never leaves the client, ever, in this model. That single property is what makes the rest of the design work.",[153,966,968],{"id":967},"why-the-public-key-is-genuinely-safe-to-share","Why the Public Key Is Genuinely Safe to Share",[149,970,971,972,975],{},"A common misconception is worth addressing directly: if an attacker gets hold of a public key, can't they use it to impersonate the owner? No — and this isn't just a design convention, it's a mathematical property of how these algorithms are constructed. Public-key cryptography is built on ",[936,973,974],{},"trapdoor functions",": operations that are computationally easy to perform in one direction and, without a specific secret, computationally infeasible to reverse. Signing with a private key is the easy direction. Recovering that private key from the corresponding public key is the direction that isn't practically reversible — not because nobody's tried, but because of specific, well-studied mathematical problems that current computers can't solve at scale in any useful timeframe.",[153,977,979],{"id":978},"the-actual-math-rsa","The Actual Math: RSA",[149,981,982,983,986,987,992,993,996],{},"RSA's trapdoor is built on ",[936,984,985],{},"integer factorization",". Key generation starts by picking two large prime numbers, p and q, and multiplying them together to get n = p × q. The public key is derived from n; the private key is derived from p and q individually. Multiplying two large primes together is fast — any computer does it instantly. Going the other direction — given only n, recovering the specific primes p and q that were multiplied to produce it — is the integer factorization problem, and it's dramatically harder. There's no known way to do it efficiently for large n; the best publicly known method, the ",[197,988,991],{"href":989,"rel":990},"https://cr.yp.to/nfscircuit/rsa.html",[201],"General Number Field Sieve",", has running time that grows ",[335,994,995],{},"superpolynomially"," with the size of the number, meaning the difficulty explodes far faster than the key size grows.",[149,998,999,1000,1005],{},"This isn't theoretical. In 2020, researchers factored RSA-250, an 829-bit number, and it required ",[197,1001,1004],{"href":1002,"rel":1003},"https://quantum.cloud.ibm.com/learning/en/courses/fundamentals-of-quantum-algorithms/quantum-algorithmic-foundations/factoring-and-gcd",[201],"thousands of CPU-core-years distributed across tens of thousands of machines"," to do it. Real-world RSA keys today run 2048 or 4096 bits — not roughly twice as hard to factor as an 829-bit number, but astronomically harder, because the difficulty curve is exponential-ish, not linear, in key length. An attacker holding an RSA public key holds n. Turning that into the private key means factoring n, and at current key sizes, that's outside the reach of every computer on Earth combined, working for longer than the age of the universe, using any publicly known classical algorithm.",[153,1007,1009],{"id":1008},"the-actual-math-elliptic-curve-cryptography-ecdsa","The Actual Math: Elliptic Curve Cryptography (ECDSA)",[149,1011,1012,1013,1016,1017,1020,1021,1024,1025,1027,1028,1031,1032,950,1034,1036,1037,950,1039,1041,1042,1044,1045,1048],{},"Elliptic curve algorithms — increasingly common in FAPI deployments because of smaller key sizes and faster operations — rely on a different hard problem with the same basic shape. A private key is just a number, ",[335,1014,1015],{},"d",". The public key is a point on an elliptic curve, computed as ",[335,1018,1019],{},"Q = d × G",", where ",[335,1022,1023],{},"G"," is a fixed, publicly known starting point on the curve and the \"multiplication\" is a specific operation called elliptic curve point multiplication — repeated point addition, done ",[335,1026,1015],{}," times. Computing ",[335,1029,1030],{},"Q"," from ",[335,1033,1015],{},[335,1035,1023],{}," is fast. Going backward — given ",[335,1038,1030],{},[335,1040,1023],{},", recovering ",[335,1043,1015],{}," — is the ",[936,1046,1047],{},"elliptic curve discrete logarithm problem (ECDLP)",", and no efficient classical algorithm is known to solve it. The best known general approaches, like Pollard's rho method, still take time that grows exponentially with key size, which is exactly why elliptic curve keys can be so much smaller than RSA keys while offering comparable security — the underlying hard problem is harder, per bit, than integer factorization is.",[153,1050,1052],{"id":1051},"what-computationally-infeasible-actually-means-here","What \"Computationally Infeasible\" Actually Means Here",[149,1054,1055,1056,1059],{},"It's worth being precise about the claim being made, because it's narrower than \"impossible.\" Nobody has mathematically proven that fast factoring or fast ECDLP-solving algorithms ",[335,1057,1058],{},"can't"," exist — it's an open problem in computer science whether one might someday be found. What's true today is that decades of dedicated cryptanalytic research haven't found one, and the best publicly known methods remain computationally infeasible at standard key sizes using classical computers. There is one well-known exception worth naming honestly: a sufficiently large, fault-tolerant quantum computer running Shor's algorithm could solve both integer factorization and the elliptic curve discrete logarithm problem efficiently, which is exactly why NIST has been standardizing post-quantum cryptographic algorithms as a hedge against that future possibility. That's a genuine, forward-looking caveat — and it's also why \"computationally infeasible with current technology\" is the accurate phrase, rather than a stronger claim the math doesn't actually support.",[153,1061,1063],{"id":1062},"a-simple-analogy-for-the-non-mathematical-version","A Simple Analogy for the Non-Mathematical Version",[149,1065,1066],{},"Think of it like a wax seal. Anyone can look at the seal pattern, and anyone can compare another mark against it to check whether it matches. But only the person holding the actual stamp can produce that exact impression. The public key is the reference pattern everyone's allowed to see. The private key is the physical stamp nobody else has. Having a perfect description of what the seal looks like doesn't let you manufacture the stamp that makes it — and the RSA and elliptic curve math above is the formal version of exactly that intuition.",[153,1068,1070],{"id":1069},"why-this-matters-specifically-for-fapi","Why This Matters Specifically for FAPI",[149,1072,1073],{},"Financial-grade systems need client authentication that resists leaked configuration files, shared secrets, replay attempts, and impersonation — and Private Key JWT delivers on each of those specifically because of the math above. The authorization server never stores a secret whose theft would let someone else authenticate as the client; it stores a public key that's only useful for verification, not signing. Keys can be rotated by publishing a new public key without any password synchronization. And because compromising the authorization server only exposes public keys, that compromise alone doesn't hand an attacker the ability to forge new authenticated requests.",[153,1075,1077],{"id":1076},"what-an-attacker-actually-gets-from-a-stolen-public-key","What an Attacker Actually Gets From a Stolen Public Key",[149,1079,1080],{},"Very little. They can verify signatures, identify which key produced a given signature, and inspect any associated certificate. They cannot generate a valid signature, authenticate as the client, or produce a new JWT assertion the authorization server will accept — because none of those actions are possible without solving integer factorization or ECDLP for that specific key, which remains outside practical reach.",[153,1082,1084],{"id":1083},"the-private-key-is-still-the-whole-game","The Private Key Is Still the Whole Game",[149,1086,1087],{},"None of this makes the private key's protection optional — quite the opposite. If an attacker steals the private key directly, all the math above becomes irrelevant, because they no longer need to solve a hard problem; they just have the answer. That's why organizations pairing Private Key JWT with real security invest in Hardware Security Modules, Trusted Platform Modules, secure enclaves, cloud key management services, and strict access controls around where and how the private key is ever used. Private Key JWT is only as strong as the protection around the one thing the whole scheme depends on staying secret.",[153,1089,252],{"id":251},[149,1091,1092,1093,1096],{},"Private Key JWT doesn't replace TLS, secure key storage, replay protection, or proper JWT validation — including audience checks, expiration checks, and unique ",[192,1094,1095],{},"jti"," values to prevent a captured token from being reused. It strengthens one specific layer, client authentication, by replacing a shared secret with a mathematical guarantee: the public key genuinely can't be reversed into the private key using any publicly known method, at the key sizes financial-grade systems actually use. That's not a policy choice or a convention — it's integer factorization and the elliptic curve discrete logarithm problem doing exactly what they were chosen to do. Identity, in this model, was never protected by hiding the public key. It's protected by the fact that even total, public knowledge of it doesn't get an attacker any closer to the private key that actually proves who's on the other end of the request.",[257,1098],{},[153,1100,262],{"id":261},[264,1102,1103,1111,1119],{},[267,1104,1105,1106,1110],{},"D. J. Bernstein, ",[197,1107,1109],{"href":989,"rel":1108},[201],"\"Background: RSA and the Number Field Sieve\""," — a technical overview of why RSA key size directly determines resistance to the best known factoring methods.",[267,1112,1113,1114,1118],{},"IBM Quantum Learning, ",[197,1115,1117],{"href":1002,"rel":1116},[201],"\"Factoring and Computing GCDs\""," — includes the RSA-250 factoring record and what it took to break an 829-bit key classically.",[267,1120,1121,1122,1127],{},"LORIA, ",[197,1123,1126],{"href":1124,"rel":1125},"https://members.loria.fr/EThome/teaching/2022-cse-291-14/slides/cse-291-14.pdf",[201],"\"The Number Field Sieve\""," — lecture notes distinguishing the integer factorization problem (RSA) from the discrete logarithm problem (Diffie-Hellman/ECDSA) and why both underpin modern public-key cryptography.",{"title":293,"searchDepth":294,"depth":295,"links":1129},[1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141],{"id":942,"depth":295,"text":943},{"id":956,"depth":295,"text":957},{"id":967,"depth":295,"text":968},{"id":978,"depth":295,"text":979},{"id":1008,"depth":295,"text":1009},{"id":1051,"depth":295,"text":1052},{"id":1062,"depth":295,"text":1063},{"id":1069,"depth":295,"text":1070},{"id":1076,"depth":295,"text":1077},{"id":1083,"depth":295,"text":1084},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-09","A public key is an identity. A private key is the proof. The math behind why an attacker who steals your public key still can't forge a signature — RSA factoring, ECDLP, and what FAPI actually gets from Private Key JWT.",{},"/blog/why-fapi-recommends-private-key-jwt","9 min",{"title":922,"description":1143},"blog/16.why-fapi-recommends-private-key-jwt","hMATOyg14y13qBpAP1jCrqBNm28NVURN83Dn85riaO8",{"id":1151,"title":1152,"body":1153,"date":1287,"description":1288,"extension":311,"featured":35,"meta":1289,"navigation":313,"path":1290,"readTime":459,"seo":1291,"stem":1292,"tag":129,"__hash__":1293},"blog/blog/15.stop-turning-deployments-into-board-meetings.md","Stop Turning Deployments Into Board Meetings",{"type":146,"value":1154,"toc":1275},[1155,1158,1162,1165,1169,1176,1180,1190,1203,1207,1210,1214,1217,1221,1224,1228,1231,1235,1238,1240,1243,1245,1247],[149,1156,1157],{},"The better a team's rollback strategy is, the less frightening its deployments become. One of the more interesting engineering anti-patterns to watch for isn't bad code — it's bad confidence. Some organizations treat a routine deployment like a major organizational event: a call gets scheduled, managers join, architects join, developers join, operations joins, and everyone watches one engineer click deploy. If it succeeds, everyone disconnects. If it fails, the postmortem starts before the rollback does. That's not engineering maturity. That's engineering anxiety, dressed up as process.",[153,1159,1161],{"id":1160},"if-a-deployment-needs-an-audience-something-upstream-is-broken","If a Deployment Needs an Audience, Something Upstream Is Broken",[149,1163,1164],{},"Deploying software shouldn't require spectators. If a deployment consistently needs multiple calls, a dozen stakeholders, manual coordination, and someone watching in real time just in case, that ceremony is compensating for a weakness somewhere else in the system. The actual problem was never that deployments carry risk — every deployment does. The problem is that recovery isn't trusted, and when recovery isn't trusted, people substitute human attention for the confidence a well-designed system should be providing on its own.",[153,1166,1168],{"id":1167},"the-question-isnt-how-do-we-prevent-failure-its-how-fast-can-we-recover","The Question Isn't \"How Do We Prevent Failure.\" It's \"How Fast Can We Recover.\"",[149,1170,1171,1172,1175],{},"Engineering maturity was never about eliminating failure. It's about making failure cheap and recoverable. A rollback plan isn't a backup plan bolted onto the deployment strategy — it ",[335,1173,1174],{},"is"," the deployment strategy, or at least the half of it that determines whether a bad release is a five-minute non-event or a three-hour incident.",[153,1177,1179],{"id":1178},"roll-back-to-the-previous-image-tag-is-not-a-rollback-strategy","\"Roll Back to the Previous Image Tag\" Is Not a Rollback Strategy",[149,1181,1182,1183,1185,1186,1189],{},"This is worth being blunt about, because it's a genuinely common failure mode dressed up as a solved problem: a rollback plan that consists entirely of \"redeploy the previous container image tag\" is just as confused as whoever wrote it down as sufficient. Reverting an image tag rolls back your ",[335,1184,192],{},". It does nothing about your ",[335,1187,1188],{},"data",", and in any system with a database, those two things are not the same problem. If the new version ran a schema migration — added a non-nullable column, changed a constraint, renamed a field the old code doesn't know about — reverting the image doesn't undo the schema. It puts old code in front of a database that no longer matches what that code expects, which isn't a rollback. It's a second, different outage, layered on top of the first one.",[149,1191,1192,1193,1198,1199,1202],{},"This is exactly the failure mode the ",[197,1194,1197],{"href":1195,"rel":1196},"https://blog.thepete.net/blog/2023/12/05/expand/contract-making-a-breaking-change-without-a-big-bang/",[201],"expand/contract pattern"," exists to prevent. The pattern breaks a schema change into stages — expand the schema to support both old and new structures simultaneously, migrate data and traffic over gradually, and only contract (remove the old structure) once nothing depends on it anymore. Done properly, it means both the new ",[335,1200,1201],{},"and"," the previous version of your application code can run against the same database at the same time, which is precisely the property that makes \"redeploy the old image\" an actual, safe rollback instead of a coin flip. A real rollback plan has to answer specific, checkable questions: how do we revert, how long does it take, is it automated, is the data compatible with the version we're rolling back to, have we actually tested the reverse path, and can we do all of this without downtime? If nobody on the team can answer those questions with confidence, the deployment isn't ready — no matter how many people are watching it happen.",[153,1204,1206],{"id":1205},"confidence-comes-from-engineering-not-from-a-call","Confidence Comes From Engineering, Not From a Call",[149,1208,1209],{},"No number of people on a call has ever made software more reliable. Twenty observers watching a deployment doesn't reduce technical risk by a single percentage point — it just distributes the anxiety more evenly across more people. What actually reduces risk is automated deployment pipelines, health checks, smoke tests, feature flags, blue-green deployments, canary releases, staged rollouts, real observability, and rollback procedures that have actually been exercised rather than just documented. Visibility isn't safety. Everyone being present just means more people are watching the same uncertainty unfold together, which feels like control without providing any.",[153,1211,1213],{"id":1212},"every-deployment-is-an-experiment-with-an-exit-strategy","Every Deployment Is an Experiment With an Exit Strategy",[149,1215,1216],{},"Testing narrows the unknowns; it doesn't eliminate them. Production traffic, real user behavior, and live infrastructure conditions are different every time, which means every deployment is closer to a controlled experiment than a guaranteed outcome. A mature engineering culture designs around that reality instead of pretending it away. Instead of asking how to prevent every possible failure, the more useful question is how to make recovering from one routine enough that it doesn't require a war room. Systems built that way aren't the ones that never have problems — they're the ones that recover fast enough that most users never notice one happened.",[153,1218,1220],{"id":1219},"remove-the-hero-from-the-critical-path","Remove the Hero From the Critical Path",[149,1222,1223],{},"If a deployment only feels safe because the most experienced engineer on the team is standing by \"just in case,\" that's not resilience — it's knowledge concentrated in one person instead of built into the system. A well-designed deployment process should let anyone following it deploy safely, because the safeguards live in the tooling and the process, not in one person's memory of what usually goes wrong.",[153,1225,1227],{"id":1226},"invest-where-the-meetings-currently-are","Invest Where the Meetings Currently Are",[149,1229,1230],{},"If an organization has enough time to schedule multiple deployment calls every week, it has enough time to fix the thing making those calls feel necessary. Automation, rollback testing, deployment observability, and operational playbooks all keep paying off long after the meeting invite would have expired. The trade being made every time a team chooses the meeting over the investment is a short-term feeling of control for a long-term dependency on people showing up.",[153,1232,1234],{"id":1233},"the-goal-is-boring-deployments","The Goal Is Boring Deployments",[149,1236,1237],{},"The best deployment anyone's ever seen is the one nobody talks about afterward. It starts, it completes, monitoring stays healthy, users notice nothing, and the team moves on to the next problem — no emergency call, no war room, no ceremony to mark that it happened. Boring isn't a sign the work didn't matter. It's a sign the work that mattered already happened, earlier, in the rollback plan, the automation, and the migration pattern that made the deployment forgettable in the first place.",[153,1239,252],{"id":251},[149,1241,1242],{},"Engineering was never about convincing yourself deployments will never fail. It's about building systems where failure is manageable enough that it stops requiring an audience. The better the rollback strategy — a real one, tested against actual data compatibility, not just \"redeploy the old tag\" — the more confidence a team earns on its own, and the fewer people need to be on a call to provide it artificially. Confidence built on process outlasts confidence borrowed from a meeting, because the meeting doesn't scale and the process does.",[257,1244],{},[153,1246,262],{"id":261},[264,1248,1249,1257,1266],{},[267,1250,1251,1252,1256],{},"Pete Hodgson, ",[197,1253,1255],{"href":1195,"rel":1254},[201],"\"Expand/Contract: Making a Breaking Change Without a Big Bang\""," — a detailed walkthrough of the pattern that makes rollbacks actually safe across schema changes, not just code changes.",[267,1258,1259,1260,1265],{},"PlanetScale, ",[197,1261,1264],{"href":1262,"rel":1263},"https://planetscale.com/blog/backward-compatible-databases-changes",[201],"\"Backward Compatible Database Changes\""," — a practical guide to the expand-migrate-contract pattern for keeping every deployment stage genuinely reversible.",[267,1267,1268,1269,1274],{},"Reliable Penguin, ",[197,1270,1273],{"href":1271,"rel":1272},"https://blogs.reliablepenguin.com/2025/11/16/database-migrations-without-drama-expand-contract-in-practice",[201],"\"Database Migrations Without Drama: Expand/Contract in Practice\""," — a concrete look at why \"redeploy the old image\" fails once a schema migration is involved, and how to sequence deploys so it doesn't.",{"title":293,"searchDepth":294,"depth":295,"links":1276},[1277,1278,1279,1280,1281,1282,1283,1284,1285,1286],{"id":1160,"depth":295,"text":1161},{"id":1167,"depth":295,"text":1168},{"id":1178,"depth":295,"text":1179},{"id":1205,"depth":295,"text":1206},{"id":1212,"depth":295,"text":1213},{"id":1219,"depth":295,"text":1220},{"id":1226,"depth":295,"text":1227},{"id":1233,"depth":295,"text":1234},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-08","A deployment call with a dozen stakeholders watching one engineer click deploy isn't engineering maturity. It's engineering anxiety, dressed up as process — and it's usually compensating for a rollback plan nobody trusts.",{},"/blog/stop-turning-deployments-into-board-meetings",{"title":1152,"description":1288},"blog/15.stop-turning-deployments-into-board-meetings","jQ3abt5bGcr4JKdUbw0qqtuYf_75_b6-8HMlmvY4o2k",{"id":1295,"title":1296,"body":1297,"date":1415,"description":1416,"extension":311,"featured":35,"meta":1417,"navigation":313,"path":1418,"readTime":624,"seo":1419,"stem":1420,"tag":1421,"__hash__":1422},"blog/blog/14.payday-vs-engineering.md","If All You Care About Is Payday, Stop Pretending You Care About Engineering",{"type":146,"value":1298,"toc":1404},[1299,1302,1305,1309,1318,1322,1325,1329,1342,1346,1349,1353,1356,1360,1367,1371,1374,1376,1379,1381,1383],[149,1300,1301],{},"A salary rewards your employment. It doesn't validate your judgment. Somewhere along the way, plenty of organizations let those two things blur together — as long as you show up, say the right things in the meeting, and don't rock the boat, the paycheck keeps clearing, and it's easy to mistake that for professional competence. It isn't. If your only objective is making it to the end of the month without questioning anything, that's a choice you're entitled to make. What you don't get to do is call that engineering.",[149,1303,1304],{},"Engineering demands curiosity. It demands evidence. It demands a willingness to challenge an assumption, including your own. Nodding through a decision you know is wrong because disagreeing would be politically inconvenient isn't protecting the business. It's protecting your own comfort, and dressing it up as loyalty.",[153,1306,1308],{"id":1307},"confidence-without-understanding-isnt-leadership","Confidence Without Understanding Isn't Leadership",[149,1310,1311,1312,1317],{},"The most dangerous person on an engineering team usually isn't the junior developer willing to say \"I don't know.\" It's the experienced one who's forgotten how to. Rejecting ideas without evidence, defending decisions they can't actually explain, hiding behind a title or \"that's how we've always done it\" instead of engaging with a real question — that's not seniority doing its job. Organizational theorist Chris Argyris spent decades studying exactly this pattern and gave it an uncomfortably accurate name: ",[197,1313,1316],{"href":1314,"rel":1315},"https://banotes.org/administrative-thinkers/foundations-organizational-learning-behavior-chris-argyris/",[201],"skilled incompetence"," — people who are highly practiced at protecting themselves from embarrassment, and whose skill at doing so is precisely what makes them unable to learn or change. It's not a lack of ability. It's a well-rehearsed defense that happens to produce bad decisions as a side effect, over and over, without the person doing it ever quite noticing.",[153,1319,1321],{"id":1320},"a-promotion-earned-through-politics-isnt-an-engineering-achievement","A Promotion Earned Through Politics Isn't an Engineering Achievement",[149,1323,1324],{},"There's nothing impressive about advancing because you controlled information, made a colleague look worse than they were, blocked a better idea because it wasn't yours, or stayed quiet when you knew a call was wrong. That's office politics wearing engineering's clothes. If your career moves forward because better engineers got pushed aside along the way, the promotion is telling you something about the organization's incentives. It's not telling you anything about your ability.",[153,1326,1328],{"id":1327},"stop-mistaking-meetings-for-technical-work","Stop Mistaking Meetings for Technical Work",[149,1330,1331,1332,1337,1338,1341],{},"If shipping a routine change requires three calls, four approval meetings, twenty people watching a deploy, and an hour spent deciding who clicks the button, that's not a deployment process. That's organizational dysfunction wearing a process's clothes. A mature engineering organization automates the repeatable parts of shipping software; it doesn't schedule recurring meetings around them. This isn't an abstract preference — it's measurable. The ",[197,1333,1336],{"href":1334,"rel":1335},"https://www.atlassian.com/devops/frameworks/dora-metrics",[201],"DORA research"," behind ",[335,1339,1340],{},"Accelerate",", one of the most cited studies of software delivery performance, consistently finds that elite-performing teams deploy on demand, often multiple times a day with lead times measured in hours, while low performers are stuck deploying once every month or two — and the gap between those two groups isn't talent. It's automation, small batch sizes, and the absence of exactly the kind of manual ceremony that turns a five-minute deploy into an hour of scheduling. Every recurring deployment meeting should trigger the same question: why isn't this automated yet?",[153,1343,1345],{"id":1344},"being-loud-isnt-the-same-as-being-right","Being Loud Isn't the Same as Being Right",[149,1347,1348],{},"Engineering isn't decided by whoever talks longest, whoever has the biggest title, or whoever leads with \"I've been doing this for twenty years.\" Show the evidence. Show the measurements. Show the trade-offs and the production data. Engineering respects facts. Egos demand applause, and a team that's stopped being able to tell the difference between the two has usually stopped being able to catch its own mistakes before they ship.",[153,1350,1352],{"id":1351},"your-job-is-to-improve-the-system-not-protect-your-image","Your Job Is to Improve the System, Not Protect Your Image",[149,1354,1355],{},"If someone shows you a better idea, take it. If someone finds a real flaw in your design, thank them for finding it before production did. If the evidence says you were wrong, change your mind — that's what a professional does with new information, not a threat to their standing. Software doesn't care about your ego. Production doesn't care about your title. The people using what you build only care whether it works, and every hour spent defending a decision purely to avoid admitting it was wrong is an hour not spent making the system better.",[153,1357,1359],{"id":1358},"coasting-isnt-the-safety-net-it-feels-like","Coasting Isn't the Safety Net It Feels Like",[149,1361,1362,1363,1366],{},"There's a version of this that's worth naming directly, because it's easy to tell yourself a story that sounds responsible on the surface: ",[335,1364,1365],{},"I just need this paycheck for my family, so I'll stop pushing, stop questioning, stop caring about whether the work is actually good."," It feels like prioritizing what matters. It's usually the opposite. Disengaging from your own craft doesn't protect the people depending on you — it quietly erodes the thing your income actually depends on, which is your ability to do the job well enough that it keeps being yours. Caring about your family and caring about doing good work were never actually in competition; the trap isn't caring about family, it's mistaking coasting for security when the two rarely turn out to be the same thing. The engineer who keeps sharpening their judgment protects their family more reliably than the one who's decided thinking hard isn't worth the effort anymore, because the first one is the harder person to replace.",[153,1368,1370],{"id":1369},"reward-the-quiet-ones","Reward the Quiet Ones",[149,1372,1373],{},"The engineers worth following are rarely the loudest in the room. They're the ones quietly removing unnecessary complexity, automating what shouldn't require a human, sharing what they know instead of hoarding it, and changing their minds the moment the evidence tells them to. Those engineers move the field forward. Everyone else is mostly just moving meeting invitations around.",[153,1375,252],{"id":251},[149,1377,1378],{},"If engineering, to you, means surviving the month without ever questioning anything, that's your decision to make. Just don't mistake passive participation for professional excellence — they're not the same thing, no matter how long either one has kept a paycheck coming. Engineering is built on evidence, accountability, and the willingness to keep learning, including from your own mistakes. The people who actually improve software are the ones willing to challenge a bad idea, even when it's theirs. The people who don't are usually the loudest defenders of whatever's already in place. Software has a way of exposing the difference eventually — it just doesn't always do it on a schedule you get to choose.",[257,1380],{},[153,1382,262],{"id":261},[264,1384,1385,1393],{},[267,1386,1387,1388,1392],{},"BA Notes, ",[197,1389,1391],{"href":1314,"rel":1390},[201],"\"Chris Argyris' Foundations of Organizational Learning and Behavior\""," — an overview of Argyris's concept of \"skilled incompetence\" and defensive reasoning in organizations.",[267,1394,1395,1396,1400,1401,1403],{},"Atlassian, ",[197,1397,1399],{"href":1334,"rel":1398},[201],"\"DORA Metrics: How to Measure DevOps Success\""," — an overview of the four key metrics from ",[335,1402,1340],{}," (Forsgren, Humble, Kim) distinguishing elite-performing engineering teams from low performers.",{"title":293,"searchDepth":294,"depth":295,"links":1405},[1406,1407,1408,1409,1410,1411,1412,1413,1414],{"id":1307,"depth":295,"text":1308},{"id":1320,"depth":295,"text":1321},{"id":1327,"depth":295,"text":1328},{"id":1344,"depth":295,"text":1345},{"id":1351,"depth":295,"text":1352},{"id":1358,"depth":295,"text":1359},{"id":1369,"depth":295,"text":1370},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-07","A salary rewards your employment. It doesn't validate your judgment. Coasting through the month without questioning anything is a choice you're entitled to make — just don't call it engineering.",{},"/blog/payday-vs-engineering",{"title":1296,"description":1416},"blog/14.payday-vs-engineering","Field Notes","RTpWCpoZq4Ev2eN9hgTcmnFXYPArMBaII1y7WnOpVeA",{"id":1424,"title":1425,"body":1426,"date":1554,"description":1555,"extension":311,"featured":313,"meta":1556,"navigation":313,"path":1557,"readTime":315,"seo":1558,"stem":1559,"tag":1421,"__hash__":1560},"blog/blog/13.why-writing-engineering-blogs-is-hard.md","Why Writing Engineering Blogs Is Hard",{"type":146,"value":1427,"toc":1542},[1428,1431,1434,1438,1441,1445,1448,1452,1469,1473,1476,1480,1486,1490,1493,1497,1500,1504,1513,1517,1520,1522,1524],[149,1429,1430],{},"The hardest habit to break when I write about engineering is the pull toward sounding absolute. It's so much easier to write \"this is how software should be built\" than it is to write \"given what I've seen, this is how I'd approach it.\" The first sentence closes the conversation. The second one leaves room for someone to push back, add context I didn't have, or just disagree — and that's the sentence I actually mean, every time.",[149,1432,1433],{},"It's worth being explicit about what that distinction actually means in practice, because it's easy to read past it. When I write about a topic, I'm describing what I'm doing or what I would do — not trying to move you toward my line of thought. Those are genuinely different goals. One is a report from where I'm standing. The other is an argument built to relocate you to where I'm standing, and that's not what I'm after. If you read something I've written and end up somewhere different from me, having actually thought it through, that's not a miss. That's the only outcome I was ever really hoping for.",[153,1435,1437],{"id":1436},"i-wont-give-you-an-architecture-without-understanding-the-problem-first","I Won't Give You an Architecture Without Understanding the Problem First",[149,1439,1440],{},"If someone asked me how I'd build a Hospital Management System, I could talk for hours about Clean Architecture, DDD, CQRS, event sourcing, microservices, modular monoliths, vertical slices. None of that would be an answer yet. Before I'd commit to any of it, I'd want to know whether this is a rural clinic or a national referral hospital, how many concurrent users it needs to handle, whether it has to work offline, whether it integrates with labs or insurers, what the regulatory and audit requirements are, how often it's expected to change, and who's going to be the one maintaining it after I'm gone. Recommend an architecture before knowing those answers, and what I'm really giving you is an opinion wearing the costume of expertise.",[153,1442,1444],{"id":1443},"what-i-write-are-preferences-not-standards","What I Write Are Preferences, Not Standards",[149,1446,1447],{},"When I write about architecture, security, or engineering practice, I'm not handing anyone a checklist to follow. I'm documenting how I'd approach a specific problem, shaped by the projects I've actually worked on, the incidents that taught me something the hard way, the systems that succeeded, the ones that didn't, and a fair amount of reading and arguing with other engineers along the way. Another experienced engineer looking at the same problem might land somewhere different. That's not a flaw in either of our reasoning — it's what happens when two people bring different constraints and different scars to the same question.",[153,1449,1451],{"id":1450},"i-think-in-trade-offs-not-in-collected-patterns","I Think in Trade-offs, Not in Collected Patterns",[149,1453,1454,1455,1460,1461,1464,1465,1468],{},"Every choice I make optimizes for something at the expense of something else. A modular monolith trades independent scaling for simplicity and a smaller team's ability to reason about the whole system. Microservices trade that simplicity for independent deployment and organizational autonomy. Neither is better in the abstract — they're answers to different questions, and the same is true of every database, messaging system, authentication approach, and caching strategy I've ever picked. This is basically why I've come to like the discipline behind ",[197,1456,1459],{"href":1457,"rel":1458},"http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions",[201],"Architecture Decision Records",", the format Michael Nygard proposed back in 2011: not a record of the \"right\" answer, but of the specific context that made a particular trade-off the right call ",[335,1462,1463],{},"then"," — which is also an honest admission that a different context might make a different call the right one ",[335,1466,1467],{},"now",". I've stopped thinking of my job as collecting the right patterns. It's choosing the right trade-off for the specific thing in front of me, and being able to say why.",[153,1470,1472],{"id":1471},"i-try-not-to-mock-legacy-systems-because-i-might-have-built-them","I Try Not to Mock Legacy Systems, Because I Might Have Built Them",[149,1474,1475],{},"One thing that's genuinely changed how I think: every legacy system I've ever complained about was, at some point, someone's modern, considered decision — often made by an engineer just as experienced as I am, working with the tools, timelines, and knowledge available at that moment. Given the same constraints, I might have made exactly the same call. That's a humbling thing to sit with, and it's made it a lot harder for me to look at an old system and ask \"what were they thinking\" without also asking what I don't yet know about the decision I'm currently proud of.",[153,1477,1479],{"id":1478},"when-people-ask-if-everyone-should-use-x-my-honest-answer-is-it-depends","When People Ask If Everyone Should Use X, My Honest Answer Is \"It Depends\"",[149,1481,1482,1483,1485],{},"I get some version of this question constantly — should every project use Clean Architecture, should everything be event-driven, should we always reach for CQRS. My answer is almost always \"it depends,\" and I've come to think that's not a dodge. A pattern exists because it solved a real problem for someone, somewhere. That's not the same as it solving my problem, here, now. The engineers I respect most are the ones who can tell me ",[335,1484,930],{}," a pattern exists before they'll tell me whether to use it.",[153,1487,1489],{"id":1488},"nothing-i-write-today-is-meant-to-be-final","Nothing I Write Today Is Meant to Be Final",[149,1491,1492],{},"Part of why I resist writing anything as definitive guidance is that the target keeps moving. New vulnerabilities get discovered. Frameworks change. Business requirements shift under a system that was designed around yesterday's assumptions. Something I write with full conviction today might genuinely deserve revision in two years — not because I was wrong when I wrote it, but because the field kept evolving underneath the article the whole time. I'd rather admit that up front than pretend I've written something timeless.",[153,1494,1496],{"id":1495},"my-perspective-comes-from-where-ive-actually-worked","My Perspective Comes From Where I've Actually Worked",[149,1498,1499],{},"An engineer who's spent years in banking systems is going to value different things than one who's spent years on gaming engines, hospital platforms, or embedded devices — and that's exactly as true of me as it is of anyone else. My opinions are downstream of my experience, not independent of it. That's the whole reason I say \"this is how I'd approach it\" instead of \"this is how everyone should approach it.\" The first sentence is honest about where the opinion came from. The second one pretends my specific path through this field is somehow universal.",[153,1501,1503],{"id":1502},"id-rather-start-a-discussion-than-end-one","I'd Rather Start a Discussion Than End One",[149,1505,1506,1507,1512],{},"The engineering conversations I've learned the most from almost never ended in everyone agreeing. Someone challenges an assumption I hadn't examined. Someone else brings production experience that reframes the whole question. There's a half-joking version of this idea floating around under the name ",[197,1508,1511],{"href":1509,"rel":1510},"https://www.laws-of-software.com/laws/cunningham/",[201],"Cunningham's Law"," — that the fastest way to get a good answer isn't to ask a clean question, it's to state something slightly wrong and let people who know better correct it. I don't write with that as a strategy, but I recognize the shape of it in how these conversations actually go: what comes out the other side usually isn't a winner — it's a better version of the question we started with. That's what I actually want from anything I write: not agreement, but the kind of pushback that leaves both of us thinking more clearly than we started.",[153,1514,1516],{"id":1515},"im-not-trying-to-be-right-im-trying-to-show-my-reasoning","I'm Not Trying to Be Right. I'm Trying to Show My Reasoning.",[149,1518,1519],{},"If someone reads something I've written and walks away thinking \"I wouldn't have solved it that way, but I understand why you did\" — that's the article working exactly as intended. I'm not trying to hand anyone the definitive answer to how software should be built, because I don't think that answer exists in a form portable enough to hand over. What I can share honestly is how I reason through a hard problem, built from what's actually worked for me and what's genuinely failed. When I write about a hospital system, a banking application, or a distributed system's architecture, I'm not setting a standard for the industry to follow. I'm showing my work — and if I learn something next month that changes my mind, that's not a contradiction of what I wrote before. It's just the job continuing to do what it's always done to me: reward curiosity over certainty, every time I've let it.",[257,1521],{},[153,1523,262],{"id":261},[264,1525,1526,1534],{},[267,1527,1528,1529,1533],{},"Michael Nygard, ",[197,1530,1532],{"href":1457,"rel":1531},[201],"\"Documenting Architecture Decisions\""," (2011) — the original proposal for Architecture Decision Records, capturing context and trade-offs rather than a universal \"right\" answer.",[267,1535,1536,1537,1541],{},"Laws of Software, ",[197,1538,1540],{"href":1509,"rel":1539},[201],"\"Cunningham's Law\""," — the (loosely attributed, half-apocryphal) adage that correction spreads faster than agreement, and why that shapes how good technical discussion actually happens.",{"title":293,"searchDepth":294,"depth":295,"links":1543},[1544,1545,1546,1547,1548,1549,1550,1551,1552,1553],{"id":1436,"depth":295,"text":1437},{"id":1443,"depth":295,"text":1444},{"id":1450,"depth":295,"text":1451},{"id":1471,"depth":295,"text":1472},{"id":1478,"depth":295,"text":1479},{"id":1488,"depth":295,"text":1489},{"id":1495,"depth":295,"text":1496},{"id":1502,"depth":295,"text":1503},{"id":1515,"depth":295,"text":1516},{"id":261,"depth":295,"text":262},"2026-08-06","Everything here is a report from where I'm standing, not an argument built to relocate you there. If you land somewhere different after actually thinking it through, that's not a miss — that's the point.",{},"/blog/why-writing-engineering-blogs-is-hard",{"title":1425,"description":1555},"blog/13.why-writing-engineering-blogs-is-hard","IOOwICjaiZgTXz79nFQFgtEc4gnGCXdoUqtHRRvy1b4",{"id":1562,"title":1563,"body":1564,"date":1731,"description":1732,"extension":311,"featured":35,"meta":1733,"navigation":313,"path":1734,"readTime":315,"seo":1735,"stem":1736,"tag":117,"__hash__":1737},"blog/blog/12.experience-is-earned-not-downloaded.md","Experience Is Earned, Not Downloaded",{"type":146,"value":1565,"toc":1721},[1566,1573,1577,1586,1590,1593,1606,1610,1619,1623,1644,1648,1651,1655,1658,1660,1663,1665,1667],[149,1567,1568,1569,1572],{},"Credentials, tutorials, bootcamps, and even AI are accelerators for learning — they compress the time it takes to encounter an idea, and that's genuinely valuable. What none of them are is a substitute for hands-on experience operating real systems under real constraints. That distinction sounds obvious stated plainly. It gets lost constantly in practice, because consuming enough content about a topic ",[335,1570,1571],{},"feels"," a lot like acquiring the judgment that topic actually requires, right up until reality tests the difference.",[153,1574,1576],{"id":1575},"knowledge-is-not-experience","Knowledge Is Not Experience",[149,1578,1579,1580,1585],{},"Courses, bootcamps, documentation, conference talks, AI-assisted learning — all of it has real value, and none of it can replicate what gets learned from operating a system real people actually depend on. Reading about distributed systems and operating one under load are different activities that happen to share vocabulary. Understanding the OAuth spec and being the person responsible when authentication starts failing for a fraction of live traffic are different skills wearing the same name. The lessons that come from production failures, debugging under real pressure, and living with the operational cost of an architectural decision made two years ago aren't lessons a document can fully transmit, because they're not primarily propositional knowledge — they're closer to what philosopher Michael Polanyi called ",[197,1581,1584],{"href":1582,"rel":1583},"https://plato.stanford.edu/entries/tacit-knowledge/",[201],"tacit knowledge",": the kind of understanding that's genuinely hard to make fully explicit, built through doing rather than through being told.",[153,1587,1589],{"id":1588},"writing-software-is-easy-owning-software-is-hard","Writing Software Is Easy. Owning Software Is Hard.",[149,1591,1592],{},"Plenty of developers have integrated an API, built a CRUD app, deployed a website, wrapped a third-party SDK. Those are real, useful skills — and they're also a small slice of what engineering actually involves. The harder questions start after deployment: how does this behave under load nobody tested for, how does it recover from a partial failure, how do you migrate millions of live records without downtime, how do you rotate a secret without breaking every service depending on the old one, how does someone three years from now — who wasn't in the room for any of these decisions — support what got built. Software isn't finished when it works. It's finished when it keeps working, under conditions its author never got to choose.",[149,1594,1595,1596,1601,1602,1605],{},"This is also where the difference between stages of expertise becomes visible in a very concrete way. The ",[197,1597,1600],{"href":1598,"rel":1599},"https://en.wikipedia.org/wiki/Dreyfus_model_of_skill_acquisition",[201],"Dreyfus Model of Skill Acquisition"," — developed by Stuart and Hubert Dreyfus and later expanded in their book ",[335,1603,1604],{},"Mind Over Machine"," — describes skill development as a progression through distinct stages, from novices who need explicit rules to follow, through competence, to experts whose decisions increasingly come from pattern recognition and situational judgment rather than conscious rule-application. A novice engineer asks \"how do I build this.\" An expert asks \"what happens when this fails\" or \"who maintains this after I'm gone\" — not because they're smarter, but because they've accumulated enough situations where the first question turned out to be the wrong one to be asking alone.",[153,1607,1609],{"id":1608},"integrating-an-api-is-not-the-same-as-building-a-platform","Integrating an API Is Not the Same as Building a Platform",[149,1611,1612,1613,1618],{},"Calling a third-party API is the easy part. A payment integration, to take a concrete example, is not really about the HTTP request — it's about retries that don't double-charge a customer (a problem real payment APIs solve with ",[197,1614,1617],{"href":1615,"rel":1616},"https://docs.stripe.com/api/idempotent_requests",[201],"idempotency keys"," precisely because \"just retry on failure\" isn't safe by default), reconciliation when two systems' records disagree, fraud signals, observability into failures that happen downstream of your own code, and the operational support structure for when something breaks at 2 a.m. The wrapper around the API is often the least interesting part of the system. Everything experience actually teaches lives in the parts around it.",[153,1620,1622],{"id":1621},"every-production-incident-is-a-teacher-if-the-culture-lets-it-be","Every Production Incident Is a Teacher, If the Culture Lets It Be",[149,1624,1625,1626,1631,1632,1637,1638,1643],{},"This is where experience actually gets encoded into an organization rather than just an individual's memory, and it's worth being specific about how, because \"years on the job\" and \"expertise\" aren't automatically the same thing. Psychologist K. Anders Ericsson's research on ",[197,1627,1630],{"href":1628,"rel":1629},"https://psycnet.apa.org/record/1993-40718-001",[201],"deliberate practice"," found that raw years of repetition don't reliably build expertise on their own — what does is practice with clear feedback on what went wrong and a real chance to correct it next time. A production incident is exactly that kind of feedback, if the organization's culture actually lets the lesson land. In 2012, then-Etsy CTO John Allspaw published ",[197,1633,1636],{"href":1634,"rel":1635},"https://www.sherlocks.ai/blog/blameless-postmortems-explained-lessons-from-real-outages",[201],"\"Blameless PostMortems and a Just Culture,\""," arguing — building on aviation and safety research by Sidney Dekker — that when organizations respond to failure by looking for someone to blame, they get less information out of the incident, not more, because the engineers closest to what happened have every incentive to minimize their own account of it. Google's ",[197,1639,1642],{"href":1640,"rel":1641},"https://sre.google/books/",[201],"Site Reliability Engineering"," practice later formalized the same idea as a core part of its incident process, not an optional cultural nicety layered on top of the \"real\" engineering work. The mechanism matters as much as the sentiment: a postmortem culture that actually extracts the lesson from a failure requires engineers to describe, in full and afraid of nothing, exactly what they saw, assumed, and did — and that's not achievable through reading about the incident afterward. It requires having been the person making decisions in real time, with incomplete information, while the system was actively breaking. That's precisely the kind of judgment no course simulates, because a course, by design, doesn't have real consequences attached to being wrong.",[153,1645,1647],{"id":1646},"titles-and-experience-are-different-things","Titles and Experience Are Different Things",[149,1649,1650],{},"Degrees and certifications open doors and demonstrate commitment — there's nothing wrong with either. But they're not evidence of the same thing engineering experience is evidence of. Organizations routinely buy software from vendors whose teams spent years solving deeply specialized problems, and those products weren't built because someone in the room had the strongest academic credential. They were built through iteration: design, implementation, production support, and the accumulated scar tissue of things that didn't work the first time. Academic environments optimize for theory, correctness, and formal reasoning; industry optimizes for reliability, delivery, and what happens when a customer is affected by a decision made under deadline pressure. Both perspectives are genuinely valuable. Neither substitutes for the other, and the strongest engineers tend to keep drawing on both rather than treating one as having superseded the need for the other.",[153,1652,1654],{"id":1653},"ai-shortens-the-path-to-knowledge-it-doesnt-shorten-the-path-to-wisdom","AI Shortens the Path to Knowledge. It Doesn't Shorten the Path to Wisdom.",[149,1656,1657],{},"AI can explain a concept, generate code, and review an implementation faster than almost any other tool available today, and that's a real, meaningful acceleration of the learning curve. What it can't do is transfer the specific memory of diagnosing an outage that was actually affecting real customers while the clock was running — the instinct that comes from having seen a version of this exact failure mode before and knowing, viscerally rather than theoretically, where to look first. The gap here isn't a knowledge gap AI will eventually close by getting better. It's a category difference: AI compresses the time it takes to encounter an idea. It has no mechanism for compressing the years it takes to develop judgment about which ideas actually hold up once reality gets involved.",[153,1659,252],{"id":251},[149,1661,1662],{},"Education gives engineers vocabulary. Experience gives engineers judgment. Neither one, on its own, is the whole discipline. The goal was never to dismiss academic study or to romanticize experience as though credentials don't matter at all — it's to recognize that the engineers who actually get trusted with ambiguous, high-stakes problems are the ones who've studied, built, broken things, and had to live with the consequences long enough that the next production incident teaches them something a book never could have. That process doesn't have a shortcut. It just has accelerators — and an accelerator is only useful once there's an actual journey underneath it to speed up.",[257,1664],{},[153,1666,262],{"id":261},[264,1668,1669,1677,1683,1691,1701,1713],{},[267,1670,1671,1672,1676],{},"Stanford Encyclopedia of Philosophy, ",[197,1673,1675],{"href":1582,"rel":1674},[201],"\"Tacit Knowledge\""," — on Michael Polanyi's concept of knowledge that resists full explicit articulation and is built through doing.",[267,1678,269,1679,1682],{},[197,1680,1600],{"href":1598,"rel":1681},[201]," — the five-stage framework describing how expertise develops from rule-following toward situational, pattern-based judgment.",[267,1684,1685,1686,1690],{},"Sherlocks.ai, ",[197,1687,1689],{"href":1634,"rel":1688},[201],"\"Blameless Postmortems Explained: Lessons From Real Outages\""," — the history and mechanics of John Allspaw's blameless postmortem practice, originating at Etsy and later adopted industry-wide.",[267,1692,1693,1694,1700],{},"Google, ",[197,1695,1697],{"href":1640,"rel":1696},[201],[335,1698,1699],{},"Site Reliability Engineering: How Google Runs Production Systems"," — the book formalizing blameless postmortems and other operational practices as core engineering discipline, not just an operations afterthought.",[267,1702,1703,1704,1708,1709,1712],{},"K. Anders Ericsson, Ralf Th. Krampe, and Clemens Tesch-Römer, ",[197,1705,1707],{"href":1628,"rel":1706},[201],"\"The Role of Deliberate Practice in the Acquisition of Expert Performance\""," (",[335,1710,1711],{},"Psychological Review",", 1993) — the foundational research distinguishing mere years of repetition from the kind of deliberate, feedback-driven practice that actually builds expertise.",[267,1714,1715,1716,1720],{},"Stripe, ",[197,1717,1719],{"href":1615,"rel":1718},[201],"\"Idempotent Requests\""," — a concrete, widely referenced example of the operational complexity — retries without double-processing — that sits behind a \"simple\" payment API call.",{"title":293,"searchDepth":294,"depth":295,"links":1722},[1723,1724,1725,1726,1727,1728,1729,1730],{"id":1575,"depth":295,"text":1576},{"id":1588,"depth":295,"text":1589},{"id":1608,"depth":295,"text":1609},{"id":1621,"depth":295,"text":1622},{"id":1646,"depth":295,"text":1647},{"id":1653,"depth":295,"text":1654},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-05","Credentials, tutorials, and AI compress the time it takes to encounter an idea. None of them shorten the years it takes to develop judgment about which ideas actually hold up once reality gets involved.",{},"/blog/experience-is-earned-not-downloaded",{"title":1563,"description":1732},"blog/12.experience-is-earned-not-downloaded","IsHONLsk9y55YskZ2-aif5tlZ1OLpCU_Ew_yPsG2BmI",{"id":1739,"title":1740,"body":1741,"date":1856,"description":1857,"extension":311,"featured":35,"meta":1858,"navigation":313,"path":1859,"readTime":459,"seo":1860,"stem":1861,"tag":117,"__hash__":1862},"blog/blog/11.no-si-unit-for-software-engineering.md","There Is No SI Unit for Software Engineering",{"type":146,"value":1742,"toc":1845},[1743,1746,1750,1753,1762,1766,1769,1773,1782,1786,1789,1793,1796,1800,1803,1807,1810,1812,1815,1817,1819],[149,1744,1745],{},"Spend enough time around engineering discourse and you'll notice how often it turns absolute. This is the only correct architecture. You should never use that pattern. Every project should follow these principles. Anything else is bad engineering. It's a comforting way to talk, because certainty is easier to hold onto than nuance — but it doesn't match how the discipline actually works. There's no SI unit for software engineering. No formula that outputs the correct architecture. No checklist that, followed precisely, guarantees good software. What there is instead is a long list of trade-offs, made by people with incomplete information, under constraints that mostly aren't visible to whoever judges the decision later.",[153,1747,1749],{"id":1748},"todays-legacy-was-yesterdays-best-practice","Today's Legacy Was Yesterday's Best Practice",[149,1751,1752],{},"It's easy to look at an old system and ask why anyone would build it that way. It's a much more useful question to ask what constraints existed when the decision was actually made. Cloud infrastructure that's now assumed by default might not have existed. The framework in use might have lacked capabilities considered basic today. Regulations, performance envelopes, organizational priorities, and the collective knowledge available to that team were all different, often in ways that are invisible from the outside once enough time has passed.",[149,1754,1755,1756,1761],{},"This is essentially ",[197,1757,1760],{"href":1758,"rel":1759},"https://fs.blog/chestertons-fence/",[201],"Chesterton's Fence"," applied to code. G.K. Chesterton's original parable is about a reformer who comes across a fence in a field, sees no obvious purpose for it, and wants to tear it down — met by a more careful response: don't remove what you don't understand until you've gone and found out why it's there. The point isn't that every old decision was correct. Some genuinely weren't, and understanding the reasoning sometimes confirms that a change is exactly right. The point is that \"I don't see why this exists\" is a starting question, not a finished argument — and legacy code criticized without asking what it was solving for gets exactly the same treatment as a fence torn down by someone who never checked what it was keeping out.",[153,1763,1765],{"id":1764},"be-careful-mocking-decisions-you-might-have-made-yourself","Be Careful Mocking Decisions You Might Have Made Yourself",[149,1767,1768],{},"Engineering communities have a well-worn habit of piling onto old systems — who designed this, what were they thinking, this architecture is terrible. It's worth remembering that plenty of these systems were built by experienced people: technical leads, architects, principal engineers making calls under deadline pressure, budget limits, and information nobody in that room had the benefit of yet. It's entirely possible that anyone doing the mocking, dropped into that same room at that same time, would have made the same call. Humility isn't a soft skill tacked onto engineering — it's a pretty direct consequence of taking seriously how much of any decision depends on context nobody outside the room can fully reconstruct later.",[153,1770,1772],{"id":1771},"no-architecture-stays-optimal-forever","No Architecture Stays Optimal Forever",[149,1774,1775,1776,1781],{},"Every system accumulates technical debt, and not because the team building it was careless. Business requirements shift. Technology moves. Security guidance changes — sometimes formally and publicly, the way NIST deprecating certain authentication patterns does, and sometimes just by attackers getting better at exploiting what used to be considered safe. Customer expectations move. Every design decision is a compromise struck at a specific moment against a specific set of constraints, and none of those constraints hold still. Fred Brooks made a version of this argument decades ago in his essay ",[197,1777,1780],{"href":1778,"rel":1779},"https://en.wikipedia.org/wiki/No_Silver_Bullet",[201],"\"No Silver Bullet\"",", arguing that no single technique or technology eliminates the fundamental difficulty of building software — there's no tool, pattern, or methodology that, adopted once, keeps a system optimal indefinitely. Brooks was writing about a different set of tools than we use today, and the argument hasn't aged out of relevance at all; it's arguably proven itself again with every subsequent \"this changes everything\" technology since.",[153,1783,1785],{"id":1784},"there-is-no-perfect-blueprint-and-thats-not-a-gap-waiting-to-be-filled","There Is No Perfect Blueprint, and That's Not a Gap Waiting to Be Filled",[149,1787,1788],{},"A lot of engineering energy goes into searching for the definitive way to structure a project — the correct folder layout, the correct pattern, the correct stack — as though the search just hasn't concluded yet. It has concluded, in the sense that matters: it doesn't resolve to a single answer. Two experienced teams can solve the same problem in genuinely different ways and both end up with something correct, maintainable, and successful. That's not a sign that one of them is wrong and just hasn't been found out yet. It's a sign that the problem had more than one valid solution, which is true of almost every non-trivial engineering problem worth having an opinion about.",[153,1790,1792],{"id":1791},"preferences-dressed-up-as-standards","Preferences Dressed Up as Standards",[149,1794,1795],{},"A lot of the certainty in engineering discourse is personal preference wearing the costume of objective law. \"Every project must use Clean Architecture.\" \"Microservices are always better.\" \"Every class needs an interface.\" These are opinions shaped by real experience, which is exactly why they're worth listening to — and exactly why they're not universal. A pattern that worked well in one organization, under one set of constraints, can introduce nothing but unnecessary complexity in another. Ask ten experienced engineers how to solve the same problem and it's entirely plausible to get ten different, all-defensible answers, because they're optimizing for different things: simplicity, scalability, delivery speed, operational maturity, developer experience. None of those priorities is inherently wrong. They're different axes in a genuinely multi-dimensional problem, and optimizing hard for one usually means accepting a real cost on another.",[153,1797,1799],{"id":1798},"every-team-has-its-own-captain","Every Team Has Its Own Captain",[149,1801,1802],{},"Every engineering team develops a philosophy shaped by what's actually worked for them — minimalism, abstraction, explicitness, speed, long-term maintainability. Team leads naturally trust the approach that's succeeded in their environment, and that confidence is earned, not arbitrary. The mistake isn't having that confidence. It's assuming it should generalize automatically to teams operating under a completely different set of constraints, where the same approach might solve nothing that's actually wrong there.",[153,1804,1806],{"id":1805},"principles-endure-dogma-doesnt","Principles Endure. Dogma Doesn't.",[149,1808,1809],{},"There's a real difference between the two, and it's worth being precise about it instead of using them interchangeably. Principles — reduce coupling, improve cohesion, design for change, protect business rules, avoid unnecessary complexity — hold up across contexts because they describe trade-offs rather than prescribing specific answers. Dogma collapses that into \"there is only one correct way,\" which sounds like confidence and functions like a shortcut around actually engaging with a problem's specific constraints. Engineering that's actually good at what it does tends to hold principles firmly and hold specific implementations loosely — because every abstraction adds cognitive load, every simplification trades away some flexibility, every dependency is a small bet on someone else's future decisions, and no optimization is free. Understanding those trade-offs, specifically, for the problem in front of you, is what separates an engineering decision from a preference asserted with enough confidence to sound like one.",[153,1811,252],{"id":251},[149,1813,1814],{},"Software engineering was never a competition to discover the one true architecture. It's the ongoing work of making the best available decision under conditions that keep changing, using information that's always incomplete. Legacy systems deserve context before criticism. Current systems deserve humility before certainty, because they'll be someone else's legacy sooner than anyone building them expects. The goal was never to build something that satisfies every engineer's aesthetic preference. It's to build something that solves the problem it exists to solve — and there's no SI unit, no formula, and no universal blueprint that gets to skip the part where you actually understand the problem first.",[257,1816],{},[153,1818,262],{"id":261},[264,1820,1821,1829,1836],{},[267,1822,1823,1824,1828],{},"Farnam Street, ",[197,1825,1827],{"href":1758,"rel":1826},[201],"\"Chesterton's Fence: A Lesson in Thinking\""," — the origin and modern application of the principle that reforms shouldn't happen until the reasoning behind the current state is understood.",[267,1830,269,1831,1835],{},[197,1832,1834],{"href":1778,"rel":1833},[201],"No Silver Bullet — Essence and Accident in Software Engineering"," — Fred Brooks's 1986 essay arguing no single technique eliminates software's essential difficulty.",[267,1837,1838,1839,1844],{},"Thoughtbot, ",[197,1840,1843],{"href":1841,"rel":1842},"https://thoughtbot.com/blog/chestertons-fence",[201],"\"Chesterton's Fence: Understanding Past Decisions\""," — a practical, engineering-adjacent take on applying the principle before removing something you don't fully understand.",{"title":293,"searchDepth":294,"depth":295,"links":1846},[1847,1848,1849,1850,1851,1852,1853,1854,1855],{"id":1748,"depth":295,"text":1749},{"id":1764,"depth":295,"text":1765},{"id":1771,"depth":295,"text":1772},{"id":1784,"depth":295,"text":1785},{"id":1791,"depth":295,"text":1792},{"id":1798,"depth":295,"text":1799},{"id":1805,"depth":295,"text":1806},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-04","No formula outputs the correct architecture, and no checklist guarantees good software. What exists instead is a long list of trade-offs made under constraints nobody judging the decision later can fully see.",{},"/blog/no-si-unit-for-software-engineering",{"title":1740,"description":1857},"blog/11.no-si-unit-for-software-engineering","6niGeZF0D5d-G88DHU1ewG2R2KFmwwvsO8i9skd8j6w",{"id":1864,"title":1865,"body":1866,"date":1990,"description":1991,"extension":311,"featured":35,"meta":1992,"navigation":313,"path":1993,"readTime":459,"seo":1994,"stem":1995,"tag":1421,"__hash__":1996},"blog/blog/10.ownership-and-career-growth.md","Hard Skills Get You in the Room. Ownership Is What Keeps You There.",{"type":146,"value":1867,"toc":1983},[1868,1871,1875,1884,1888,1897,1901,1904,1915,1918,1922,1931,1944,1947,1949,1951],[149,1869,1870],{},"Technology changes faster than any individual's expertise can keep pace with permanently. The stack you're deep in today will be someone's legacy system in five years. The company you're at will outgrow the org chart you joined into. Colleagues will move on, move up, or move sideways past you. None of that is a threat to plan around — it's just the baseline condition of a career in this field, and the people who thrive in it aren't the ones who found a way to stop it from happening. They're the ones who got good at operating inside it.",[153,1872,1874],{"id":1873},"hard-skills-open-the-door-they-dont-determine-what-happens-after-you-walk-through-it","Hard Skills Open the Door. They Don't Determine What Happens After You Walk Through It",[149,1876,1877,1878,1883],{},"Technical depth is what gets an engineer into the room in the first place — nobody's arguing otherwise. But once you're in the room, the thing that determines whether you thrive over years rather than one good project is how well you work with people who don't think the way you do. That means communicating with people who have different skill sets, different professional backgrounds, and different mental models for approaching the same problem. It means being effective with people who disagree with your approach and turn out to have a point. Psychologist Carol Dweck's research on ",[197,1879,1882],{"href":1880,"rel":1881},"https://www.mindsetworks.com/science/",[201],"growth mindset"," is relevant here for a reason that goes beyond individual learning: teams and organizations that treat ability as something that develops through effort and feedback, rather than something fixed and already sorted, consistently outperform ones that don't — and that culture doesn't exist without people willing to be genuinely collaborative with people unlike themselves, not just technically correct in isolation.",[153,1885,1887],{"id":1886},"growth-isnt-a-synonym-for-promotion","Growth Isn't a Synonym for Promotion",[149,1889,1890,1891,1896],{},"It's easy to measure growth only in title changes, and easy to feel stuck when a promotion isn't imminent. But growth that actually compounds over a career usually looks smaller and more frequent than that: a hard problem solved with a new approach, a domain understood for the first time, a mistake examined honestly enough to actually change how you work afterward. Learning to ask for feedback — and, harder, learning to actually receive it without getting defensive or dismissive — is its own skill, and it's one that determines how much of the growth available to you actually reaches you. ",[197,1892,1895],{"href":1893,"rel":1894},"https://www.radicalcandor.com/",[201],"Radical Candor",", Kim Scott's framework built from her time managing at Google and Apple, frames this as a two-way obligation: feedback only works as a growth mechanism when it's given with genuine care and received without either crumbling or shutting it out. Growth that depends entirely on formal review cycles and promotion committees is growth you've outsourced to a process. Growth that comes from mistakes examined, successes understood well enough to repeat, and feedback actually taken in — that's growth you own.",[153,1898,1900],{"id":1899},"nobody-else-is-going-to-own-your-career-for-you","Nobody Else Is Going to Own Your Career For You",[149,1902,1903],{},"This is the part that's easy to nod along with and hard to actually practice: your career is not something a manager, a company, or a performance review cycle is responsible for on your behalf. They can support it, fund it, create opportunities for it — but the ownership of what you learn, how you grow, what you do with a failure, and what you do with a success is yours by default, whether or not you've claimed it. Outsourcing that ownership doesn't make it go away. It just means someone else's priorities end up filling the space where your own should have been.",[149,1905,1906,1907,1910,1911,1914],{},"Ownership shows up as a concrete question worth asking regularly, not just at review time: what impact does my work actually have — on the company, on my team, on my own trajectory? Most engineers can describe the technologies they used on a project in detail. Far fewer can describe how that project moved a business outcome, and that gap is exactly where a lot of otherwise strong engineers plateau. Knowing a technology stack fluently and understanding how your use of it contributed to the company's actual goals are different kinds of knowledge, and only one of them is usually being actively developed day to day. Deliberately building the second — sometimes called business acumen — means asking, on a real project, not just ",[335,1908,1909],{},"what did I build"," but ",[335,1912,1913],{},"what did this actually do for the business",", and being able to answer it in terms someone outside engineering would recognize as an outcome, not a tech stack.",[149,1916,1917],{},"None of that impact does much good if it's invisible. Making the outcome of your work visible — to your team, to your company, to the people whose support your career depends on — isn't self-promotion for its own sake. It's closing the loop between doing valuable work and that value actually being known, which is a step a lot of technically strong people skip entirely, assuming the work will speak for itself. It rarely does, on its own, at the volume needed to actually register.",[153,1919,1921],{"id":1920},"the-dri-model-what-owning-it-end-to-end-actually-looks-like","The DRI Model: What \"Owning It End to End\" Actually Looks Like",[149,1923,1924,1925,1930],{},"There's a useful pattern for what real ownership looks like at the senior end of an engineering career, and it comes from a place a lot of people have heard of without necessarily having the full context: Apple's concept of the ",[197,1926,1929],{"href":1927,"rel":1928},"https://handbook.gitlab.com/handbook/people-group/directly-responsible-individuals/",[201],"Directly Responsible Individual (DRI)"," — since adopted, under various names, by companies like GitLab that have documented it publicly. The idea is simple and a little uncomfortable: for any given initiative, exactly one person is the one whose name comes up when someone asks \"who owns this,\" and that person is accountable for the outcome end to end, even when the work is genuinely collaborative and involves people they don't manage. It's not about doing everything alone. It's about there being no ambiguity, ever, about who's actually driving the thing to a real outcome — which is precisely the pattern that separates the engineers who can be handed an open-ended, poorly specified problem and trusted to bring back a real solution, from engineers who need the problem pre-decomposed before they can start.",[149,1932,1933,1934,1939,1940,1943],{},"This is worth distinguishing clearly from a related but different framework: the ",[197,1935,1938],{"href":1936,"rel":1937},"https://en.wikipedia.org/wiki/Responsibility_assignment_matrix",[201],"RACI matrix"," — Responsible, Accountable, Consulted, Informed — a project-management technique for mapping who does the work, who's ultimately answerable for it, whose input gets sought, and who just needs to stay in the loop. RACI is a tool for clarifying a lot of stakeholders' relationships to one task at once; DRI is a cultural stance about one person owning an outcome without hiding behind the ambiguity a group can create. They're solving related problems — confusion about who's actually responsible for what — but a DRI is closer to ",[335,1941,1942],{},"being"," the Accountable role in a RACI chart taken seriously as an identity, not just a matrix cell.",[149,1945,1946],{},"Becoming the kind of engineer who gets handed an abstract, underspecified problem and trusted to own the solution isn't a status conferred by title. It's built the same way everything else in this piece is built: by taking ownership before it's assigned, making the resulting impact visible, asking for and actually absorbing feedback on how it went, and doing it again on the next ambiguous thing that shows up — which, in this field, is never in short supply.",[257,1948],{},[153,1950,262],{"id":261},[264,1952,1953,1960,1968,1976],{},[267,1954,269,1955,1959],{},[197,1956,1958],{"href":1936,"rel":1957},[201],"Responsibility assignment matrix (RACI)"," — the standard reference for the Responsible/Accountable/Consulted/Informed framework.",[267,1961,1962,1963,1967],{},"GitLab Handbook, ",[197,1964,1966],{"href":1927,"rel":1965},[201],"Directly Responsible Individuals (DRI)"," — one of the few large organizations to publicly document how they apply the Apple-originated DRI concept.",[267,1969,1970,1971,1975],{},"Carol Dweck / Mindset Works, ",[197,1972,1974],{"href":1880,"rel":1973},[201],"\"Decades of Scientific Research That Started a Growth Mindset Revolution\""," — the research behind treating ability and skill as things that develop through effort and feedback.",[267,1977,1978,1979,1982],{},"Kim Scott, ",[197,1980,1895],{"href":1893,"rel":1981},[201]," — a framework for giving and receiving feedback built from experience managing teams at Google and Apple.",{"title":293,"searchDepth":294,"depth":295,"links":1984},[1985,1986,1987,1988,1989],{"id":1873,"depth":295,"text":1874},{"id":1886,"depth":295,"text":1887},{"id":1899,"depth":295,"text":1900},{"id":1920,"depth":295,"text":1921},{"id":261,"depth":295,"text":262},"2026-08-03","Technical depth gets an engineer into the room. What determines whether they thrive over years is how well they collaborate, absorb feedback, and own outcomes nobody assigned them.",{},"/blog/ownership-and-career-growth",{"title":1865,"description":1991},"blog/10.ownership-and-career-growth","9oJd2nCGZ5h7tjhdBEjAG5obGrMFFQUEDeFb0Z6WVcs",{"id":1998,"title":1999,"body":2000,"date":1990,"description":2160,"extension":311,"featured":35,"meta":2161,"navigation":313,"path":2162,"readTime":1146,"seo":2163,"stem":2164,"tag":462,"__hash__":2165},"blog/blog/8.multiple-wrongs-banking-patterns.md","Multiple Wrongs Do Not Make a Right: Challenging Industry Patterns in Banking Software",{"type":146,"value":2001,"toc":2153},[2002,2008,2012,2023,2026,2030,2033,2052,2055,2065,2084,2087,2094,2098,2101,2104,2108,2111,2113,2115],[149,2003,2004,2005,2007],{},"In regulated industries like banking, \"other banks do it this way\" gets treated as a load-bearing argument far more often than it should. The reasoning is understandable — banks share regulations, share customer journeys, share compliance obligations, so convergence on similar products is natural. But convergence isn't the same thing as correctness. A pattern repeated across an entire industry can be a genuinely proven practice, a regulatory necessity, or it can simply be a decision nobody has revisited since the system that first implemented it was new. From the outside, those three look identical. Only asking ",[335,2006,930],{}," tells them apart.",[153,2009,2011],{"id":2010},"bank-a-does-it-answers-the-wrong-question","\"Bank A Does It\" Answers the Wrong Question",[149,2013,2014,2015,2018,2019,2022],{},"\"This is how other banks do onboarding\" answers ",[335,2016,2017],{},"is this common",". It says nothing about ",[335,2020,2021],{},"is this the most secure, least exploitable, best-fit approach for what we're actually trying to prevent",". Those are different questions, and treating the first as a stand-in for the second is exactly how weak patterns propagate without anyone deciding to adopt them on purpose.",[149,2024,2025],{},"The right questions for any onboarding requirement look more like: does this reduce fraud in a way we can point to? Does it resist the attack methods that are actually current? Does it create exposure — data collected that becomes a liability if breached, without a corresponding security or compliance benefit? Does current guidance still support this, or did the guidance move on while the requirement stayed put? Security questions and mandatory password rotation were both once industry-standard. Both turned out to be patterns that survived on inertia long after the reasoning behind them stopped holding up. Nothing about \"everyone does this\" prevented that.",[153,2027,2029],{"id":2028},"two-concrete-examples-worth-actually-interrogating","Two Concrete Examples Worth Actually Interrogating",[149,2031,2032],{},"It's easy to agree with \"question your assumptions\" in the abstract and much harder to do it against a specific requirement sitting in your own onboarding flow. Two common ones are worth walking through directly, because both turn out to hinge on something templates rarely question: not whether to collect something, but where it sits relative to everything else in the flow.",[149,2034,2035,2041,2042,950,2045,2048,2049,342],{},[936,2036,2037,2038,2040],{},"Why does onboarding ask for next of kin, and why does it ask ",[335,2039,1467],{},"?"," This is worth real scrutiny — not necessarily to remove the field, but to question where it sits in the flow. Standard KYC and AML guidance — the frameworks that actually drive most mandatory onboarding fields — centers on verifying ",[335,2043,2044],{},"who the customer is",[335,2046,2047],{},"what risk they present",": legal name, date of birth, address, government ID, beneficial ownership for entities. Next-of-kin doesn't verify identity and doesn't feed a fraud or AML risk model; its real justification is usually operational, borrowed from insurance and legacy retail-banking products — knowing who to contact for a dormant account or an estate after a customer's death. That's a legitimate reason to collect it. It is not, however, a reason to collect it ",[335,2050,2051],{},"before the account exists",[149,2053,2054],{},"Once the requirement is understood that way, the sequencing question answers itself: next of kin doesn't gate identity verification or account creation the way a government ID or a phone-risk check does, so it doesn't belong in the same critical path as those steps. A stronger design treats it as a post-login profile-completion task — surfaced to the customer once they can already authenticate, framed as \"finish setting up your account\" rather than bundled into the identity-critical funnel where every extra field is measured against abandonment. This is the same instinct that makes onboarding friction dangerous in the first place: every field added to the front of the funnel is friction against the steps that actually determine whether the account should exist at all, and a field that doesn't gate that decision shouldn't be allowed to add drag to it. The mistake isn't collecting next of kin. It's treating \"we need this eventually\" and \"we need this before you can log in\" as the same requirement, when they're not.",[149,2056,2057,2060,2061,2064],{},[936,2058,2059],{},"Why verify a phone number before starting eKYC — or should it come last instead?"," The instinct to check a phone number early makes sense as a cheap filter: reject an obviously bad number (VoIP, recently SIM-swapped) before spending money on document verification and liveness checks. But there's a stronger version of this control that only becomes available if phone verification comes ",[335,2062,2063],{},"last",", after device fingerprint, ID document, and face match have already been captured.",[149,2066,2067,2068,2071,2072,2075,2076,2079,2080,2083],{},"Here's why the ordering matters. Once a device, an ID, and a face have already been bound together earlier in the flow, the phone verification step stops being an isolated check and becomes a correlation point. A device that's already associated with ID ",[335,2069,2070],{},"Y"," and face ",[335,2073,2074],{},"U"," attempting phone verification and failing tells you something specific depending on ",[335,2077,2078],{},"how"," it failed: dropped at the OTP step could mean the number was recently swapped, that delivery genuinely failed, or something else worth a closer look — and if that same device then cycles through several different phone numbers trying to find one that gets past verification, that pattern is a far stronger fraud signal than any single number's risk score in isolation. A phone number checked in isolation, before any device or identity binding exists, can only be judged against generic signals like line type or carrier history. The same check run ",[335,2081,2082],{},"after"," device-ID-face binding can be judged against a specific, already-established identity claim — and a device burning through multiple numbers against one claimed identity is exactly the kind of pattern that justifies blocking the device outright rather than just rejecting one number.",[149,2085,2086],{},"That's a genuinely different design than \"verify the phone early to save money on document checks,\" and it's arguably the stronger one for fraud detection specifically, even though it accepts the cost of running device and document capture before knowing whether the phone number will hold up. Which trade-off wins depends on what a given bank is optimizing for — cost containment on the expensive steps, or maximizing the fraud signal available at the point phone verification actually happens. Neither answer is \"obviously correct\" in the abstract. What matters is that the choice gets made deliberately, with the correlation trade-off understood, rather than defaulting to whichever order a template shipped with.",[149,2088,2089,2090,2093],{},"The lesson from putting these two side by side isn't \"legacy fields bad, verification steps good.\" Neither turns out to be a keep-or-cut decision at all — both are sequencing questions that only reveal themselves once someone asks where a step sits relative to everything around it. Next of kin doesn't gate anything, so it belongs after login, as a profile-completion task. Phone verification gates a fraud decision, but ",[335,2091,2092],{},"which"," fraud decision it can support depends entirely on what's already been established before it runs — a phone check with no device or identity binding behind it can only judge the number in isolation, while the same check placed after device-ID-face capture can catch a device cycling through numbers against one claimed identity, which is a meaningfully stronger signal. The only way to find the right position for a given requirement is to ask what it depends on and what depends on it, rather than accepting \"this is standard\" — or even \"this is standard, early\" — as a complete answer.",[153,2095,2097],{"id":2096},"onboarding-is-a-security-boundary-not-just-a-funnel","Onboarding Is a Security Boundary, Not Just a Funnel",[149,2099,2100],{},"Customer onboarding determines who gets an account, what identity gets trusted, and what data enters the system in the first place. A weak onboarding process creates fraudulent accounts, identity theft exposure, and mule-account risk that no amount of downstream authentication hardening can fully undo — you can't retroactively verify an identity that was never properly checked at the door. That's also exactly why onboarding is such fertile ground for inherited, unexamined requirements: it's high-stakes, expensive to redesign, and every field in it was, at some point, someone's deliberate decision — even if that someone left the organization a decade ago and the reasoning left with them.",[149,2102,2103],{},"Business continuity deserves the same scrutiny and rarely gets it. Most onboarding design centers on the happy path: identity provider responds, fraud checks clear, document upload succeeds. Real systems need an answer for what happens when the identity provider is down, when fraud scoring is unavailable, when a legitimate customer's device or number has changed since registration. A flow copied from a template that never had to answer those questions inherits that gap along with everything else.",[153,2105,2107],{"id":2106},"security-isnt-decided-by-popularity","Security Isn't Decided by Popularity",[149,2109,2110],{},"If ten banks implement the same onboarding pattern, the eleventh bank doesn't inherit their security by copying it — it inherits their assumptions, unexamined, plus whatever has changed in the threat landscape since those assumptions were made. The standard to evaluate against is evidence, current guidance, and an honest threat model specific to what's actually being built — not how many other institutions happen to be doing the same thing. Common and secure sometimes overlap. When they do, it's worth being able to say why, the same way it's worth being able to say why when they don't.",[257,2112],{},[153,2114,262],{"id":261},[264,2116,2117,2126,2135,2144],{},[267,2118,2119,2120,2125],{},"ComplyCube, ",[197,2121,2124],{"href":2122,"rel":2123},"https://www.complycube.com/en/customer-onboarding-kyc-for-banks/",[201],"\"Customer Onboarding KYC for Banks\""," — on why onboarding data collection should be proportionate to identity and risk assessment, and the cost of collecting more than that requires.",[267,2127,2128,2129,2134],{},"U.S. Bank, ",[197,2130,2133],{"href":2131,"rel":2132},"https://www.usbank.com/corporate-and-commercial-banking/insights/risk/compliance/why-kyc-for-organizations.html",[201],"\"Why Know Your Customer (KYC): for Organizations\""," — an overview of what KYC data collection is actually meant to establish.",[267,2136,2137,2138,2143],{},"Forbes Business Council, ",[197,2139,2142],{"href":2140,"rel":2141},"https://www.forbes.com/councils/forbesbusinesscouncil/2026/07/08/fintech-must-treat-phone-number-authentication-as-infrastructure/",[201],"\"Fintech Must Treat Phone Number Authentication As Infrastructure\""," — on pre-verification signals like SIM-swap and line-type checks as a real fraud-prevention layer, not a formality.",[267,2145,2146,2147,2152],{},"Message Central, ",[197,2148,2151],{"href":2149,"rel":2150},"https://www.messagecentral.com/blog/sim-swap-fraud-protection-usa",[201],"\"SIM Swap Fraud Protection: OTP Verification Defense\""," — a practical breakdown of why checking a phone number's risk profile before sending a one-time code closes a real account-takeover gap.",{"title":293,"searchDepth":294,"depth":295,"links":2154},[2155,2156,2157,2158,2159],{"id":2010,"depth":295,"text":2011},{"id":2028,"depth":295,"text":2029},{"id":2096,"depth":295,"text":2097},{"id":2106,"depth":295,"text":2107},{"id":261,"depth":295,"text":262},"\"Other banks do it this way\" answers is this common, not is this the right control. Two onboarding requirements worth interrogating on their own merits, not on precedent.",{},"/blog/multiple-wrongs-banking-patterns",{"title":1999,"description":2160},"blog/8.multiple-wrongs-banking-patterns","oERWZxePFANKRN5JQIuG1C4Am-kEUoKSkb1popt0heg",{"id":2167,"title":2168,"body":2169,"date":1990,"description":2352,"extension":311,"featured":35,"meta":2353,"navigation":313,"path":2354,"readTime":315,"seo":2355,"stem":2356,"tag":117,"__hash__":2357},"blog/blog/9.ai-engineering-judgment.md","AI Knows Your Language. It Doesn't Know Your Business.",{"type":146,"value":2170,"toc":2342},[2171,2174,2177,2181,2191,2204,2208,2211,2218,2222,2225,2229,2240,2257,2261,2264,2268,2271,2275,2278,2281,2283,2285],[149,2172,2173],{},"AI has become one of the most genuinely useful tools in a software engineer's kit. It spots code smells, flags common vulnerabilities, suggests refactorings, explains unfamiliar code in seconds, and takes real drudgery off a developer's plate. Used well, it makes engineers faster. Used carelessly, it quietly replaces engineering judgment with statistical pattern matching — and organizations are starting to let that happen without noticing.",[149,2175,2176],{},"The problem was never the technology. It's what gets outsourced to it.",[153,2178,2180],{"id":2179},"fluent-in-the-syntax-illiterate-in-the-business","Fluent in the Syntax, Illiterate in the Business",[149,2182,2183,2184,2187,2188,2190],{},"Large language models are trained on enormous volumes of public source code, which makes them genuinely good at recognizing what code ",[335,2185,2186],{},"looks like",": common design patterns, language idioms, framework conventions, known vulnerability shapes, style inconsistencies. What they have no access to is ",[335,2189,930],{}," the software exists in the first place — who the customers are, what regulations apply, what operational constraints shaped a decision, what trade-off a previous team already made and documented nowhere but in a Slack thread from two years ago. That context never made it into the source code, so no amount of training on source code will teach a model to infer it.",[149,2192,2193,2194,2197,2198,2203],{},"Picture a banking application with a conditional that triggers extra verification above a certain transaction amount. A model reviewing that code might reasonably suggest simplifying the branching, extracting a strategy pattern, or improving readability — all technically sound observations about the ",[335,2195,2196],{},"shape"," of the code. What it has no way of knowing is that the threshold exists because of anti-money-laundering regulation, a fraud-detection requirement, or a central bank reporting obligation. To the model, it's just a conditional. To the business, altering it without understanding why it's there could mean real regulatory exposure. The hard part was never the language. It's the domain — which is exactly the thing a discipline like ",[197,2199,2202],{"href":2200,"rel":2201},"https://www.domainlanguage.com/ddd/",[201],"Domain-Driven Design"," was built to take seriously: Eric Evans's foundational argument, in the book of the same name, is that the real complexity in most software lives in the business domain itself, not in the code that represents it, and that the domain has to be modeled deliberately rather than inferred after the fact.",[153,2205,2207],{"id":2206},"when-expert-review-becomes-automated-pattern-matching-in-a-trench-coat","When \"Expert Review\" Becomes Automated Pattern Matching in a Trench Coat",[149,2209,2210],{},"A troubling pattern has started showing up around AI-assisted code review. An organization hires a consultancy for its supposed expertise in some technology stack, expecting experienced engineers to actually look at the software. What sometimes happens instead: the source code gets fed into an LLM, the LLM generates review comments, pull requests get blocked until every comment is addressed, and a report gets delivered that looks comprehensive and professional without anyone involved having understood the business the code serves. Reviewing syntax isn't the same activity as reviewing software, even when the output is formatted identically.",[149,2212,2213,2214,2217],{},"This matters because every review — human or automated — can always find ",[335,2215,2216],{},"something",". Another abstraction, another optimization, another naming tweak, another dependency bump, another architectural alternative. Software is never finished being improvable, which is precisely why engineering isn't about implementing every possible suggestion. It's about deciding which suggestions create meaningful value for this system, at this point in its life, given everything the reviewer knows that the tool doesn't.",[153,2219,2221],{"id":2220},"every-system-is-carrying-technical-debt-on-purpose","Every System Is Carrying Technical Debt On Purpose",[149,2223,2224],{},"No review — automated or otherwise — eliminates technical debt permanently, because debt isn't a symptom of carelessness. It's a consequence of software existing in time. Today's accepted practice is tomorrow's legacy pattern. Today's secure dependency is tomorrow's disclosed vulnerability. Expecting an AI review to produce \"perfect\" code misunderstands what software engineering actually is: an ongoing negotiation between competing pressures, not a one-time correctness proof.",[153,2226,2228],{"id":2227},"blind-compliance-is-the-real-risk-not-bad-suggestions","Blind Compliance Is the Real Risk, Not Bad Suggestions",[149,2230,2231,2232,2235,2236,2239],{},"The more dangerous failure mode isn't an AI making a wrong call — it's an organization that treats every AI comment as a mandatory action item regardless of whether it's right. Once that happens, developers stop asking ",[335,2233,2234],{},"is this recommendation correct"," and start asking ",[335,2237,2238],{},"how do I make the comment disappear",". That's not engineering judgment. It's compliance theater, and it trains a team to optimize for passing a review tool instead of solving the actual problem in front of them.",[149,2241,2242,2243,2250,2251,2256],{},"It's worth being precise about what distinguishes this from a legitimate checklist. Atul Gawande's ",[197,2244,2247],{"href":2245,"rel":2246},"https://www.summrize.com/books/checklist-manifesto-summary",[201],[335,2248,2249],{},"The Checklist Manifesto"," makes a strong case that checklists genuinely save lives in aviation and surgery — but the checklists that work encode hard-won expert judgment about what actually goes wrong, refined by people who understand the domain, kept deliberately short, and used to catch known failure modes rather than to generate an ever-expanding list of stylistic opinions. An AI-generated review that must be fully satisfied before merging isn't that kind of checklist. It's closer to what researchers studying human-automation interaction call ",[197,2252,2255],{"href":2253,"rel":2254},"https://www.techtarget.com/searchitoperations/definition/What-is-automation-bias",[201],"automation bias"," — the well-documented tendency for people to defer to an automated system's output even when it's wrong or beside the point, especially under time pressure. A pull request gate that can't be reasoned with produces exactly the conditions where that bias thrives.",[153,2258,2260],{"id":2259},"engineering-is-the-art-of-trade-offs-and-trade-offs-need-context","Engineering Is the Art of Trade-offs, and Trade-offs Need Context",[149,2262,2263],{},"Nearly every meaningful software decision is contested by design: optimize for performance or readability, add another abstraction or leave the duplication, refactor now or ship the feature. The honest answer is almost always \"it depends\" — on business value, delivery timelines, regulatory exposure, operational risk, and cost of maintenance. None of that is recoverable from source code alone, which is exactly why the best code reviews have always been conversations rather than verdicts. A reviewer asks why something was built a certain way; the author explains that changing it would break an integration contract with hundreds of existing customers; the conversation resolves the question in a way no static analysis ever could. An AI can surface the \"why did you do it this way\" question. It can't have the conversation that follows, because the answer lives in institutional memory, not in the diff.",[153,2265,2267],{"id":2266},"expertise-is-bigger-than-fluency","Expertise Is Bigger Than Fluency",[149,2269,2270],{},"Knowing C# doesn't make someone a banking expert. Knowing Java doesn't make someone a healthcare expert. Knowing Python doesn't make someone an aviation expert. Real expertise is technical knowledge plus domain understanding plus operational experience plus historical context plus a feel for business priorities — and AI, today, is genuinely useful for exactly one of those five ingredients. That's not a small contribution. It's just not the whole job, and treating it as though it were is how organizations end up rejecting sound engineering decisions because a model disagreed with them for reasons that don't actually apply.",[153,2272,2274],{"id":2273},"use-it-to-start-the-conversation-not-end-it","Use It to Start the Conversation, Not End It",[149,2276,2277],{},"None of this is an argument against using AI in engineering workflows — it's an argument about where the authority sits. AI should surface possible issues, point out alternative approaches, flag security concerns worth a second look, and take repetitive review work off people's plates. It shouldn't become the final word on whether code is \"correct,\" and a pull request shouldn't get rejected simply because a model disagreed with an implementation choice. It should get discussed, because the model noticed something worth a human looking at — which is a meaningfully different thing from the model having the final say.",[149,2279,2280],{},"The future of this work isn't engineers versus AI. It's engineers who stay responsible for the decision, using AI as one more input among several, because software was never just code. It's regulation, operational history, customer commitments, and years of trade-offs that never made it into any file the model could have trained on. AI can read your codebase fluently. It has no way of knowing your bank — and that gap is exactly why the judgment still has to belong to the engineer.",[257,2282],{},[153,2284,262],{"id":261},[264,2286,2287,2297,2306,2315,2325,2333],{},[267,2288,2289,2290,2296],{},"Eric Evans, ",[197,2291,2293],{"href":2200,"rel":2292},[201],[335,2294,2295],{},"Domain-Driven Design: Tackling Complexity in the Heart of Software"," (2003) — the foundational argument that a business domain's complexity, not its code, is where real software difficulty lives.",[267,2298,2299,2300,2305],{},"Martin Fowler, ",[197,2301,2304],{"href":2302,"rel":2303},"https://martinfowler.com/bliki/CodeSmell.html",[201],"\"CodeSmell\""," — the definition of the term Kent Beck coined for the surface-level signals AI code review tools are generally best at spotting.",[267,2307,2308,2309,2314],{},"OWASP, ",[197,2310,2313],{"href":2311,"rel":2312},"https://owasp.org/www-project-top-ten/",[201],"OWASP Top 10"," — the standard reference list of common web application vulnerability classes that automated security scanning (AI-assisted or otherwise) typically checks against.",[267,2316,2317,2318,2324],{},"Atul Gawande, ",[197,2319,2321],{"href":2245,"rel":2320},[201],[335,2322,2323],{},"The Checklist Manifesto: How to Get Things Right"," (2009) — a case for checklists that also clarifies what separates a genuinely useful checklist from a rubber-stamp gate.",[267,2326,2327,2328,2332],{},"TechTarget, ",[197,2329,2331],{"href":2253,"rel":2330},[201],"\"What Is Automation Bias?\""," — an overview of the tendency to over-trust automated recommendations even when they're wrong, and why that risk grows under time pressure.",[267,2334,2335,2336,2341],{},"Ward Cunningham, ",[197,2337,2340],{"href":2338,"rel":2339},"https://c2.com/doc/oopsla92.html",[201],"\"The WyCash Portfolio Management System\""," (1992) — the original description of \"technical debt\" as a natural, ongoing cost of shipping software rather than a sign of carelessness.",{"title":293,"searchDepth":294,"depth":295,"links":2343},[2344,2345,2346,2347,2348,2349,2350,2351],{"id":2179,"depth":295,"text":2180},{"id":2206,"depth":295,"text":2207},{"id":2220,"depth":295,"text":2221},{"id":2227,"depth":295,"text":2228},{"id":2259,"depth":295,"text":2260},{"id":2266,"depth":295,"text":2267},{"id":2273,"depth":295,"text":2274},{"id":261,"depth":295,"text":262},"Used well, AI makes engineers faster. Used carelessly, it quietly replaces engineering judgment with statistical pattern matching — and organizations are starting to let that happen without noticing.",{},"/blog/ai-engineering-judgment",{"title":2168,"description":2352},"blog/9.ai-engineering-judgment","Azo5MMBV0BNEHiyhiIe93YPcoaud5xxwLrYONp7zau4",{"id":2359,"title":2360,"body":2361,"date":2494,"description":2495,"extension":311,"featured":35,"meta":2496,"navigation":313,"path":2497,"readTime":459,"seo":2498,"stem":2499,"tag":462,"__hash__":2500},"blog/blog/7.security-in-product-requirements.md","A Secure Implementation Cannot Save an Insecure Product Decision",{"type":146,"value":2362,"toc":2484},[2363,2366,2369,2373,2376,2391,2395,2398,2402,2405,2414,2418,2421,2425,2428,2432,2439,2441,2444,2446,2448],[149,2364,2365],{},"Most security conversations happen at the code review stage: is input validated, are dependencies patched, does this endpoint leak anything it shouldn't. Those questions matter. But by the time a pull request exists, the most consequential security decision may already be locked in — because it was made in a product requirements document, weeks or months before anyone wrote a line of code.",[149,2367,2368],{},"A team can implement a specification flawlessly and still ship something insecure, if the specification itself was built on a security assumption that stopped being true years ago. Security isn't only a coding problem. Sometimes the vulnerability was designed in on purpose, by people who had no idea they were introducing one.",[153,2370,2372],{"id":2371},"the-requirement-is-part-of-the-security-boundary","The Requirement Is Part of the Security Boundary",[149,2374,2375],{},"Take a familiar example: \"users must answer security questions to recover their account.\" An engineering team can build this feature to a genuinely high standard — encrypt the stored answers, rate-limit recovery attempts, log every recovery event, monitor for abuse, protect the API surface end to end. Every one of those controls can pass review and testing. None of it touches the actual question: is the recovery mechanism itself sound? If the \"secret\" an attacker needs is something they can find in ten minutes of searching, no amount of implementation quality changes the answer.",[149,2377,2378,2379,2384,2385,2390],{},"This isn't hypothetical or dated. ",[197,2380,2383],{"href":2381,"rel":2382},"https://pages.nist.gov/800-63-4/sp800-63b.html",[201],"NIST's Digital Identity Guidelines"," — the federal standard that much of the industry treats as a baseline — explicitly prohibit knowledge-based authentication as a recovery or verification mechanism. The reasoning is direct: answers to questions like \"what was your first pet's name\" or \"what city were you born in\" are disproportionately available through social media, public records, and the enormous volume of data already sitting in breach dumps. One ",[197,2386,2389],{"href":2387,"rel":2388},"https://authlayer.dev/blog/nist-800-63-4-authentication-requirements/",[201],"analysis of the standard's evolution"," put it plainly: searching a person's name alongside a hometown or school on public sources now surfaces answers to the majority of a typical security-question set — before an attacker has done anything more sophisticated than a search engine query. The problem was never how the answer is stored. It's whether the answer was ever a real secret.",[153,2392,2394],{"id":2393},"engineers-cannot-fix-a-requirement-they-didnt-write","Engineers Cannot Fix a Requirement They Didn't Write",[149,2396,2397],{},"This is where the usual security workflow quietly breaks down. Product defines the requirement, engineering builds it, QA verifies the behavior matches spec, security reviews the implementation, and the feature ships. Every person in that chain can do their job correctly. If the requirement itself encoded an outdated security model, the weakness survives the entire process untouched — because nobody in that pipeline was actually positioned to question whether the feature should exist in its current form. Code review answers \"is this implementation safe?\" It doesn't answer \"is this feature design still considered safe?\" — and by the time code review happens, that second question usually isn't on the table anymore.",[153,2399,2401],{"id":2400},"security-guidance-has-to-reach-the-requirements-stage-not-just-the-code-review-stage","Security Guidance Has to Reach the Requirements Stage, Not Just the Code Review Stage",[149,2403,2404],{},"This is precisely why security guidance can't be treated as something engineering consults during implementation and product teams never see. NIST's guidance on knowledge-based authentication didn't change quietly — it was a documented, public reversal, the kind of update that a security-aware product process should catch automatically, the same way a dependency scanner catches a deprecated package. But a product requirements document written five years ago doesn't get a CVE alert when the security assumptions underneath it expire. Someone has to go looking, and that someone increasingly needs to sit upstream of engineering, not downstream of it.",[149,2406,2407,2408,2413],{},"CISA's ",[197,2409,2412],{"href":2410,"rel":2411},"https://www.cisa.gov/securebydesign/pledge",[201],"Secure by Design pledge"," — signed by dozens of major software manufacturers — frames this exact shift as an industry-wide priority: security should be treated as a core business requirement embedded during the design phase, not a technical feature bolted on afterward or verified only at the review stage. The pledge's own framing is worth taking literally: products should arrive secure by default, with the expensive, hard-to-reverse decisions about authentication, defaults, and data handling made correctly before a single feature ships — because retrofitting security into a shipped product is a fundamentally different (and more expensive) problem than designing it in from the requirement forward.",[153,2415,2417],{"id":2416},"requirements-age-the-same-way-code-does","Requirements Age the Same Way Code Does",[149,2419,2420],{},"Security recommendations aren't static, because the conditions they respond to aren't static. Attackers develop new techniques. Data that used to be private becomes searchable. Platforms add authentication capabilities that didn't exist five years ago — passkeys, hardware security keys, and authenticator apps are all comparatively recent, and standards bodies have moved decisively in their direction while moving decisively away from the mechanisms they replace. A requirement that was defensible when it was written can become the weakest part of a system purely by standing still while the threat landscape and the available tooling both moved on. Treating a requirements document as a one-time artifact, reviewed once at creation and never revisited, guarantees that this drift accumulates silently until an incident forces the conversation nobody scheduled.",[153,2422,2424],{"id":2423},"sometimes-the-fix-isnt-a-better-implementation-its-a-different-requirement","Sometimes the Fix Isn't a Better Implementation — It's a Different Requirement",[149,2426,2427],{},"The most effective security intervention in a case like this often isn't a pull request at all. It's a product decision: replacing \"answer three security questions\" with a genuinely strong recovery mechanism — verified email or device-based recovery, an authenticator app, a hardware key — rather than trying to shore up a fundamentally weak mechanism with better encryption and more logging around it. A perfectly implemented version of the wrong design is still the wrong design. Organizations that pour effort into securing the implementation layer while leaving the underlying product decision untouched are optimizing the part of the problem that was never actually broken.",[153,2429,2431],{"id":2430},"security-belongs-in-the-same-conversation-as-usability-and-scalability","Security Belongs in the Same Conversation as Usability and Scalability",[149,2433,2434,2435,2438],{},"Product teams already treat usability, performance, and scalability as first-class questions asked ",[335,2436,2437],{},"during"," design, not bolted on after a feature ships. Security deserves the same standing, asked with the same two-sided framing: not just \"can users complete this workflow,\" but \"can attackers abuse this workflow\" — asked by the people writing the requirement, not deferred entirely to whoever reviews the code six weeks later. The strongest security teams don't only review implementations. They review the assumptions requirements are built on, and they're willing to ask whether a given approach is still the right one given current guidance, not just whether it was defensible when it was first written down.",[153,2440,252],{"id":251},[149,2442,2443],{},"A secure implementation cannot compensate for an insecure product decision. That's not a criticism of engineering — a team building exactly what was specified, to a high technical standard, can still inherit a vulnerability that existed before their first commit. The most expensive security failures are rarely caused by careless code. They're caused by building the wrong thing correctly, at scale, for years, because the assumption behind the requirement was never revisited after the world it was written in had already changed.",[257,2445],{},[153,2447,262],{"id":261},[264,2449,2450,2460,2468,2476],{},[267,2451,883,2452,2459],{},[197,2453,2455,2458],{"href":2381,"rel":2454},[201],[335,2456,2457],{},"Digital Identity Guidelines"," (SP 800-63B)"," — the current federal standard explicitly prohibiting knowledge-based authentication as a verifier mechanism.",[267,2461,2462,2463,2467],{},"AuthLayer, ",[197,2464,2466],{"href":2387,"rel":2465},[201],"\"NIST SP 800-63-4 Is Final: What the New Digital Identity Guidelines Actually Change\""," — a plain-language breakdown of why knowledge-based authentication was deprecated industry-wide.",[267,2469,2470,2471,2475],{},"CISA, ",[197,2472,2474],{"href":2410,"rel":2473},[201],"Secure by Design Pledge"," — the industry framework for treating security as a design-phase business requirement rather than a post-hoc technical feature.",[267,2477,2470,2478,2483],{},[197,2479,2482],{"href":2480,"rel":2481},"https://www.cisa.gov/securebydesign",[201],"Secure by Design"," — CISA's broader initiative and supporting guidance on building security in from the earliest stages of the product lifecycle.",{"title":293,"searchDepth":294,"depth":295,"links":2485},[2486,2487,2488,2489,2490,2491,2492,2493],{"id":2371,"depth":295,"text":2372},{"id":2393,"depth":295,"text":2394},{"id":2400,"depth":295,"text":2401},{"id":2416,"depth":295,"text":2417},{"id":2423,"depth":295,"text":2424},{"id":2430,"depth":295,"text":2431},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-08-02","A team can build a specification flawlessly and still ship something insecure, if the specification itself was built on a security assumption that stopped being true years ago.",{},"/blog/security-in-product-requirements",{"title":2360,"description":2495},"blog/7.security-in-product-requirements","PZDAUqCERlZXtIupvmEUjjSpMVskv4jQ1Lcd_iinGtc",{"id":2502,"title":2503,"body":2504,"date":2795,"description":2796,"extension":311,"featured":35,"meta":2797,"navigation":313,"path":2798,"readTime":2799,"seo":2800,"stem":2801,"tag":462,"__hash__":2802},"blog/blog/3.security-through-obscurity.md","Security Through Obscurity: The Oldest Bad Idea That Refuses to Die",{"type":146,"value":2505,"toc":2784},[2506,2509,2512,2516,2519,2526,2530,2537,2548,2552,2555,2558,2561,2564,2571,2575,2582,2585,2589,2592,2598,2604,2610,2616,2620,2623,2626,2630,2633,2644,2647,2658,2661,2668,2672,2675,2694,2697,2700,2715,2724,2727,2773,2776,2778,2781],[149,2507,2508],{},"Every few years, security through obscurity gets declared dead — and every few years, it turns out someone is still renaming their admin account, moving SSH to a random port, or base64-encoding an API payload and calling it \"encryption.\" The practice never really goes away, because the instinct behind it is completely natural: if nobody can find the door, nobody can walk through it.",[149,2510,2511],{},"The problem is that this instinct is a poor substitute for an actual lock. This post looks at where the idea comes from, why security professionals distrust it so strongly, where it still has a legitimate (if modest) place, and how the same debate plays out today in something as ordinary as an API.",[153,2513,2515],{"id":2514},"where-the-idea-comes-from","Where the Idea Comes From",[149,2517,2518],{},"The term dates back further than most people expect. In 1883, French cryptographer Auguste Kerckhoffs laid out a principle that still anchors modern security thinking: a cryptographic system should stay secure even if everything about it — except the key — becomes public knowledge. A research write-up tracing the history of the idea notes that for centuries, secrecy was the dominant approach to designing secure systems, with military communications relying mainly on the fact that few people understood how they worked rather than on any inherently secure cipher. Kerckhoffs argued the opposite: a well-designed system should assume the enemy already knows exactly how it works, and survive anyway.",[149,2520,2521,2522,2525],{},"This is easy to confuse with \"secrecy is bad,\" but that's not quite it. A password, a private key, a signing secret — these are still secrets, and they're essential. What Kerckhoffs objected to was relying on secrecy about the ",[335,2523,2524],{},"mechanism itself",": the algorithm, the protocol, the lock design. Mechanism-secrecy is fragile in a way that key-secrecy isn't, because a mechanism is shared across every user of the system, gets studied, gets leaked, and — once it's out — can't be changed as easily as swapping a password.",[153,2527,2529],{"id":2528},"an-even-older-origin-story","An Even Older Origin Story",[149,2531,2532,2533,2536],{},"Kerckhoffs wasn't even the first to make this argument publicly. Three decades earlier, in 1851, English locksmith Alfred Charles Hobbs demonstrated to a stunned public how supposedly unpickable, state-of-the-art locks could in fact be picked. Critics worried that showing everyone the flaw would just teach criminals how to exploit it. Hobbs's reply, as recorded on Wikipedia's overview of the topic, has become one of the earliest and most quoted rebuttals to obscurity-based thinking: \"rogues are very keen in their profession, and know already much more than we can teach them.\" His point was that the people most motivated to find a flaw — the actual attackers — will find it with or without public disclosure; the only question is whether the ",[335,2534,2535],{},"defenders"," find out about it in time to fix it too.",[149,2538,2539,2540,2543,2544,2547],{},"The term \"security through obscurity\" itself has a murkier, more folkloric origin. One story traces it to MIT's Incompatible Timesharing System (ITS) community in contrast to the neighboring Multics project, where security was taken far more seriously. Within the self-mocking ITS culture, the phrase reportedly described their own system's threadbare documentation and obscure commands — the joke being that by the time an outside \"tourist\" figured out how to cause trouble, they usually felt enough a part of the community to lose the urge. It's a nice reminder that the phrase started out as an in-joke about ",[335,2541,2542],{},"accidental"," obscurity, long before it became a formal critique of ",[335,2545,2546],{},"deliberate"," obscurity as a design choice.",[153,2549,2551],{"id":2550},"a-ledger-of-systems-that-tried-it-and-lost","A Ledger of Systems That Tried It and Lost",[149,2553,2554],{},"Abstract arguments are one thing; a track record is another. According to the history compiled on Wikipedia, a long list of telecommunication and digital rights management cryptosystems have relied on keeping their algorithms secret rather than public and peer-reviewed — and have, over time, been broken anyway. That list includes components of the GSM mobile standard, GMR satellite phone encryption, GPRS data encryption, several RFID encryption schemes, and — as recently as 2023 — the encryption used in TETRA, the radio standard relied on by police and emergency services across much of Europe. In every case, the same story played out: the algorithm's secrecy held for years, then researchers reverse-engineered it anyway, and every deployed system built on top of it was suddenly exposed all at once, with no way to quietly swap out a \"key\" the way you'd rotate a compromised password.",[149,2556,2557],{},"Modern anti-malware software is called out as one of the largest remaining strongholds of security-through-obscurity thinking still in everyday use: antivirus vendors keep their detection signatures secret, which sets up a permanent arms race where attackers keep finding novel ways to slip past detection while defenders keep inventing new secret signatures to catch them. It works well enough to be worth doing, but it's never a final answer — it's an ongoing chase, not a solved problem.",[149,2559,2560],{},"More recently, the same instinct showed up in an unexpected place: election infrastructure. In January 2020, NPR reported that Iowa Democratic Party officials refused to disclose security details about the mobile app being used to report caucus results, reasoning that they didn't want to hand attackers a blueprint. Cybersecurity experts weren't convinced, arguing that withholding an app's technical details doesn't meaningfully protect the system it's guarding — a modern echo of the exact argument locksmiths were having in 1851.",[149,2562,2563],{},"Standards bodies have been consistent on this point for a long time. NIST's guidance on general server security explicitly warns that system security should not depend on the secrecy of an implementation or its components, and the MITRE Corporation's Common Weakness Enumeration project formally catalogs the problem as its own entry, CWE-656: \"Reliance on Security Through Obscurity.\"",[149,2565,2566,2567,2570],{},"None of that means obscurity is worthless everywhere. Wikipedia's overview draws a useful distinction: obscurity's effectiveness depends heavily on whether it sits ",[335,2568,2569],{},"on top of"," otherwise sound security practices or is being asked to carry the whole job alone. Used as one layer among several, it's treated as a legitimate security tool rather than a discredited one — and in recent years, more deliberate, engineered versions of the idea have actually gained ground in the security field under names like Moving Target Defense and cyber deception, where systems are intentionally randomized, disguised, or seeded with decoys specifically to waste an attacker's time and resources. That's a far cry from a hardcoded API key split across three JavaScript variables — it's obscurity treated as a real, engineered discipline rather than an afterthought.",[153,2572,2574],{"id":2573},"why-security-people-reach-for-their-pitchforks","Why Security People Reach for Their Pitchforks",[149,2576,2577,2578,2581],{},"Ask why \"security through obscurity is bad\" on any security forum and you'll get a fairly consistent answer, distilled down over decades of hard lessons: hiding the design of a system creates a false sense of safety without addressing the underlying weakness. Security researcher Robert Graham, writing about the cliché, argues that the real point isn't obscurity at all but ",[335,2579,2580],{},"transparency"," — and that transparency is good for security for much the same reason it's good in government, where it enables accountability. Only by exposing a system's design to scrutiny — peer review, penetration testing, public disclosure of flaws — can weaknesses actually get found and fixed, rather than sitting undiscovered until an attacker stumbles onto them.",[149,2583,2584],{},"This is also why the open-source world has historically leaned so hard on \"many eyes make bugs shallow\" as a security argument, and why cryptographic algorithms like AES aren't secret at all — they're published, standardized, and attacked by thousands of researchers for years before anyone trusts them. The algorithm is public. Only the key is secret. That split is the whole point.",[153,2586,2588],{"id":2587},"obscurity-in-the-wild-a-few-familiar-battlegrounds","Obscurity in the Wild: A Few Familiar Battlegrounds",[149,2590,2591],{},"The debate isn't abstract — it shows up in ordinary sysadmin decisions all the time, and the arguments on both sides are worth taking seriously rather than dismissing.",[149,2593,2594,2597],{},[936,2595,2596],{},"Renaming the Administrator account."," Windows hardening guides have recommended this for years: rename the built-in Administrator account so an attacker can't target a known username directly. A well-known Microsoft TechNet debate on the topic opened by noting that in some circles the term \"security by obscurity\" is treated as nearly a slur among security professionals — yet the same piece went on to give real weight to the argument that renaming the account still eliminates a real, cheap attack vector, even if it isn't a complete defense on its own.",[149,2599,2600,2603],{},[936,2601,2602],{},"Moving SSH off port 22."," This might be the most litigated obscurity debate in operations. Automated bots scan port 22 constantly, so moving SSH to a random high port cuts down that noise dramatically — one systems administrator who made the switch after years on the default port described their authentication logs shrinking from hundreds of daily attempts down to a handful almost overnight. But the same writer was blunt about what that change actually buys you, summing it up in one line: changing your SSH port is obscurity, not real security. A targeted attacker running a full port scan finds the service either way; what changed is the volume of drive-by noise, not the ceiling on what a determined attacker can do.",[149,2605,2606,2609],{},[936,2607,2608],{},"Obfuscated or \"hidden\" API endpoints."," The same pattern shows up constantly in web and mobile development. Developers try to disguise API calls — encoding a payload, scrambling a URL path, hiding a key in minified JavaScript — hoping that if the request doesn't look readable in a browser's Network tab, it's somehow protected. It isn't. Anything a client needs in order to make a request has to exist somewhere the client can read it, which means anyone willing to open developer tools can read it too.",[149,2611,2612,2615],{},[936,2613,2614],{},"DRM and software licensing."," Digital rights management schemes and license-key obfuscation are, almost by definition, security through obscurity applied to code instead of network traffic — and they sit alongside GSM, GPRS, and TETRA in the ledger of secret-algorithm systems that eventually got broken anyway, for exactly the reason Kerckhoffs predicted: the \"secret\" is baked into something the attacker has full, unsupervised access to (their own machine, their own binary), so cracking it is a matter of time and patience, not a fundamental barrier.",[153,2617,2619],{"id":2618},"so-is-obscurity-ever-legitimate","So Is Obscurity Ever Legitimate?",[149,2621,2622],{},"Here's where the conversation usually gets more honest than the \"obscurity bad, always\" soundbite suggests. Most practitioners who've actually run production infrastructure will tell you obscurity isn't worthless — it's just not sufficient, and it needs to know its place in the stack. Identity-security vendor Okta puts this nuance plainly: obscurity works best as a complementary layer used alongside other real security controls, not as a standalone defense.",[149,2624,2625],{},"That distinction — complementary layer versus sole defense — is really the whole debate in one sentence. Changing your SSH port doesn't stop a targeted attacker, but it does cut the ambient noise of automated scanning, freeing up your logs and attention for things that matter. Renaming an admin account doesn't stop a skilled intruder who's already inside your network, but it removes one of the laziest, most automatable first moves available to an opportunistic one. Security commentator Bruce Schneier captured the honest middle ground well: obscurity makes information harder to find or understand, which makes a system safer in a probabilistic sense — but \"safer\" is not the same as \"inaccessible.\" Reduced probability of compromise is a real, measurable benefit. It just isn't the same thing as an actual barrier, and treating it as one is where teams get burned.",[153,2627,2629],{"id":2628},"the-analogies-that-make-it-click","The Analogies That Make It Click",[149,2631,2632],{},"Some of the clearest explanations of this idea don't come from academic papers at all — they come from the kind of plain-language back-and-forth that fills a long-running Stack Overflow thread on the topic. A few of the analogies from that discussion are worth stealing, because they make the abstract argument concrete.",[149,2634,2635,2636,2639,2640,2643],{},"One answer frames it as burying money under a tree versus locking it in a safe. Buried money is only safe as long as nobody knows where it's buried; a safe sitting in plain view on a street corner is still safe, because what protects it isn't its location but the lock. A follow-up comment on that same answer adds the detail that really makes the distinction click: if someone discovers your ",[335,2637,2638],{},"password",", you just change it — a minor inconvenience. If someone discovers where the money is ",[335,2641,2642],{},"buried",", you have to physically dig it up and relocate the entire stash, and if the \"burial spot\" was baked into a piece of software, you'd have to rewrite the program itself. That's the recoverability argument in miniature: a compromised key is cheap to replace, a compromised mechanism is not.",[149,2645,2646],{},"That same thread makes a related point about peer review. One answer argues that the best way to evaluate or improve a security product is to have it examined by a large, skeptical group of outsiders — and that products designed to work as a sealed \"black box\" can never get that benefit, which tends to invite the suspicion (often justified) that they wouldn't hold up to scrutiny anyway. It's the same logic that makes open, published algorithms like AES trustworthy: thousands of qualified people have already tried to break them and failed, in public, over years.",[149,2648,2649,2650,2653,2654,2657],{},"Several answers in the thread also push back on a subtler danger: the assumption baked into most obscurity schemes that ",[335,2651,2652],{},"you're"," clever and ",[335,2655,2656],{},"your users"," — or attackers — are not. One contributor put it bluntly: relying on obscurity tends to assume you're smart and everyone else is not, and unless that assumption is backed by real evidence rather than confidence, both your users and any attackers will eventually work out the hidden method, find the unlinked page, or pull the plaintext password straight out of a compiled binary. Another commenter compared weak, obscurity-based protection to driving without a seatbelt: technically survivable in plenty of ordinary situations, but not something you'd want to rely on the one time it actually matters.",[149,2659,2660],{},"Finally, more than one answer in that thread makes the same observation about scale: obscurity that depends on a secret nobody has bothered to look for tends to collapse instantly, and completely, the moment even one person does look. Once a \"hidden\" backdoor, bug, or exploit is stumbled onto by a single curious person, word travels fast — within a day, it's no longer obscure to anyone.",[149,2662,2663,2664,2667],{},"It's also worth noting the debate isn't entirely one-sided even within that same thread. One reply points out that steganography — hiding a secret message inside an ordinary image or audio file rather than encrypting it outright — is arguably security through obscurity by definition, since the entire point is that an observer can't even tell a hidden message exists. And unlike most obscurity schemes, some research on steganographic detection has produced a genuinely counterintuitive result: under certain conditions, throwing ",[335,2665,2666],{},"more"," detection algorithms at a suspected stego-channel can actually increase the amount of data that can be hidden in it undetected, rather than making concealment harder. It's a reminder that \"hiding that something exists\" and \"hiding how something works\" aren't quite the same category of obscurity, even if they get lumped together in casual conversation.",[153,2669,2671],{"id":2670},"a-concrete-modern-example-obscured-apis-vs-a-properly-verified-jwt","A Concrete Modern Example: Obscured APIs vs. a Properly Verified JWT",[149,2673,2674],{},"It's worth walking through a before-and-after, because this is where the abstract debate turns into a decision most developers actually have to make. Picture a typical \"obscured\" API:",[264,2676,2677,2688,2691],{},[267,2678,2679,2680,2683,2684,2687],{},"The endpoint path looks like ",[192,2681,2682],{},"/api/x7f2q/data"," instead of ",[192,2685,2686],{},"/api/data",", on the theory that a random-looking path is harder to guess.",[267,2689,2690],{},"The request payload is base64-encoded or run through a simple substitution cipher before it's sent, so it doesn't look like readable JSON in a Network tab.",[267,2692,2693],{},"A hardcoded API key sits in the front-end bundle, lightly disguised by splitting the string across a few variables.",[149,2695,2696],{},"None of this stops a determined user. The random path is visible the moment the page fires a request. Base64 decodes in one line of browser console JavaScript. A hardcoded key is sitting in the shipped bundle for anyone to search for. All of it is obscurity: friction for the casual observer, no real barrier for anyone who opens developer tools with intent.",[149,2698,2699],{},"Now compare that to a basic JWT (JSON Web Token) setup done properly:",[264,2701,2702,2705,2712],{},[267,2703,2704],{},"The user authenticates once, and the server issues a signed token containing claims like the user's ID and an expiry time.",[267,2706,2707,2708,2711],{},"The client attaches that token to every subsequent request, typically as an ",[192,2709,2710],{},"Authorization: Bearer \u003Ctoken>"," header.",[267,2713,2714],{},"The server verifies the token's signature on every single request using a secret or key only it holds. If the signature doesn't check out, or the token has expired, the request is rejected — regardless of whether the caller knows the endpoint's real name or anything else about how the API is built.",[149,2716,2717,2718,2720,2721,2723],{},"The endpoint path can be ",[192,2719,2686],{},", fully documented in public API docs, with zero obfuscation, and this version is ",[335,2722,2666],{}," secure than the disguised one above. That's Kerckhoffs' principle applied to a modern stack: the mechanism (how JWTs are issued and verified) can be completely public, because the actual secret — the server's signing key, and each user's ability to obtain a valid token — is the only thing doing real work.",[149,2725,2726],{},"But \"we use JWT\" isn't a magic phrase that ends the conversation either; it's a floor, not a ceiling. A few habits quietly turn a JWT setup back into obscurity dressed up as security:",[264,2728,2729,2738,2744,2750,2760],{},[267,2730,2731,2734,2735,342],{},[936,2732,2733],{},"Decoding a token without verifying it."," Reading claims out of a JWT without checking its signature is no different from trusting a client-supplied cookie that says ",[192,2736,2737],{},"isAdmin: true",[267,2739,2740,2743],{},[936,2741,2742],{},"Accepting a weak or missing signing algorithm."," Some early JWT libraries would accept a token that claimed to have no signature at all, or let the client dictate the algorithm — letting an attacker forge a token by construction.",[267,2745,2746,2749],{},[936,2747,2748],{},"Long-lived tokens with no revocation path."," A token valid for 30 days with no way to invalidate it early turns a single leak into a month-long compromise.",[267,2751,2752,2755,2756,2759],{},[936,2753,2754],{},"Storing the token somewhere obscurity-minded rather than security-minded."," Stashing a JWT in ",[192,2757,2758],{},"localStorage"," because it's \"less obvious\" than a cookie doesn't stop a cross-site scripting bug from reading it straight out.",[267,2761,2762,2765,2766,2769,2770,2772],{},[936,2763,2764],{},"Trusting the signature but skipping authorization."," A valid signature proves the token is genuine; it says nothing about whether ",[335,2767,2768],{},"this"," user should be allowed to hit ",[335,2771,2768],{}," endpoint. That check still has to happen on the server, every time.",[149,2774,2775],{},"Done properly, a JWT does what obscurity only pretends to do: it gives the server a cheap, reliable way to answer \"do I actually trust this request?\" on every call, using a secret the client never sees and can't forge. Done carelessly, it becomes a more official-looking version of the same obscurity trap — a secret-looking string that nobody actually checks.",[153,2777,252],{"id":251},[149,2779,2780],{},"Security through obscurity isn't a myth or a rookie mistake invented by confused developers — it's a genuinely old, genuinely tempting idea with a long track record of buying just enough false confidence to get organizations hurt. It shows up everywhere: renamed admin accounts, relocated SSH ports, obfuscated API payloads, disguised license checks. In every one of these cases, the pattern is the same. Obscurity can reduce noise, discourage casual attackers, and buy a system a bit of breathing room. What it cannot do is replace a real security boundary — a verified signature, an authenticated session, a properly scoped permission check — because the moment the \"hidden\" detail gets out, and it eventually does, a system that depended on it has nothing left standing behind it.",[149,2782,2783],{},"Kerckhoffs made that case in 1883, using locks and ciphers. Every generation since has relearned it with whatever technology happens to be new — Windows accounts, SSH daemons, mobile apps, and now REST APIs and JWTs. The lesson doesn't change: design as if the attacker already knows how your system works, and make sure that knowledge alone still isn't enough to get in.",{"title":293,"searchDepth":294,"depth":295,"links":2785},[2786,2787,2788,2789,2790,2791,2792,2793,2794],{"id":2514,"depth":295,"text":2515},{"id":2528,"depth":295,"text":2529},{"id":2550,"depth":295,"text":2551},{"id":2573,"depth":295,"text":2574},{"id":2587,"depth":295,"text":2588},{"id":2618,"depth":295,"text":2619},{"id":2628,"depth":295,"text":2629},{"id":2670,"depth":295,"text":2671},{"id":251,"depth":295,"text":252},"2026-08-01","From Kerckhoffs' 1883 principle to obscured APIs and JWTs done wrong — where security-through-obscurity comes from, why it fails, and the narrow cases where it still earns a place.",{},"/blog/security-through-obscurity","15 min",{"title":2503,"description":2796},"blog/3.security-through-obscurity","iPIL42b2S4It6yVD1YV_mKzyCQId2VaxWpsQ7onzYMQ",{"id":2804,"title":2805,"body":2806,"date":2960,"description":2961,"extension":311,"featured":35,"meta":2962,"navigation":313,"path":2963,"readTime":459,"seo":2964,"stem":2965,"tag":462,"__hash__":2966},"blog/blog/6.password-security-not-complexity.md","Password Security Was Never About Complexity — It's About Exposure",{"type":146,"value":2807,"toc":2951},[2808,2815,2818,2822,2829,2839,2843,2852,2867,2871,2874,2878,2887,2890,2894,2905,2908,2910,2913,2915,2917],[149,2809,2810,2811,2814],{},"For most of the last two decades, \"secure password\" meant \"complex password.\" Require an uppercase letter, a digit, a symbol, a minimum length, a mandatory reset every 90 days — check every box, and the password was considered strong. That model treated password security as a formatting problem: does this string ",[335,2812,2813],{},"look"," hard to guess?",[149,2816,2817],{},"The trouble is that \"looks hard to guess\" and \"is safe to use\" are two completely different properties, and attackers have never cared much about the first one.",[153,2819,2821],{"id":2820},"attackers-arent-attacking-your-password-policy","Attackers Aren't Attacking Your Password Policy",[149,2823,2824,2825,2828],{},"Complexity rules assume the threat is a human or a brute-force script trying combinations one at a time. That's not how most credential attacks work anymore. Modern attacks lean on credential stuffing, leaked password databases, password reuse across services, and phishing — methods that don't care how a password is ",[335,2826,2827],{},"shaped",", only whether it's already sitting in a dataset the attacker has access to. An attacker running a credential-stuffing campaign isn't trying every possible eight-character string with a symbol in it. They're trying the millions of real passwords that showed up in the last decade of public breaches, because those are astronomically more likely to work than anything generated at random.",[149,2830,2831,2832,2835,2836,2838],{},"This is the gap complexity rules never closed. A password like ",[192,2833,2834],{},"P@ssw0rd1!"," satisfies nearly every policy ever written — upper, lower, digit, symbol, ten characters. It's also one of the most common passwords in breach databases, appearing hundreds of thousands of times across known credential dumps. A policy that only checks ",[335,2837,2196],{}," will happily accept it, right up until it's used against a login form by someone who already has it on a list.",[153,2840,2842],{"id":2841},"the-standard-already-changed-most-policies-havent-caught-up","The Standard Already Changed — Most Policies Haven't Caught Up",[149,2844,2845,2846,2851],{},"This isn't a fringe opinion. ",[197,2847,2850],{"href":2848,"rel":2849},"https://pages.nist.gov/800-63-3/sp800-63b.html",[201],"NIST's Digital Identity Guidelines (SP 800-63B)"," have recommended checking new passwords against known-compromise lists since 2017, and explicitly de-emphasized mandatory character-class mixing as the primary defense. The guidance is direct: verifiers should compare a chosen password against a list of values known to be commonly used, expected, or already compromised — not just verify it contains a symbol.",[149,2853,2854,2855,2860,2861,2866],{},"The database that makes this practical exists and is widely used. ",[197,2856,2859],{"href":2857,"rel":2858},"https://haveibeenpwned.com/Passwords",[201],"Have I Been Pwned (HIBP)",", maintained by security researcher Troy Hunt, holds a searchable index of more than 10 billion real-world passwords harvested from actual data breaches. A recent implementation guide from ",[197,2862,2865],{"href":2863,"rel":2864},"https://duendesoftware.com/blog/20260722-stop-accepting-breached-passwords-hibp-duende-usermanagement",[201],"Duende Software"," walks through wiring HIBP checks into a .NET identity system's password validation pipeline — rejecting a password not because it fails a character-class rule, but because it's already known to attackers. The API is queried using k-anonymity: only the first five characters of the password's SHA-1 hash are ever sent over the network, so the full password (or even its full hash) never leaves the client's side of the check. It's a clean illustration of the underlying shift: the meaningful question isn't \"does this password look secure,\" it's \"is this password already known to attackers\" — and that question has a real, checkable answer, at registration time, before the account exists.",[153,2868,2870],{"id":2869},"security-questions-and-complexity-rules-fail-for-the-same-reason","Security Questions and Complexity Rules Fail for the Same Reason",[149,2872,2873],{},"It's worth naming the pattern, because it shows up more than once. Security questions assumed personal information stays private. Complexity rules assumed a more complicated-looking string is automatically a safer one. Both assumptions were reasonable when they were written, and both quietly stopped being true as data availability and attacker tooling changed around them. Neither failure was really about implementation quality — a security-question feature can be beautifully engineered and still insecure, and a password policy can enforce its rules flawlessly while doing nothing to stop the one attack method that actually gets used against it. Security controls that were designed around a threat model from ten years ago don't become dangerous by getting worse. They become dangerous by staying exactly the same while the threat model moves.",[153,2875,2877],{"id":2876},"what-actually-reduces-risk","What Actually Reduces Risk",[149,2879,2880,2881,2886],{},"The alternative isn't \"no rules\" — it's rules aimed at the right target: length over artificial complexity, uniqueness across services, password managers to make uniqueness actually achievable, breached-password screening at the point of registration, multi-factor authentication as a backstop, and a longer-term push toward phishing-resistant methods that don't depend on a shared secret at all. A long, memorable passphrase that's never been used anywhere else is a stronger credential than a short, \"complex\" one that satisfies every character rule and appears in a breach dump — a pattern the ",[197,2882,2885],{"href":2883,"rel":2884},"https://xkcd.com/936/",[201],"xkcd \"correcthorsebatterystaple\" comic"," has been making the case for since 2011, well before most standards bodies caught up to it.",[149,2888,2889],{},"There's also a behavioral cost to getting this wrong that pure format-checking ignores entirely. Excessive complexity requirements and forced rotation schedules push real users toward exactly the behaviors that undermine security: predictable variations on an old password, passwords written down somewhere insecure, or one password reused across a dozen accounts because remembering fifteen \"compliant\" ones isn't realistic. A policy that creates that behavior isn't neutral — it's actively manufacturing the weakness it was supposed to prevent.",[153,2891,2893],{"id":2892},"ask-what-attack-youre-actually-preventing","Ask What Attack You're Actually Preventing",[149,2895,2896,2897,2900,2901,2904],{},"The right way to evaluate a password control isn't \"does it look strict enough.\" It's a threat-model question: what attack does this actually stop, how do real attackers operate today, and does this control meaningfully reduce that risk or just add friction that pushes users toward worse habits? A password policy that blocks ",[192,2898,2899],{},"password123"," because it lacks a symbol, while happily accepting ",[192,2902,2903],{},"Str0ng&Compromised!"," because it technically passes every rule, is optimizing for the wrong thing. A policy that rejects both — the second one specifically because it's sitting in a breach database, not because of how it's formatted — is solving the problem that actually exists.",[149,2906,2907],{},"Like security questions, this is a decision that benefits from being made before implementation rather than patched afterward. Product teams should be asking whether a given password requirement still reflects current guidance; engineering teams should be asking whether a given control addresses a real, current attack pattern or just an inherited assumption nobody's revisited. The best version of this fix often isn't a smarter regex — it's swapping \"enforce four character classes\" for \"check against a breach list,\" which is a product and policy decision as much as a technical one.",[153,2909,252],{"id":251},[149,2911,2912],{},"A password can be long, complex, and fully compliant with policy, and still be worthless the moment it's already sitting in an attacker's dataset. The purpose of a password control was never to make users satisfy a validator — it was to prevent real accounts from being taken over by real attacks, and those are measurably different goals. The strongest password policy isn't the one with the most rules. It's the one built around how credentials are actually stolen and reused, which increasingly means checking exposure, not counting character classes.",[257,2914],{},[153,2916,262],{"id":261},[264,2918,2919,2927,2935,2943],{},[267,2920,883,2921,2926],{},[197,2922,2924,2458],{"href":2848,"rel":2923},[201],[335,2925,2457],{}," — the standard recommending breach-list screening over mandatory character-class complexity.",[267,2928,2929,2930,2934],{},"Troy Hunt, ",[197,2931,2933],{"href":2857,"rel":2932},[201],"Have I Been Pwned: Pwned Passwords"," — the public database of breach-exposed passwords used for exactly this kind of screening.",[267,2936,2937,2938,2942],{},"Duende Software, ",[197,2939,2941],{"href":2863,"rel":2940},[201],"\"Stop Accepting Breached Passwords: Integrating HaveIBeenPwned with Duende UserManagement\""," — a concrete implementation walkthrough of breach-based password validation using k-anonymity.",[267,2944,2945,2946,2950],{},"Randall Munroe, ",[197,2947,2949],{"href":2883,"rel":2948},[201],"\"Password Strength\"",", xkcd — the widely referenced comic making the case for long passphrases over artificial complexity.",{"title":293,"searchDepth":294,"depth":295,"links":2952},[2953,2954,2955,2956,2957,2958,2959],{"id":2820,"depth":295,"text":2821},{"id":2841,"depth":295,"text":2842},{"id":2869,"depth":295,"text":2870},{"id":2876,"depth":295,"text":2877},{"id":2892,"depth":295,"text":2893},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-07-29","Complexity rules check whether a password looks hard to guess. Attackers running credential stuffing don't care how a password is shaped — only whether it's already sitting in a breach dataset.",{},"/blog/password-security-not-complexity",{"title":2805,"description":2961},"blog/6.password-security-not-complexity","FKUwML26z77Jrcx76Nw_ltnf5NGWpM7pYTH6f2WilHg",{"id":2968,"title":2969,"body":2970,"date":3119,"description":3120,"extension":311,"featured":35,"meta":3121,"navigation":313,"path":3122,"readTime":624,"seo":3123,"stem":3124,"tag":462,"__hash__":3125},"blog/blog/5.rsa-fips-fapi-myth.md","RSA Isn't Banned by FIPS or FAPI — But the Way Most People Use It Basically Is",{"type":146,"value":2971,"toc":3111},[2972,2979,2983,2991,2994,2998,3022,3029,3033,3036,3045,3048,3052,3055,3062,3064,3067,3069,3071],[149,2973,2974,2975,2978],{},"There's a common claim floating around security discussions that RSA has been dropped from FIPS and FAPI. It's a reasonable thing to believe, given how much scrutiny RSA's padding schemes have gotten lately — but it's not quite accurate, and the actual story is more useful than the myth. RSA is still an approved algorithm in both standards. What changed is ",[335,2976,2977],{},"which version"," of RSA is allowed, and that distinction matters more than the headline claim does.",[153,2980,2982],{"id":2981},"fips-still-approves-rsa-it-just-retired-its-weaker-cousins","FIPS Still Approves RSA — It Just Retired Its Weaker Cousins",[149,2984,2985,2990],{},[197,2986,2989],{"href":2987,"rel":2988},"https://csrc.nist.gov/pubs/fips/186-5/final",[201],"FIPS 186-5",", NIST's Digital Signature Standard published in February 2023, specifies exactly three approved techniques for generating and verifying digital signatures: RSA, ECDSA (elliptic curve), and EdDSA (Edwards curve). RSA is right there, first on the list — it was never removed.",[149,2992,2993],{},"What actually happened in the 186-5 revision is narrower. DSA, the original NIST-designed signature algorithm, was dropped from approval for new signature generation — retained only to verify old signatures already created with it — due to a combination of low industry adoption and academic findings about vulnerabilities when domain parameters aren't generated correctly. Separately, the older X9.31 RSA signature scheme was removed, and the standard now permits larger RSA key sizes than before. RSA itself came through the revision essentially intact; it was the outdated padding conventions and the unrelated DSA algorithm that didn't.",[153,2995,2997],{"id":2996},"fapis-restriction-isnt-on-rsa-its-on-one-specific-rsa-padding-scheme","FAPI's Restriction Isn't on RSA — It's on One Specific RSA Padding Scheme",[149,2999,3000,3001,3006,3007,3010,3011,3014,3015,3018,3019,342],{},"This is where the confusion really originates, and it's worth being precise about it. The ",[197,3002,3005],{"href":3003,"rel":3004},"https://openid.net/specs/fapi-2_0-security-02.html",[201],"FAPI 2.0 Security Profile"," requires that authorization servers, clients, and resource servers use PS256, ES256, or EdDSA when creating or processing JWTs, and explicitly prohibits the ",[192,3008,3009],{},"none"," algorithm. FAPI 1.0's Advanced profile said something even more pointed: implementations ",[335,3012,3013],{},"shall"," use PS256 or ES256, and ",[335,3016,3017],{},"should not"," use algorithms built on RSASSA-PKCS1-v1_5 — the family that includes the extremely common ",[192,3020,3021],{},"RS256",[149,3023,3024,3025,3028],{},"Here's the detail that gets lost: ",[936,3026,3027],{},"PS256 is RSA."," It stands for RSASSA-PSS using SHA-256 — a signature scheme built on the exact same RSA math as RS256. FAPI isn't asking implementers to abandon RSA. It's asking them to abandon one specific padding scheme (PKCS#1 v1.5) in favor of a newer one (PSS) built on the same underlying algorithm, alongside allowing elliptic-curve alternatives for anyone who wants to move off RSA entirely for other reasons like key size and performance.",[153,3030,3032],{"id":3031},"why-pkcs1-v15-specifically-fell-out-of-favor","Why PKCS#1 v1.5 Specifically Fell Out of Favor",[149,3034,3035],{},"The distinction between RS256 and PS256 comes down to how the padding is constructed before the RSA operation runs, and it's not a cosmetic difference.",[149,3037,3038,3039,3044],{},"RSASSA-PKCS1-v1.5, the scheme behind RS256, is deterministic — the same message and key always produce the same signature — and it lacks a formal security proof under standard cryptographic assumptions. In practice, that gap between \"widely deployed\" and \"provably secure\" showed up as real vulnerabilities: a string of implementation bugs across multiple cryptography libraries over the years stemmed from verifiers that checked PKCS#1 v1.5 padding too permissively, allowing crafted signatures to be accepted as valid without the attacker ever having the private key. Best-practice guidance for JSON Web Tokens specifically — ",[197,3040,3043],{"href":3041,"rel":3042},"https://www.rfc-editor.org/rfc/rfc8725",[201],"RFC 8725, \"JSON Web Token Best Current Practices\""," — which FAPI 2.0 explicitly requires adherence to, reflects this history by steering implementers toward algorithms with better security properties and away from legacy defaults chosen more for compatibility than for security margin.",[149,3046,3047],{},"RSASSA-PSS, the scheme behind PS256, is randomized — it incorporates a salt, so the same message signed twice produces different signatures — and it comes with a security proof in the random oracle model. That's a meaningfully stronger guarantee, and it's the reason FAPI's high-assurance, financial-grade context specifically calls it out rather than treating RS256 and PS256 as interchangeable.",[153,3049,3051],{"id":3050},"the-broader-pattern-algorithm-agility-not-algorithm-abandonment","The Broader Pattern: Algorithm Agility, Not Algorithm Abandonment",[149,3053,3054],{},"Both standards are really doing the same thing, just at different layers. FIPS 186-5 kept RSA but tightened the specific construction allowed and made room for elliptic-curve alternatives as a modern option. FAPI kept RSA-based signing available through PS256 but closed the door on the specific padding legacy that kept causing implementation bugs, while giving implementers ES256 and EdDSA as equally acceptable paths that don't involve RSA's larger keys and slower operations at all.",[149,3056,3057,3058,3061],{},"This is a pattern worth recognizing beyond these two standards specifically: modern cryptographic guidance rarely retires an algorithm family wholesale. It retires the ",[335,3059,3060],{},"specific, older construction"," of that algorithm once enough real-world weaknesses accumulate against it, while leaving a well-specified, better-analyzed version of the same underlying math in place. RSA the algorithm is fine. RSA-with-1990s-padding, applied without the scrutiny modern standards now require, is what's actually being phased out — and treating those as the same thing is exactly the kind of imprecision that leads engineering teams to either panic-migrate away from RSA entirely when they didn't need to, or worse, assume they're already compliant with PS256-based specs while still shipping RS256.",[153,3063,252],{"id":251},[149,3065,3066],{},"If your team is implementing FAPI or targeting FIPS compliance, the actionable question isn't \"should we stop using RSA.\" For most systems the answer is closer to: keep RSA if you want to, but make sure you're specifying RSASSA-PSS (PS256), not RSASSA-PKCS1-v1.5 (RS256), and check whether your libraries default to the older scheme without telling you — a surprising number still do, because RS256 has been the path-of-least-resistance default in JWT tooling for years. RSA was never asked to leave the room. It was asked to update how it signs things, and the standards are just being explicit about which version they'll actually accept.",[257,3068],{},[153,3070,262],{"id":261},[264,3072,3073,3080,3087,3094,3102],{},[267,3074,883,3075,3079],{},[197,3076,3078],{"href":2987,"rel":3077},[201],"FIPS 186-5: Digital Signature Standard (DSS)"," (2023) — the current federal standard, confirming RSA, ECDSA, and EdDSA as the three approved signature algorithms.",[267,3081,3082,3083,3086],{},"OpenID Foundation, ",[197,3084,3005],{"href":3003,"rel":3085},[201]," — the specification requiring PS256, ES256, or EdDSA for JWT signing.",[267,3088,588,3089,3093],{},[197,3090,3092],{"href":3041,"rel":3091},[201],"RFC 8725: JSON Web Token Best Current Practices"," — the JWT security guidance FAPI 2.0 requires adherence to.",[267,3095,754,3096,3101],{},[197,3097,3100],{"href":3098,"rel":3099},"https://www.authlete.com/kb/financial-grade-api/general-information/how-to-use-fapi/",[201],"\"How to Use FAPI\""," — a practical summary of FAPI 1.0 Advanced's explicit guidance against RSASSA-PKCS1-v1.5 algorithms like RS256.",[267,3103,3104,3105,3110],{},"Corsec, ",[197,3106,3109],{"href":3107,"rel":3108},"https://www.corsec.com/fips-186-5/",[201],"\"FIPS 186-5 Digital Signature Standard Compliance\""," — an overview of what changed between FIPS 186-4 and 186-5, including the removal of DSA and the X9.31 RSA scheme.",{"title":293,"searchDepth":294,"depth":295,"links":3112},[3113,3114,3115,3116,3117,3118],{"id":2981,"depth":295,"text":2982},{"id":2996,"depth":295,"text":2997},{"id":3031,"depth":295,"text":3032},{"id":3050,"depth":295,"text":3051},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-07-25","RSA was never dropped from FIPS or FAPI. What changed is which padding scheme is allowed, and the gap between RS256 and PS256 is exactly where the myth comes from.",{},"/blog/rsa-fips-fapi-myth",{"title":2969,"description":3120},"blog/5.rsa-fips-fapi-myth","xm9J_egzqNeOrwidC-VYN8eZVIWAbt2tJx0nBtq2imk",{"id":3127,"title":3128,"body":3129,"date":3253,"description":3254,"extension":311,"featured":35,"meta":3255,"navigation":313,"path":3256,"readTime":624,"seo":3257,"stem":3258,"tag":627,"__hash__":3259},"blog/blog/4.ciba-vs-authorization-code-banking.md","CIBA and the Case for Matching Auth Flows to Threat Models, Not Popularity",{"type":146,"value":3130,"toc":3245},[3131,3140,3143,3147,3150,3153,3157,3163,3180,3184,3187,3190,3194,3197,3201,3204,3207,3209,3211],[149,3132,3133,3134,3139],{},"For native mobile apps, OAuth 2.0 Authorization Code Flow with PKCE has been the default answer for so long that it's easy to stop asking whether it's actually the right answer for a given app. It usually is. ",[197,3135,3138],{"href":3136,"rel":3137},"https://www.rfc-editor.org/rfc/rfc8252.html",[201],"RFC 8252",", the IETF's best-current-practice document for native apps, is explicit that public clients must implement PKCE and should authenticate through the system browser rather than an embedded web view — and for the vast majority of consumer apps, that combination remains the correct, well-understood choice.",[149,3141,3142],{},"But a banking app isn't the vast majority of consumer apps, and it's worth asking what changes when the thing being protected is a customer's money rather than their shopping cart.",[153,3144,3146],{"id":3145},"authorization-code-flow-solves-a-specific-problem-extremely-well","Authorization Code Flow Solves a Specific Problem Extremely Well",[149,3148,3149],{},"It's worth being precise about what Authorization Code Flow with PKCE was actually designed to do, because the design tells you where it fits. PKCE exists to solve one specific vulnerability: a native app is a public client that can't hold a secret safely, so if an authorization code gets intercepted — by a malicious app registered to the same URI scheme, for instance — that code alone shouldn't be redeemable for tokens. PKCE closes that gap by requiring the client to prove it initiated the original request before the code can be exchanged. RFC 8252 also steers native apps toward the system browser specifically, because it gives the app a separate security boundary from the browser's cookie jar and page content — a native app shouldn't be able to read or inject into the login page it's redirecting the user to.",[149,3151,3152],{},"That's an excellent model when the client is a third-party consumer of an identity provider's authentication — social login, a shopping app authenticating through Google or Apple, any scenario where the app and the identity provider are, in some sense, separate parties negotiating trust through a browser handshake.",[153,3154,3156],{"id":3155},"banking-apps-arent-really-in-that-relationship","Banking Apps Aren't Really in That Relationship",[149,3158,3159,3160,3162],{},"A bank's own mobile app isn't a third party asking a stranger's identity provider to vouch for a user. The bank ",[335,3161,1174],{}," the identity provider. It already has a relationship with the device, the customer, and frequently a risk engine that's been scoring this exact login attempt before the authentication request even starts. Routing that interaction through a full browser redirect — with all the phishing surface, redirect handling, and session management a browser introduces — solves a trust problem the bank and its own app don't actually have with each other.",[149,3164,3165,3166,3171,3172,3175,3176,3179],{},"This is where ",[197,3167,3170],{"href":3168,"rel":3169},"https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html",[201],"Client-Initiated Backchannel Authentication (CIBA)",", an OpenID Foundation specification finalized in 2021, becomes worth a serious look. CIBA separates the ",[335,3173,3174],{},"consumption device"," — where the user is interacting with the client — from the ",[335,3177,3178],{},"authentication device",", where the user actually authenticates and grants consent, and lets the client talk to the identity provider directly over a backchannel rather than bouncing the user through a browser redirect. In practice, that's the flow behind \"we sent a push notification to your registered device — approve the login there\" patterns that already feel native to how mobile banking apps work today.",[153,3181,3183],{"id":3182},"where-that-distinction-actually-matters","Where That Distinction Actually Matters",[149,3185,3186],{},"The appeal isn't that CIBA is inherently more secure in the abstract — it's that it removes a step (the browser redirect) that was solving a problem the bank's own first-party app doesn't have, while adding a step (authenticate-and-approve on a known, registered device) that maps much more directly onto what banking security actually needs to verify: not just \"does this person know a password,\" but \"is this the customer's own trusted device, approving this specific action, right now.\"",[149,3188,3189],{},"That last part matters more in banking than almost anywhere else, because banking isn't only about login. It's about authorizing specific, consequential actions — a wire transfer, a new payee, a change to account limits — and a push-based backchannel approval model generalizes naturally to \"approve this $4,000 transfer\" in a way that a generic login redirect doesn't. The same underlying mechanism that authenticates a session can be reused to get explicit, user-present approval for the transaction that actually matters.",[153,3191,3193],{"id":3192},"ciba-isnt-a-security-upgrade-by-itself","CIBA Isn't a Security Upgrade by Itself",[149,3195,3196],{},"None of this makes CIBA automatically safer than Authorization Code Flow — it removes one attack surface (browser redirects) while introducing requirements of its own: strong device binding so the \"authentication device\" really is the customer's device, robust protection against social-engineering attacks that trick a user into approving a push notification they didn't initiate, and the same baseline of secure key management and token handling any OAuth flow needs regardless of which grant type it uses. A poorly implemented CIBA deployment — weak binding, sloppy consent screens, no fraud signal feeding the approval decision — can be just as exploitable as a poorly implemented redirect flow. The flow is a tool, not a guarantee.",[153,3198,3200],{"id":3199},"the-actual-decision-isnt-which-protocol-is-better","The Actual Decision Isn't \"Which Protocol Is Better\"",[149,3202,3203],{},"The useful question was never \"is CIBA better than Authorization Code Flow.\" It's \"what relationship does this client actually have with this identity provider, and what does that relationship's threat model require.\" A third-party app requesting delegated access to a user's data through an external identity provider is exactly the scenario Authorization Code Flow with PKCE was built for, and it remains an excellent, mature, broadly supported answer there. A bank's own first-party app, authenticating its own known customer on an already-registered device, and increasingly needing to authorize specific high-value transactions rather than just establish a session, is a meaningfully different relationship — and CIBA was built with exactly that shape of problem in mind.",[149,3205,3206],{},"Picking a flow because it's the most standardized option, without asking which relationship it was designed for, is how teams end up bolting transaction-approval requirements onto a login flow that was never meant to carry them. The stronger habit is starting from the threat model — who owns the client, who owns the identity, what's actually being authorized — and letting that answer determine the protocol, rather than the other way around.",[257,3208],{},[153,3210,262],{"id":261},[264,3212,3213,3220,3228,3236],{},[267,3214,3082,3215,3219],{},[197,3216,3218],{"href":3168,"rel":3217},[201],"OpenID Connect Client-Initiated Backchannel Authentication Flow — Core 1.0"," — the finalized CIBA specification.",[267,3221,3222,3223,3227],{},"W. Denniss and J. Bradley, ",[197,3224,3226],{"href":3136,"rel":3225},[201],"RFC 8252: OAuth 2.0 for Native Apps"," (IETF, 2017) — the best-current-practice standard requiring PKCE and browser-based authorization for native app clients.",[267,3229,588,3230,3235],{},[197,3231,3234],{"href":3232,"rel":3233},"https://datatracker.ietf.org/doc/html/rfc7636",[201],"RFC 7636: Proof Key for Code Exchange (PKCE)"," — the extension that protects native-app authorization codes from interception.",[267,3237,3238,3239,3244],{},"OAuth.net, ",[197,3240,3243],{"href":3241,"rel":3242},"https://oauth.net/2/native-apps/",[201],"OAuth for Native Apps"," — a practical overview of why Authorization Code Flow with PKCE became the native-app standard.",{"title":293,"searchDepth":294,"depth":295,"links":3246},[3247,3248,3249,3250,3251,3252],{"id":3145,"depth":295,"text":3146},{"id":3155,"depth":295,"text":3156},{"id":3182,"depth":295,"text":3183},{"id":3192,"depth":295,"text":3193},{"id":3199,"depth":295,"text":3200},{"id":261,"depth":295,"text":262},"2026-07-22","Authorization Code Flow with PKCE is the default for native apps for good reason — but a bank's own first-party app has a different relationship with its identity provider, and CIBA was built for exactly that shape of problem.",{},"/blog/ciba-vs-authorization-code-banking",{"title":3128,"description":3254},"blog/4.ciba-vs-authorization-code-banking","kM3ZS4sU5EWT5-N_fwdVjpzii4qV4_UZvS8wlNZ0PSI",{"id":3261,"title":3262,"body":3263,"date":3619,"description":3620,"extension":311,"featured":35,"meta":3621,"navigation":313,"path":3622,"readTime":3623,"seo":3624,"stem":3625,"tag":129,"__hash__":3626},"blog/blog/2.clean-architecture-best-practice-or-creativity-killer.md","Clean Architecture or Not? Why the Real Question Is Autonomy and Pragmatism",{"type":146,"value":3264,"toc":3608},[3265,3268,3282,3286,3292,3299,3302,3306,3309,3312,3338,3341,3345,3352,3358,3364,3382,3393,3397,3406,3415,3419,3436,3439,3442,3446,3449,3452,3466,3469,3472,3483,3486,3497,3504,3508,3511,3526,3528,3531,3534,3537,3539,3541],[149,3266,3267],{},"Few ideas in modern software design have been as widely adopted — and as widely argued over — as Clean Architecture. Popularized by Robert C. Martin (\"Uncle Bob\") in a 2012 blog post and later expanded into a full book, it gave a name and a diagram to something many senior engineers were already doing instinctively: keeping business logic independent of frameworks, databases, and UI technology, so that the parts of a system most likely to change don't drag the parts that matter most down with them.",[149,3269,3270,3271,3274,3275,3278,3279,342],{},"More than a decade later, the community is split, but the split usually hides a more useful question underneath it. \"Should I use Clean Architecture?\" isn't really a yes-or-no question about a specific pattern — it's a test of whether an engineering team has the ",[936,3272,3273],{},"autonomy"," to make that call themselves and the ",[936,3276,3277],{},"pragmatism"," to base it on the project's actual goals, rather than inheriting the decision from a template, a tutorial, or an org-wide mandate. Teams that lose either half of that — the freedom to decide, or the discipline to decide honestly — tend to land in one of two ditches: rigid over-engineering on one side, or drift into an unstructured mess on the other. Skip the decision entirely, and the outcome usually isn't \"no architecture\" at all — it's almost always the same well-documented result: a system that gradually turns into a ",[936,3280,3281],{},"Big Ball of Mud",[153,3283,3285],{"id":3284},"the-real-alternative-to-deciding-is-mud-not-freedom","The Real Alternative to Deciding Is Mud, Not Freedom",[149,3287,3288,3289,3291],{},"It's worth naming the risk on the other side of this debate plainly, because it rarely gets the attention the over-engineering critique does. In their well-known 1997 paper, software researchers Brian Foote and Joseph Yoder gave a name to what they called the most common architecture actually deployed in practice: the ",[936,3290,3281],{}," — a system whose structure, if it has any at all, was shaped by expediency rather than design. It isn't a lack of pattern so much as the default pattern that emerges whenever nobody makes an explicit architectural decision and sticks to it. Information leaks promiscuously between distant parts of the system, boundaries blur, and eventually nobody can say with confidence where a given piece of logic is actually supposed to live.",[149,3293,3294,3295,3298],{},"This is the context that makes \"just don't do Clean Architecture\" bad generic advice. Engineers who avoid Clean Architecture because it feels heavy don't automatically land somewhere lighter and cleaner instead — far more often, in the absence of ",[335,3296,3297],{},"any"," deliberate structural decision, they land in mud, because mud is what happens by default under shipping pressure, not something a team chooses on purpose. The failure mode most teams actually hit isn't \"too much architecture.\" It's \"no decision about architecture at all,\" repeated across a hundred small choices until the system can't be reasoned about anymore. That drift is what a lack of pragmatism looks like in practice — not choosing the wrong pattern, but never actually choosing.",[149,3300,3301],{},"That's the case for giving engineering teams real autonomy to make a bold, explicit call early — Clean Architecture, something lighter, or something else entirely — and then holding them to using that autonomy pragmatically, rather than letting the structure of a codebase get decided implicitly, one shortcut at a time.",[153,3303,3305],{"id":3304},"what-the-idea-actually-solves","What the Idea Actually Solves",[149,3307,3308],{},"At its core, Clean Architecture is built around one rule: dependencies should point inward, toward the business logic, never outward toward infrastructure. Your domain model shouldn't know or care whether it's backed by SQL Server, PostgreSQL, or a flat file. Your use cases shouldn't know or care whether they're triggered by a REST controller, a message queue, or a console command.",[149,3310,3311],{},"This buys real things:",[264,3313,3314,3320,3326,3332],{},[267,3315,3316,3319],{},[936,3317,3318],{},"Separation of concerns."," Business rules stop leaking into controllers, ORMs, and UI code, and vice versa.",[267,3321,3322,3325],{},[936,3323,3324],{},"Testability."," Because the domain doesn't depend on a database or an HTTP server, you can unit test business behavior with plain objects and fakes, not integration harnesses.",[267,3327,3328,3331],{},[936,3329,3330],{},"Maintainability over time."," Systems tend to live far longer than their original authors expect. Clear boundaries make it easier for developers who join years later to figure out where a given piece of logic actually belongs.",[267,3333,3334,3337],{},[936,3335,3336],{},"Framework independence."," Frameworks and libraries become swappable implementation details sitting behind interfaces, rather than the load-bearing foundation the whole codebase is built on top of.",[149,3339,3340],{},"None of this is controversial in the abstract. Almost every architectural style that predates or parallels Clean Architecture — Alistair Cockburn's Hexagonal (Ports & Adapters) Architecture, Onion Architecture, and others — is reaching for the same underlying goal by a different route: isolate the stuff that's expensive to change from the stuff that changes constantly.",[153,3342,3344],{"id":3343},"where-the-criticism-actually-comes-from","Where the Criticism Actually Comes From",[149,3346,3347,3348,3351],{},"It's worth being precise here, because most of the pushback isn't really aimed at the principles themselves. Almost nobody seriously argues that business logic ",[335,3349,3350],{},"should"," be tightly coupled to a specific ORM. The criticism is aimed at how the principles get applied in practice — and three patterns come up again and again.",[149,3353,3354,3357],{},[936,3355,3356],{},"Architecture before problems exist."," Plenty of projects start with a Repository, a Generic Repository, a Unit of Work, CQRS, a mediator library, an auto-mapper, validators, DTOs, and interfaces for all of it — before a single business requirement has actually been implemented. Days get spent scaffolding structure instead of delivering value, on the assumption that the structure will pay for itself later. Sometimes it does. Often the project never grows into the complexity the scaffolding was built for.",[149,3359,3360,3363],{},[936,3361,3362],{},"Over-abstraction for its own sake."," A single simple feature can end up spread across eight or nine files — a command, a handler, a repository interface, a repository implementation, a validator, a DTO, a mapping profile, a response object — for logic that would have fit comfortably inside one method. The code becomes mechanically correct and cognitively expensive at the same time: technically clean, practically hard to hold in your head.",[149,3365,3366,3369,3370,3373,3374,3377,3378,3381],{},[936,3367,3368],{},"Interfaces that exist to satisfy a rule, not a need."," Creating ",[192,3371,3372],{},"IUserService"," next to ",[192,3375,3376],{},"UserService"," when there is exactly one implementation and no realistic prospect of a second isn't abstraction — it's ceremony. Interfaces are supposed to abstract ",[335,3379,3380],{},"behavior"," that genuinely varies. Wrapping Entity Framework, which is already an abstraction over raw data access, in another generic repository layer that does nothing but forward the same call downward adds a layer without adding value.",[149,3383,3384,3385,3388,3389,3392],{},"This is really where the \"creativity killer\" complaint comes from, and it's more precisely a complaint about lost autonomy than lost creativity. The concern isn't that patterns exist — it's that they become mandatory. Instead of asking ",[335,3386,3387],{},"what's the simplest solution to this specific problem",", teams start asking ",[335,3390,3391],{},"what does the architecture template say I should do here",", and the framework starts making decisions that should belong to the engineer looking at the actual problem in front of them. An engineer who can't deviate from a prescribed structure when the situation calls for it isn't practicing architecture — they're following a checklist someone else wrote for a different project.",[153,3394,3396],{"id":3395},"cargo-cult-architecture","Cargo Cult Architecture",[149,3398,3399,3400,3405],{},"There's a well-known term for imitating a successful system's outward form without understanding why it worked: ",[197,3401,3404],{"href":3402,"rel":3403},"https://en.wikipedia.org/wiki/Cargo_cult_programming",[201],"cargo cult programming",". It's not hard to see the pattern repeat inside Clean Architecture codebases — CQRS added because a well-known template uses it, a mediator library wired in because every tutorial does, repositories generated because the project scaffold includes them by default, none of it tied back to an actual requirement the project has. The architecture stops being a tool for reasoning about a specific system and starts being a ritual performed because it's what \"good\" projects are supposed to look like.",[149,3407,3408,3409,3414],{},"Martin Fowler's long-standing writing on ",[197,3410,3413],{"href":3411,"rel":3412},"https://martinfowler.com/bliki/Yagni.html",[201],"YAGNI"," — \"You Aren't Gonna Need It\" — is a useful counterweight here, even though it predates Clean Architecture's popularity by years. The core idea is the same one that shows up in the more thoughtful critiques of Clean Architecture: build the abstraction when a real, current requirement demands it, not because a future requirement might.",[153,3416,3418],{"id":3417},"a-real-alternative-not-just-a-complaint-vertical-slice-architecture-vsa","A Real Alternative, Not Just a Complaint: Vertical Slice Architecture (VSA)",[149,3420,3421,3422,3427,3428,3431,3432,3435],{},"The most useful pushback on Clean Architecture doesn't stop at criticism — it offers a concrete alternative. In 2018, developer Jimmy Bogard published ",[197,3423,3426],{"href":3424,"rel":3425},"https://www.jimmybogard.com/vertical-slice-architecture/",[201],"\"Vertical Slice Architecture\"",", arguing that traditional layered approaches force nearly every request through the same rigid path — controller talks to service, service talks to repository — even when a given feature doesn't actually need that much ceremony. His alternative organizes code around individual features or use cases instead of technical layers: each \"slice\" contains everything needed to handle one request, end to end, with the explicit goal to minimize coupling ",[335,3429,3430],{},"between"," slices while accepting more coupling ",[335,3433,3434],{},"within"," one.",[149,3437,3438],{},"VSA isn't a rejection of structure — it's a different bet about where the structure should live. Clean Architecture optimizes for consistency across the whole system: every feature goes through the same layers, which makes the codebase predictable but can force simple features through unnecessary machinery. Vertical Slice Architecture optimizes for isolation between features: a simple CRUD endpoint can stay simple, while a genuinely complex use case can carry as much internal structure as it actually needs, without that complexity leaking into features that don't share it. The trade-off shows up on the other side too — critics of VSA point out that without shared layers, related slices can end up duplicating logic that a layered architecture would have naturally centralized.",[149,3440,3441],{},"Neither approach is objectively better. They're different answers to the same underlying design question — where do you want your coupling to live, and what are you optimizing for — and the right choice depends entirely on the shape of the actual problem, not on which pattern is trending in blog posts this year.",[153,3443,3445],{"id":3444},"autonomy-means-someone-has-to-decide-pragmatism-means-deciding-honestly","Autonomy Means Someone Has to Decide — Pragmatism Means Deciding Honestly",[149,3447,3448],{},"This is ultimately a trade-off question, not a right-or-wrong one — and it's exactly the kind of question that belongs with the engineering team closest to the problem, not a company-wide architecture mandate handed down in advance of ever seeing the project. Autonomy without pragmatism just produces a different flavor of dogma — a team free to choose whatever they personally prefer, disconnected from what the project actually needs. Pragmatism without autonomy is worse: a team correctly diagnoses that a project doesn't need nine layers, but ships nine anyway because the org-wide template says so. The two only work together. Before writing a line of code, it's worth stating plainly what the project is actually optimizing for: shipping speed this quarter, or maintainability over five years with a rotating cast of engineers? A single team that owns the whole stack, or a large org where infrastructure providers might genuinely change? A domain with real business complexity, or a thin CRUD layer over a database? The answer to those questions, decided on purpose by the people building the thing and written down, is the actual architecture decision — Clean Architecture, Vertical Slice Architecture, or a straight line from controller to data context are just the possible outputs of it.",[149,3450,3451],{},"Clean Architecture tends to earn its keep when the goals point toward:",[264,3453,3454,3457,3460,3463],{},[267,3455,3456],{},"large, long-lived teams where many people touch the same codebase over years",[267,3458,3459],{},"genuinely complex business domains (banking, healthcare, insurance, logistics, ERP)",[267,3461,3462],{},"multiple infrastructure providers or the realistic likelihood of swapping one out",[267,3464,3465],{},"requirements that are still actively evolving",[149,3467,3468],{},"In those environments, the cost of the extra layers is paid back many times over by the ease of onboarding new developers, isolating changes, and testing business rules without spinning up a database.",[149,3470,3471],{},"Vertical Slice Architecture tends to earn its keep when the goals point toward:",[264,3473,3474,3477,3480],{},[267,3475,3476],{},"feature teams that need to move fast and independently, without waiting on shared layers",[267,3478,3479],{},"systems made up of many loosely related use cases rather than one deeply intertwined domain",[267,3481,3482],{},"a preference for keeping each feature's complexity contained to that feature, even at the cost of some duplication",[149,3484,3485],{},"And a straight, undecorated controller-to-database path earns its keep when the goals point toward:",[264,3487,3488,3491,3494],{},[267,3489,3490],{},"small CRUD APIs and internal tools",[267,3492,3493],{},"early-stage prototypes and MVPs",[267,3495,3496],{},"proof-of-concepts meant to validate an idea, not scale to production",[149,3498,3499,3500,3503],{},"For projects like these, skipping the extra layers isn't a shortcut taken out of laziness — it's the right amount of architecture for the actual size of the problem, ",[335,3501,3502],{},"as long as it was chosen on purpose"," rather than defaulted into. As Uncle Bob himself wrote in the original post, the point of architecture is to minimize the human effort required to build and maintain a system — not to maximize the number of layers it contains. Adding structure a project doesn't need works against that same goal. So does adding none at all and hoping the mud stays shallow.",[153,3505,3507],{"id":3506},"a-practical-way-to-decide","A Practical Way to Decide",[149,3509,3510],{},"Rather than asking \"should this project use Clean Architecture?\" as a yes-or-no question, it's more useful to ask what problems the project actually has. If the honest answer includes complex business rules, a real need for infrastructure independence, or requirements that are clearly going to keep evolving, architectural boundaries are worth their cost. If the honest answer is \"it's a CRUD API with three endpoints,\" simplicity usually wins, and the boundaries can always be introduced later, once the complexity that justifies them actually shows up.",[149,3512,3513,3514,3517,3518,3521,3522,3525],{},"A decent working rule: introduce an abstraction when it solves a problem you actually have, not because a template or tutorial includes it by default. It's the same discipline Fred Brooks was gesturing at decades earlier in his essay ",[197,3515,1780],{"href":1778,"rel":3516},[201]," — there is no single technique, pattern, or architecture that eliminates the inherent difficulty of building software. Clean Architecture reduces ",[335,3519,3520],{},"some"," kinds of pain (coupling, testability, long-term maintenance cost) in exchange for ",[335,3523,3524],{},"other"," kinds (more files, more indirection, more upfront decisions). Pretending it's free in either direction — free to adopt everywhere, or free to skip everywhere — is where teams get into trouble.",[153,3527,252],{"id":251},[149,3529,3530],{},"Clean Architecture isn't inherently good or bad; it's a set of trade-offs with a name attached, and it's one of several reasonable answers to a question every project has to answer somehow. Separation of concerns, dependency inversion, and long-term maintainability are real, valuable properties, and the diagram Uncle Bob published in 2012 gave a lot of teams a shared vocabulary for reasoning about them. The trouble starts when those principles calcify into rules applied regardless of context — when a five-endpoint internal tool gets the same nine-layer treatment as a core banking platform, not because it needs it, but because that's simply what \"proper\" architecture is assumed to look like. That's a failure of autonomy: the team closest to the problem didn't get to make the call.",[149,3532,3533],{},"But the opposite failure is at least as common, and gets talked about far less: teams that never make the call at all, and drift by default into the Big Ball of Mud that Foote and Yoder described decades ago — not because anyone chose it, but because nobody chose anything else in time. That's a failure of pragmatism: even with the freedom to decide, nobody did the honest work of matching the structure to the goal.",[149,3535,3536],{},"The best engineering teams hold both at once. They're given — or take — the autonomy to choose Clean Architecture, Vertical Slice Architecture, or plain simplicity on a case-by-case basis, and they use that freedom pragmatically: stating the goal, picking the structure that actually serves it, and revisiting the choice when the goal changes, rather than defending whatever was chosen first out of habit or convenience. Architecture is supposed to serve the software being built — and the worst version of it isn't the wrong pattern. It's a decision nobody was free to make, or one nobody bothered to make honestly.",[257,3538],{},[153,3540,262],{"id":261},[264,3542,3543,3552,3561,3568,3577,3584,3591,3599],{},[267,3544,3545,3546,3551],{},"Robert C. Martin, ",[197,3547,3550],{"href":3548,"rel":3549},"https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html",[201],"\"The Clean Architecture\""," — the original 2012 blog post that introduced the concept and its now-famous diagram.",[267,3553,3554,3555,3560],{},"Alistair Cockburn, ",[197,3556,3559],{"href":3557,"rel":3558},"https://alistair.cockburn.us/hexagonal-architecture/",[201],"\"Hexagonal Architecture\""," — the Ports & Adapters style that Clean Architecture draws heavily on.",[267,3562,3563,3564,3567],{},"Jimmy Bogard, ",[197,3565,3426],{"href":3424,"rel":3566},[201]," — the 2018 post proposing feature-based slices as an alternative to layered architectures.",[267,3569,3570,3571,3576],{},"Brian Foote and Joseph Yoder, ",[197,3572,3575],{"href":3573,"rel":3574},"https://www.laputan.org/mud/",[201],"\"Big Ball of Mud\""," (1997) — the influential paper naming the de facto architecture that emerges when no deliberate structural decision is made.",[267,3578,2299,3579,3583],{},[197,3580,3582],{"href":3411,"rel":3581},[201],"\"Yagni\""," — a foundational argument against building abstractions ahead of actual need.",[267,3585,269,3586,3590],{},[197,3587,3589],{"href":3402,"rel":3588},[201],"\"Cargo cult programming\""," — the pattern of imitating a system's form without understanding its underlying reasoning.",[267,3592,3593,3594,3598],{},"Fred Brooks, ",[197,3595,3597],{"href":1778,"rel":3596},[201],"\"No Silver Bullet — Essence and Accident in Software Engineering\""," (1986) — the classic essay arguing that no single technique eliminates the essential difficulty of building software.",[267,3600,3601,3602,3607],{},"Herberto Graça, ",[197,3603,3606],{"href":3604,"rel":3605},"https://herbertograca.com/2017/09/28/clean-architecture-standing-on-the-shoulders-of-giants/",[201],"\"Clean Architecture: Standing on the Shoulders of Giants\""," — a comparison of Clean, Hexagonal, and Onion Architecture and how they relate.",{"title":293,"searchDepth":294,"depth":295,"links":3609},[3610,3611,3612,3613,3614,3615,3616,3617,3618],{"id":3284,"depth":295,"text":3285},{"id":3304,"depth":295,"text":3305},{"id":3343,"depth":295,"text":3344},{"id":3395,"depth":295,"text":3396},{"id":3417,"depth":295,"text":3418},{"id":3444,"depth":295,"text":3445},{"id":3506,"depth":295,"text":3507},{"id":251,"depth":295,"text":252},{"id":261,"depth":295,"text":262},"2026-07-20","Popularized by Robert C. Martin, Clean Architecture divides opinion for good reason. On the Big Ball of Mud, Vertical Slice Architecture, and why the real question is autonomy paired with pragmatism.",{},"/blog/clean-architecture-best-practice-or-creativity-killer","14 min",{"title":3262,"description":3620},"blog/2.clean-architecture-best-practice-or-creativity-killer","3hdgCtDTH7pc2CxZ7Hxkv31NhtVu0m11QK1ZrfJ2J_Q",{"id":3628,"title":3629,"body":3630,"date":3821,"description":3822,"extension":311,"featured":35,"meta":3823,"navigation":313,"path":3824,"readTime":315,"seo":3825,"stem":3826,"tag":117,"__hash__":3827},"blog/blog/1.copy-paste-code-vs-reuse.md","There's No Glory or Gain in Multiple Duplicated Implementations",{"type":146,"value":3631,"toc":3812},[3632,3635,3638,3642,3645,3648,3652,3655,3666,3670,3673,3712,3716,3725,3736,3740,3743,3747,3750,3753,3756,3758,3760],[149,3633,3634],{},"Every developer has done it: finish a feature, remember writing something similar six months ago in another repo, copy the file over, rename a few things, and move on. It feels fast. It feels productive. And it is — for about a week.",[149,3636,3637],{},"The trouble with copy-pasting code isn't that it's lazy. It's that it quietly converts a one-time savings into a recurring, compounding cost that somebody has to keep paying, usually without realizing they signed up for it.",[153,3639,3641],{"id":3640},"copying-isnt-reuse-its-duplication-wearing-reuses-clothes","Copying Isn't Reuse — It's Duplication Wearing Reuse's Clothes",[149,3643,3644],{},"It's tempting to think of copy-pasted code as \"reusing\" what you already built. It isn't. Genuine reuse means there's exactly one implementation, consumed by many applications. Copying produces the opposite: many independent implementations that merely started from the same source and immediately began drifting apart the moment any one of them got touched.",[149,3646,3647],{},"That drift is the whole problem in miniature. Six projects copy the same email helper. A year later, one has a bug fix the others don't, another added logging nobody backported, a third quietly changed a timeout value with no comment explaining why. They still share a name. They no longer share behavior. Nobody who joins the team later has any way to know that \"EmailHelper.cs\" in Project C isn't the same code as \"EmailHelper.cs\" in Project A — until something breaks in a way that only makes sense once you diff the two files line by line.",[153,3649,3651],{"id":3650},"every-bug-you-ship-gets-shipped-again","Every Bug You Ship Gets Shipped Again",[149,3653,3654],{},"The cost shows up most sharply the day someone finds a real problem — a security issue, a data-handling bug, a race condition. If the logic lives in one place, the fix is: find it, fix it, deploy it, done. If it's been copied into a dozen repositories, the fix is: find every copy, hope you actually found all of them, fix each one individually, test each one individually, and deploy each one on its own schedule. The repository nobody remembered about becomes next quarter's incident report.",[149,3656,3657,3658,3661,3662,3665],{},"This is precisely the failure mode the DRY principle — \"Don't Repeat Yourself\" — was coined to prevent. Programmers Andy Hunt and Dave Thomas introduced it in ",[335,3659,3660],{},"The Pragmatic Programmer",", defining it as the idea that every piece of knowledge in a system should have a single, unambiguous, authoritative representation. It's worth being precise about what that actually means, because it's widely misquoted as \"never copy code,\" which isn't quite it. DRY isn't really about the code — it's about not letting the same piece of ",[335,3663,3664],{},"knowledge"," (a business rule, a validation constraint, a timeout policy) live in more than one place, because every additional copy is another place that can silently fall out of sync with the truth.",[153,3667,3669],{"id":3668},"duplication-discourages-the-very-improvement-it-should-invite","Duplication Discourages the Very Improvement It Should Invite",[149,3671,3672],{},"Here's the part that's easy to miss: once code exists in twenty repositories, people stop improving it — not out of negligence, but because improving one copy while nineteen others stay behind doesn't actually fix anything. It just adds a twentieth variant to keep track of. The rational response to that situation is to leave it alone, and that's exactly what happens. Code that should be getting better over time instead calcifies, because there's no longer a single place where an improvement pays off for everyone at once.",[149,3674,3675,3676,3681,3682,3687,3688,3693,3694,3699,3700,3705,3706,3711],{},"Shared libraries flip that dynamic. When a validation helper, an HTTP client, or an authentication component lives in one internal package consumed by many applications, a single bug fix, a single performance improvement, a single security patch reaches every consumer at once. The ecosystem gets better together instead of one silo at a time. Modern package registries — ",[197,3677,3680],{"href":3678,"rel":3679},"https://www.nuget.org/",[201],"NuGet"," for .NET, ",[197,3683,3686],{"href":3684,"rel":3685},"https://www.npmjs.com/",[201],"npm"," for JavaScript, ",[197,3689,3692],{"href":3690,"rel":3691},"https://maven.apache.org/",[201],"Maven"," for Java, ",[197,3695,3698],{"href":3696,"rel":3697},"https://pypi.org/",[201],"PyPI"," for Python, ",[197,3701,3704],{"href":3702,"rel":3703},"https://crates.io/",[201],"Cargo"," for Rust — exist specifically to make this easy, and most support private feeds so an organization can publish and consume internal packages the same way it consumes open-source ones. Some companies formalize this practice further under the banner of ",[197,3707,3710],{"href":3708,"rel":3709},"https://en.wikipedia.org/wiki/Inner_source",[201],"\"InnerSource\""," — applying open-source collaboration norms to code shared across internal teams.",[153,3713,3715],{"id":3714},"dry-has-a-real-counterargument-and-it-matters","DRY Has a Real Counterargument — and It Matters",[149,3717,3718,3719,3724],{},"None of this means every ten-line helper deserves its own package, and it's worth taking the opposing view seriously rather than treating DRY as an absolute law. In a widely cited 2016 post, ",[197,3720,3723],{"href":3721,"rel":3722},"https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction",[201],"Sandi Metz"," argued that duplication is far cheaper than the wrong abstraction. Her point: when two pieces of code look similar but aren't actually the same underlying concept, forcing them into one shared implementation — usually by bolting on parameters and conditionals to handle each caller's quirks — produces something worse than duplication ever was: a single piece of code that nobody fully understands, serving multiple unrelated purposes at once, that everyone is now afraid to touch.",[149,3726,3727,3728,3731,3732,3735],{},"The two ideas aren't actually in conflict once you separate what each is warning against. DRY is a caution against letting the ",[335,3729,3730],{},"same knowledge"," live in multiple unsynchronized places. Metz's warning is against forcing ",[335,3733,3734],{},"coincidentally similar"," code — which represents different knowledge that only happens to look alike right now — into one shared abstraction before you're sure it's really the same thing. Good candidates for real shared libraries are the ones that pass both tests: they represent one piece of knowledge (how we authenticate a user, how we validate an email address, how we sign a request) that genuinely should stay in sync everywhere it's used, not just code that happens to look similar today.",[153,3737,3739],{"id":3738},"when-copying-is-actually-fine","When Copying Is Actually Fine",[149,3741,3742],{},"There are legitimate cases for copying, and it's worth naming them so the rule doesn't get cargo-culted into \"never copy anything, ever.\" Prototypes, throwaway scripts, one-off migrations, and tutorial code are all reasonable places to duplicate freely — the code isn't going to live long enough for drift to matter, and building a shared package for something used exactly once is pure overhead. The mistake isn't copying in these situations. It's letting temporary duplication quietly become permanent architecture because nobody circled back to it once the \"temporary\" script turned into a load-bearing part of the system.",[153,3744,3746],{"id":3745},"the-real-payoff-is-leverage-not-lines-of-code","The Real Payoff Is Leverage, Not Lines of Code",[149,3748,3749],{},"The deepest cost of copy-paste programming isn't the duplicated code itself — it's the time it steals from everything else. You might feel like you're doing interesting work each time you extend or tweak a copied helper, but the returns on that time are minimal, because you're not actually building anything new — you're carrying the same bugs, the same edge cases, and the same design mistakes from one place to the next, patching around them locally instead of fixing them once. Every copied utility becomes one more thing that has to be independently maintained, debugged, tested, documented, upgraded, and secured, multiplied by every project it was pasted into. Fixing the same bug in ten repositories is ten repositories' worth of time that could have gone into a new feature, a performance improvement, or an actual hard problem worth solving.",[149,3751,3752],{},"Great engineers tend not to measure their output in lines written. They measure it in future work eliminated. A well-designed shared library, built once and consumed everywhere it's needed, can save an organization hundreds of hours across teams that never have to think about it again. That's leverage — and it's the opposite of what copy-pasting produces, which is a debt that grows every time someone reaches for Ctrl+C instead of asking whether this is the second or third time they've written the same thing.",[149,3754,3755],{},"There's a version of this that's worth calling out directly, because it's where a lot of copy-paste programming actually starts: doing the same kind of integration or solving the same shape of problem for the third or fourth different project or client. That repetition isn't just a chance to reuse code faster — it's a chance to notice what went wrong last time and fix it going forward. The team that copies the same payment integration into a fifth codebase ships the same edge-case bugs, the same awkward error handling, and the same workarounds a fifth time. The team that pauses at that point — the moment a pattern has shown up enough times to actually be a pattern, not just a coincidence — and asks \"what did we learn building the last four of these?\" ends up with something better than any individual copy ever was: a shared implementation that's already had its rough edges found and fixed, instead of five separate implementations each carrying a different subset of the same mistakes. This is exactly the moment Sandi Metz's advice and the DRY principle stop pulling in different directions and start agreeing with each other — by the third or fourth similar integration, you're no longer guessing whether the pattern is real; you actually know, and that's when consolidating it pays off instead of backfiring.",[257,3757],{},[153,3759,262],{"id":261},[264,3761,3762,3774,3782,3789],{},[267,3763,3764,3765,3768,3769,342],{},"Andy Hunt and Dave Thomas, ",[335,3766,3767],{},"The Pragmatic Programmer: From Journeyman to Master"," (1999) — the book that coined the DRY principle. See also Wikipedia's overview: ",[197,3770,3773],{"href":3771,"rel":3772},"https://en.wikipedia.org/wiki/Don%27t_repeat_yourself",[201],"\"Don't repeat yourself\"",[267,3775,3776,3777,3781],{},"Sandi Metz, ",[197,3778,3780],{"href":3721,"rel":3779},[201],"\"The Wrong Abstraction\""," (2016) — the essay behind \"duplication is far cheaper than the wrong abstraction.\"",[267,3783,269,3784,3788],{},[197,3785,3787],{"href":3708,"rel":3786},[201],"\"Inner source\""," — applying open-source collaboration practices to internal code sharing.",[267,3790,3791,3792,3795,3796,3799,3800,3803,3804,3807,3808,3811],{},"Package registries referenced above: ",[197,3793,3680],{"href":3678,"rel":3794},[201]," (.NET), ",[197,3797,3686],{"href":3684,"rel":3798},[201]," (JavaScript), ",[197,3801,3692],{"href":3690,"rel":3802},[201]," (Java), ",[197,3805,3698],{"href":3696,"rel":3806},[201]," (Python), ",[197,3809,3704],{"href":3702,"rel":3810},[201]," (Rust).",{"title":293,"searchDepth":294,"depth":295,"links":3813},[3814,3815,3816,3817,3818,3819,3820],{"id":3640,"depth":295,"text":3641},{"id":3650,"depth":295,"text":3651},{"id":3668,"depth":295,"text":3669},{"id":3714,"depth":295,"text":3715},{"id":3738,"depth":295,"text":3739},{"id":3745,"depth":295,"text":3746},{"id":261,"depth":295,"text":262},"2026-07-10","Copy-pasting code quietly converts a one-time saving into a compounding cost. On DRY, Sandi Metz's \"Wrong Abstraction,\" and when duplication is actually fine.",{},"/blog/copy-paste-code-vs-reuse",{"title":3629,"description":3822},"blog/1.copy-paste-code-vs-reuse","onk92KrXlOVV_BaP6JHfyo2VR4Ck_SK0I-kr1XjEXns",1785744834584]