2025-12-16

I Don’t Trust Software I Can’t Understand Anymore

Software Architecture, Reliability Engineering, Security · Dorian Sotpyrc

Somewhere along the way, the systems I worked on stopped feeling knowable.
Not because they were broken — but because explaining them started to require diagrams, caveats, and “usually”.

I remember the first time I realized I couldn’t “just explain” a production service anymore. Someone asked a simple question in an incident channel: “What happens if this dependency is slow?” In the past, I’d answer with a crisp chain: request hits X, calls Y, writes Z, done.

This time I opened a slide deck. Not because I love slides, but because the truth had become conditional: cache warming, async retries, circuit breakers, multiple queues, feature flags, an auth sidecar, a service mesh policy, a hosted database with read replicas, a background reconciler, and an integration we “don’t touch because it usually works.” The story wasn’t linear anymore. It was probabilistic.

That was the trust break for me: not “bugs exist,” but failure modes I couldn’t name. And if I can’t name them, I can’t contain them.

When systems stopped feeling knowable

The old feeling: I could hold it in my head

Older stacks weren’t “simple.” They were bounded. You could still keep a mental model of:

  • where state lived
  • who talked to whom
  • what errors looked like
  • how you rolled back when things went sideways

Even when the system was messy, it was legible. Logs were ugly, but the causal chain existed.

The new feeling: diagrams, caveats, “usually”

Modern systems can be impeccably engineered and still feel unknowable. The shift isn’t just microservices or cloud. It’s the stacking of layers that each make local sense but erase global clarity:

  • platform abstractions that hide the underlying failure behavior
  • automation that changes systems indirectly
  • generated “glue” that compiles but doesn’t communicate intent
  • dependency graphs so deep that “what are we running?” becomes a research project

And every explanation starts with “usually.” Usually the queue drains. Usually the retries back off. Usually the autoscaler catches it. Usually.

Thesis: Knowability is safety. Understandability isn’t a developer comfort preference. It’s a reliability and security feature with downstream, measurable effects: shorter incidents, tighter blast radii, faster reviews, fewer unknown dependencies, and fewer “surprise behaviors” at 2 a.m.

Opacity isn’t just aesthetic—it changes risk

Reliability: debugging time explodes

Observability can tell you what is happening—latency up, errors up, saturation up. But when the system is opaque, you still don’t know why without reconstructing the hidden couplings.

That’s where incident time goes: not fixing the bug, but rebuilding a mental model under pressure. Dashboards are a map overlay; they’re not the territory. When the territory keeps changing underneath you, the overlay becomes comfort theater.

Security: unknown components become blind spots

Every layer you add is another place assumptions can break. The plain-language version of attack surface is: all the ways your system can be influenced or misused. Dependencies are influence channels. Build steps are influence channels. Default configuration is an influence channel.

Opaque systems don’t just have bigger attack surfaces—they have unknowable ones. And “unknowable” is where supply chain risk hides: the transitive dependency you didn’t mean to trust, the CI action you didn’t audit, the container base you haven’t pinned, the “helper” library pulled in by generated code.

When you can’t explain what you run, you can’t credibly claim you secure it.

Automation and AI glue code made “works” slippery

Generated integrations: nobody owns the shape

I’m not anti-automation or anti-AI. I’m anti-unowned behavior.

Generated glue code is seductive because it collapses time-to-demo. But it often ships with a quiet tax:

  • Intent disappears. The code “does the thing,” but no one can say why it was done that way.
  • Reviews degrade. PRs become pattern-matching exercises: “looks plausible.”
  • Edge cases metastasize. The “happy path” integrates; the failure semantics are a rumor.

And you get the recurring motif again: it usually works.

Toolchains: builds become magical rituals

In many stacks, the build pipeline has become a second application: generators, linters, formatters, code mods, dependency updaters, container builders, image signers, policy checkers, and deployment controllers—each useful, but together forming a system where cause and effect aren’t obvious.

When a release breaks, you’re no longer debugging your code. You’re debugging the interactions between:

  • config you didn’t know you had
  • defaults you didn’t choose
  • automation you didn’t write
  • transitive dependencies you didn’t approve

Why small tools feel safer (and it’s not nostalgia)

Transparent defaults beat clever abstractions

Small tools often feel safer because they’re inspectable. You can read the config. You can reason locally. You can see the boundary between “my responsibility” and “the tool’s responsibility.”

