Writings

Posts tagged 'Programming'

  • Coming to Accept Black Boxes

    10 July 2025

    There’s a specific moment I remember from 2003, sitting in front of my Windows XP machine in Mumbai, hex editor open, staring at the handling.cfg file for Grand Theft Auto 3, convinced that if I could just understand how the suspension values mapped to the in-game physics, I could make the Banshee handle exactly the way I wanted it to—not because I particularly needed a better-handling Banshee, but because the idea that there was a system I couldn’t see through, couldn’t modify, couldn’t understand, felt like a personal challenge, like the game was holding something back from me that I had a right to know.

    I got the Banshee handling better. Sort of. It also started flipping over on hard turns, which I told myself was a feature.


    Twenty years later I’m building on top of FastAPI and pgvector and I haven’t looked at the source code for either one. Not once. I’ve read the documentation, I’ve traced through error messages when things break, I’ve made assumptions about how the connection pooling works based on behavior I’ve observed, but I haven’t actually looked. The code is right there, it’s open source, I could clone the repo this afternoon and spend a week understanding exactly how query planning works in pgvector or how FastAPI handles dependency injection, and I just… don’t. I accept that it works—or more precisely, I accept that when it doesn’t work, I’ll treat the symptoms rather than understand the disease. This bothers me more than it should.

    The progression happened slowly enough that I didn’t notice it was a pattern until it had already reshaped how I think about systems entirely. First it was firmware—something you simply didn’t touch, something that existed in a realm that wasn’t quite software and wasn’t quite hardware, living in EPROMs or flash memory that you couldn’t easily modify even if you wanted to, and the failure modes if you got it wrong were dramatic enough that you just accepted it as substrate, as the layer below the layer you actually worked in. Then operating systems. I remember when I knew what IRQ conflicts were, when I could tell you which interrupt my sound card was using and why moving it from IRQ 5 to IRQ 7 might solve the static I was hearing in Quake, when the registry wasn’t just some mysterious database you occasionally hacked at with regedit but something you could understand, where HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services actually mapped to something comprehensible in your mental model of how Windows worked. By the time Windows XP shipped I still knew how to fix these things but I’d stopped caring why they worked—there was a Control Panel now, the abstraction was good enough, and the fifty hours I might have spent understanding the service initialization sequence felt better spent literally anywhere else.


    Databases were next. I could write SQL—still can—but somewhere around 2011 I stopped thinking about query optimizers and execution plans and started just trusting that if I created the right indexes and wrote reasonably sensible queries, Postgres would figure it out. I’d learned enough to know when to check EXPLAIN ANALYZE, when to suspect a sequential scan was killing performance, but I’d stopped trying to hold the entire query planner algorithm in my head, stopped trying to predict exactly what it would do with a three-way join before running it. The database became a black box that responded to specific inputs—indexes here, VACUUM there, occasionally a hint if things got desperate—but I no longer needed to understand how it made decisions, just which levers to pull when the decisions were wrong. Game configuration files I’d conquered. I’d spend months learning how to modify handling.cfg, weapon.dat, the entire data directory of GTA 3 and Vice City, understanding exactly which values controlled tire grip and which ones controlled weapon damage and reload speed. But by 2010 I was playing games that didn’t even have accessible config files anymore, where everything lived in compiled assets or server-side databases, where the modding scene had moved from text file editing to actual reverse engineering with disassemblers and memory editors, and I made peace with the fact that I was just going to play games the way they shipped because the barrier to modification had risen beyond what my curiosity was willing to pay.

    Then media players with skinning. I remember WinAmp skins, remember downloading the SDK, remember thinking I’d build my own interface because why not, why should I accept someone else’s aesthetic choices when the skinning engine was right there, documented, ready to be mastered. Built one skin. Never finished it. Switched to Foobar2000 because someone else had already built the perfect minimalist interface and maintaining my own fork of perfection seemed like effort I could avoid. Note-taking apps, IDEs, the entire universe of tools that were supposed to make me more productive—these came with their own black boxes built in, their own proprietary file formats and sync mechanisms and plugin architectures that I could technically understand if I invested the time but which I increasingly just… didn’t. Notion’s database structure is completely opaque to me. I use it anyway. Obsidian’s sync works through some mechanism I’ve never investigated. I pay for it monthly. VS Code has thousands of configuration options and I’ve customized maybe thirty of them, accepted the defaults for everything else, trusted that the people who built it had good reasons for the choices they made even though I don’t know what those reasons were and haven’t particularly tried to find out. And now the entire SaaS backend. I’m building agentic workflows on top of infrastructure I don’t control, using APIs I haven’t audited, trusting that when I send data to Anthropic or OpenRouter or whatever service I’m integrating this week, they’re handling it the way they claim to be handling it, that their rate limits are what they say they are, that their model updates won’t silently break my prompts.

    I’ve read the API documentation. I haven’t read the infrastructure. I have no idea how Claude is actually deployed, what the load balancing looks like, whether my requests are being routed based on geography or load or some other factor I haven’t considered.

    I just send JSON to an endpoint and get JSON back and when it’s fast I’m happy and when it’s slow I complain but I don’t actually investigate because what would investigation even look like when the systems are deliberately hidden, when the entire business model depends on the abstraction staying intact?


    Here’s what I’ve realized, slowly, painfully, over two decades of this progression: abstractions are useful because they’re black boxes, and black boxes are dangerous because they’re abstractions, and I’ve been treating these as somehow separable concerns when they’re actually the same thing viewed from different angles depending on whether the abstraction is serving me or I’m serving it. When I could modify GTA 3’s handling.cfg, the game wasn’t serving me—I was serving my own curiosity, spending hours achieving vehicle behavior that was marginally better than the default in exchange for understanding that felt like control. The control was more addictive than the gameplay. The understanding was often illusory - I knew which numbers to change but I didn’t understand the physics engine they fed into, didn’t know how the collision detection worked or how the rendering pipeline decided which textures to load when. I thought I was peering inside the black box. I was just looking at a different interface to the same opacity.

    The databases I stopped trying to understand completely didn’t become more opaque - I just accepted that the opacity had always been there, that even when I thought I understood query optimization I was trusting that the optimizer’s model of efficient queries mapped to reality, that the cost estimates were accurate, that the statistics it gathered represented actual data distribution. I’d just moved the black box boundary from “I trust the database to optimize this” to “I trust the database to optimize this and I trust my understanding of how it optimizes to be accurate enough.” Turned out the second part was doing a lot of work and wasn’t actually necessary for building working systems. What changed wasn’t the presence of black boxes. What changed was my tolerance for operating at a higher level of abstraction without feeling like I was being irresponsible.


    But there’s a cost, and I feel it most acutely when things break in ways that cross abstraction boundaries.

    Last month I had an API integration that was failing intermittently, returning 500 errors about 3% of the time, completely unpredictable, no pattern in the error messages. Worked fine in testing, failed randomly in production. The service provider’s status page showed green. Their documentation had nothing about transient failures. I could see the requests going out, could see the failures coming back, but I couldn’t see inside their infrastructure to know whether this was a rate limiting thing or a load balancing thing or a database timeout thing or something else entirely. Used to be I could trace through these things. Used to be I could attach a debugger, step through the code, see exactly where the failure was happening and why. Now I’m just making requests to an endpoint and the endpoint is either working or it’s not and my only recourse is to implement retry logic and exponential backoff and hope that whatever’s causing the transient failures resolves itself before the retries exhaust themselves.


    That’s what black boxes cost. Not understanding. Options.

    When the abstraction is working, when the black box is doing what it’s supposed to do, the cost is invisible—you’re moving faster, building more, delegating cognitive load to systems that handle it better than you could. But when the abstraction fails, when the black box reveals itself as a box you cannot open, you’re stuck building workarounds for problems you can’t diagnose, creating defensive code for edge cases you can’t verify, accepting failure modes you can’t prevent because you don’t have access to the lever that would prevent them.

    And the response to this, the thing I’ve watched myself and everyone around me do, is to build more abstraction on top of the broken abstraction, to create retry mechanisms and circuit breakers and fallback paths and health checks, to instrument around the black box instead of instrumenting inside it. The monitoring gets better. The understanding gets worse.

    There’s a particular form of learned helplessness that comes with accepting too many black boxes, where you stop even asking whether something could be understood and just immediately jump to “how do I work around this” or “is there a different service that doesn’t have this problem” or “maybe I’m using it wrong.” The instinct to investigate atrophies. Not because investigation is impossible—most of these systems are built from components that are themselves documented, open source, theoretically understandable—but because the investment required to understand them fully is wildly disproportionate to the benefit you’d gain, and you learn, eventually, that the cost-benefit doesn’t pencil out.

    I learned this at Kingston, actually, during the MSc. We had an assignment that required understanding how a particular database optimization worked, required reading the source code and writing up an explanation of the algorithm. I spent three days on it. Three days to understand one component of one system, and at the end of those three days I understood that specific optimization deeply, could explain the tradeoffs it was making and why those tradeoffs made sense given the assumptions embedded in the implementation, and then I moved on to the next assignment and never used that knowledge again because in practice I was never going to implement my own database optimizer and the one I was using was doing fine.

    Three days. For knowledge that depreciated immediately.

    And I thought at the time that this was the fault of the assignment, that it was academic busywork disconnected from practical development. But looking back I think the lesson was the opposite—the assignment was showing me what it actually costs to understand a black box, showing me that the price of true understanding is often prohibitive, that the reason we accept abstractions isn’t laziness or lack of curiosity but rational calculation of where to spend limited cognitive resources. The people who built pgvector spent months, probably years, understanding vector similarity search well enough to implement it efficiently in Postgres. I benefit from those years by running pip install pgvector and writing three lines of configuration. If I spent three years understanding it as deeply as they do, I’d have three years less to spend building the thing I’m actually trying to build. The abstraction isn’t hiding complexity to obscure it from me. It’s hiding complexity to spare me from it.

    But then I’ll hit an issue like that API integration, where the black box fails in a way I can’t diagnose and can’t fix and can barely even describe in a support ticket, where I’m reduced to saying “it fails sometimes, here are the error codes, I don’t know why,” and I feel the loss of that knowledge acutely, feel the gap between what I could have debugged twenty years ago—when the entire stack was smaller, simpler, when I could actually hold the whole thing in my head—and what I can debug now, which is basically just my own code and anything that exposes enough surface area for me to poke at with logging and observation.


    The young engineer I was modding GTA 3 config files would have been horrified by this, would have seen it as surrender, would have insisted that you can’t build good systems on top of components you don’t understand. And he wasn’t entirely wrong. There’s something real that’s lost when you accept too many black boxes too readily, when you stop interrogating the boundaries of what you understand and start just stacking abstractions because it’s faster.

    But he also thought understanding meant control, thought that if he just learned enough he could predict and manipulate everything, thought the goal was to render all systems transparent through sufficient effort. He spent six hours getting a video game car to handle marginally better. He didn’t build anything real for months because he was too busy trying to understand everything.


    I’m faster now. I build more. I deliver actual value instead of infinitely pursuing comprehensive understanding. I work with systems that are orders of magnitude more complex than anything that existed when I was learning this stuff, and I do it by accepting that I will never understand them completely, that the boundary of my understanding will always be far short of the boundary of what I’m using, that this is fine, that this is how things work now.

    The acceptance isn’t comfortable. It doesn’t feel like maturity or wisdom. It feels like compromise. When I scaffold a new FastAPI project and accept the defaults for dependency injection without understanding how the injection graph is constructed or when dependencies are resolved or what the performance implications are of my particular configuration, I’m making a bet that the defaults are good enough, that the people who built FastAPI thought through these problems more carefully than I would, that my time is better spent elsewhere. Most of the time that bet pays off. Sometimes it doesn’t, and when it doesn’t I’m stuck with a system I don’t fully understand failing in ways I can’t quite diagnose.

    And maybe this is just what software development looks like at scale, maybe the only way to build complex systems is to compose black boxes and trust that the abstractions hold and add defensive coding around the places where they might not. Maybe the alternative—trying to understand everything, trying to maintain systems where every component is fully transparent—is not noble or rigorous but simply impossible given the size and complexity of what we’re trying to build.

    But I remember what it felt like to modify that handling.cfg file, to see the change propagate through the system, to know what I was changing and why. I remember the satisfaction of understanding, even when that understanding wasn’t particularly useful, even when it didn’t generalize, even when the time investment was absurd relative to the outcome.

    I don’t know if I’ve accepted black boxes or just capitulated to them. I don’t know if the speed I’ve gained is worth the understanding I’ve lost. I still check the source code sometimes, still spend afternoons diving into library internals when I probably shouldn’t, still feel that old instinct to know rather than just use.

    I just do it less often. And I notice, more and more, that when I do it, it’s often for the feeling rather than the outcome—the feeling of understanding, of control, of having peered inside the abstraction and seen how it works. The feeling I remember from 2003, sitting in front of a hex editor, convinced that if I just understood enough I could make the system do exactly what I wanted.

    That Banshee still flipped over on hard turns, though.