They fail in ways you can describe with verbs instead of vibes: “disk full,” “timeout,” “bad schema,” “permission denied.” That legibility isn’t quaint—it’s operational leverage.

Local reasoning beats global orchestration

Platforms promise to eliminate toil, and sometimes they do. If your team is drowning in undifferentiated work—provisioning, patching, scaling—a bigger platform can absolutely win. Centralized controls, paved roads, standardized telemetry, and opinionated deployment can reduce chaos.

The trade is that platforms also centralize complexity. If the platform is a black box, you didn’t remove complexity—you moved it. Your incidents become dependency on someone else’s mental model and someone else’s priorities.

Small tools feel safer because they keep the complexity budget where you can see it—and where you can pay it down.

Minimal system map sketch showing a few boxes, trust boundaries, and data flow.
Minimal system map sketch: fewer boxes, explicit trust boundaries, and named failure points beats a perfect diagram of a fog.

A practical definition of “understandable”

Can we explain it without a whiteboard?

I don’t mean “can we draw it.” I mean: can an on-call engineer explain it verbally in a few minutes without apologizing?

Can we predict what happens when it fails?

Not perfectly. But enough to answer: what breaks, how loudly, how far it spreads, and how we recover.

Rule: If the system can’t be explained, it can’t be operated. And if it can’t be operated, it’s not “done.”

Knowability fast checklist

Knowability fast checklist (print this)
Test Good signal Bad signal
5-minute dataflow One path for request + one path for async work “Depends on flags/mesh/policies” before you can start
Failure story Named failure modes + clear degradation behavior “It retries” / “the platform handles it”
State ownership Where state lives and who mutates it is explicit Multiple writers, eventual consistency “somewhere”
Dependency inventory Short list, pinned versions, known trust boundaries Transitive sprawl, “whatever latest”
Rollback path One-click rollback + known data compatibility constraints Rollback is “risky” or “we don’t do that”
Operational ownership A named team can change and debug it end-to-end “Ask vendor/platform team” for core behavior
Download the checklist

knowability_tests_fast_checklist.csv

How to build for knowability without becoming slow

Choose constraints over features

Speed isn’t just “shipping features.” It’s also recovering from what you shipped. I’ve started treating constraints as a performance optimization for the team’s brain:

  • Constrain dependencies. Fewer libraries, fewer base images, fewer “just add a service.”
  • Constrain runtime magic. Prefer explicit configs over implicit discovery and auto-wiring.
  • Constrain data paths. One canonical write path beats three convenient ones.

Design for inspection and ownership

Knowability doesn’t require refusing platforms. It requires insisting on visibility and control:

  • Make provenance normal. Pin versions; record what built what; make artifacts traceable.
  • Keep configuration readable and versioned. If a human can’t diff it, it’s a liability.
  • Name trust boundaries. Where does untrusted input enter? Where does auth happen? Where is it enforced?
  • Prefer “fail loud” defaults. Silent retries and implicit fallbacks should be rare and justified.
  • Require an operational story. “How do we debug this?” belongs in design review, not postmortems.
  • Practice rollback and degradation. If you can’t rehearse it, you don’t really have it.
  • Make ownership explicit. If nobody can change it safely, you don’t own it—you rent risk.

The new reliability feature: being explainable

Trust is earned through legibility

I used to treat “easy to understand” as a nicety—something you got if you had time. Now I treat it like input validation: not glamorous, but foundational.

When the pressure hits, the team doesn’t rise to the level of its dashboards. It falls to the level of its mental models.

What I look for now

My current rule of thumb is blunt: If I can’t explain the system’s behavior under failure, I don’t trust it in production. Not because I’m nostalgic, but because I’m trying to manage risk like an adult.

The litmus test isn’t whether the system is modern. It’s whether it’s legible: can we say what it does, what it depends on, and how it fails—without hiding behind “usually.”

Before adopting a platform (fast checklist)

  • Can we diagram the data flow in 5 minutes (request + async) and agree on the single “source of truth” for state?
  • Do we know the failure modes, default retries/timeouts, and the rollback/degradation plan?
  • Can we pin versions and verify provenance for builds and dependencies?

Related PLEX reading

References & further reading


Closing thought: Complexity is sometimes the price of capability. Opacity is optional. If you can’t make the system explainable, you’re not done building it—you’re done understanding it.