Blog
Atom feed is here.
Over my time in the software engineering industry, one of the most common unspoken expectations I have observed among software engineers is that one is obligated to produce code. If you challenge this, there is some sort of visceral bewilderment, like, "what are you insinuating," or, "what did you hire me for?"
Yes, we need to produce code to build things. However sometimes what a project needs is for someone to delete code. Or even for someone to just maintain the code, such as when a project has shipped but the core maintainers have left for whatever reason. All engineers that I have met, and myself in my own personal experience, have been caught off guard when--if--they have this realization.
But why? That's what this blog post is about.
In this field, whether someone learned programming in school or was self-taught, the reason is because they wanted to (or were forced to) build something. Not maintain something. Not delete something. Their teacher said, "the project for this next two weeks is to build a compressor," or some other mechanism implementable in code. This makes sense. First you learn the theory and then you build it yourself for experience. But have you ever heard a professor say, here is a 300k line project. Can you preserve its behavior but simplify its implementation? Or, here is a project that meets all customer requirements. Over the next two weeks, we'll reveal real bugs to you in the form of simulatd user feedback, and you need to fix them. Or maybe the student needs to upgrade a fundamental dependency, and the next version has breaking changes.
Boring! Nobody will pay US college prices for that. And no self-taught programmer started by finding a complicated, existing project they liked and thinking, wow, can't wait to maintain this thing. No, it's, can't wait to make my mark on the world by building something.
We bring the next generation up with the incorrect expectation that software engineering is the art of emitting code in all circumstances. As anecdotal proof, how many people do you know would call a project with no changes over the last few years "dead," even if people used it all the time, because said people don't know what stable code is? Sir, that is a JSON parser, and there is just nothing more we can add to that project to parse JSON harder, better, faster, stronger.
If you're building your own thing, go ham. Take something from empty text file to product. That's awesome. The trouble comes particularly in industry, where sometimes the project is in exactly the state it needs to be in, but instead of letting it rest in that state and fulfill its requirements, we are compelled to disturb it, in the form of scratching that itch, that I must build, I must grow, I must extend something. That is why I exist here. And in doing so I feel a sense of purpose and fulfillment, stimulation, meaning.
No, the thing you must come to terms with in these periodic circumstances is that you get paid to show up and use your technical expertise to prevent the thing from growing or changing needlessly.
Alas, as we all know, most companies do not incentivize letting things be. The job description you applied for required 10 years of experience in that one language in addition to mastery of peripheral tools and skills, because you are expected to build someone's technological vision of the future described in a couple sentences in English. And who knows, if the implementation is Byzantine enough, you may even get to inject your own quarantined vision and mutate endlessly behind its interfaces.
Less facetiously, imagine sitting in front of your computer as a software engineer, and your boss walks by. You're just sitting there, maybe casually browsing the web. A couple hours later she walks by again. You're still sitting there, maybe now looking at your phone after peeking at your inbox to see if you had any new alerts mailed to you. The product is working, making money even. Things are stable and good. But neither you nor your boss feels good about--nay, can justify to your boss's boss--your behavior that day.
Or maybe you wanted that promotion. How can you quantify how much professional nothing you did, even if it was the right thing to do regarding (not) evolving the codebase, to make a case why you deserve a promotion? You can't, to most bosses in this field. But everyone understands the (often-manufactured) problem of some system that suffered for some reason, and you showed up and banged all this stuff out, and now that suffering is gone, and suffering being gone is good, right? No, it wasn't customer suffering. No...we weren't suffering either. Okay, I was suffering, because the honest best thing was to have done nothing at all, but then I feel like I'm wasting my life, and it didn't serve me!
Outcomes follow incentives. But to steelman the situation, selling the reality is hard. Which job would you apply for, the one that needs engineers to constantly be building, or the one that says you'll build when required but will otherwise maintain the result of someone else's productivity and honestly probably won't be promoted from Software Maintainer to Senior Software Maintainer?
There are companies out there that need a lot of things built, even big companies, and in these places people will find themselves building all the time. But most people are not prepared (themselves, nor by others) for what awaits them in many companies today.
This unpreparedness has a very unfortunate side effect, which is that it causes people to revel in complexity because mutating something gives the illusion of productivity, and then this gets reified, and people start to think that this is what software engineering is all about. And if you threaten this, attached people feel it the same as you threatening them.
Another unfortunate side effect is that a lot of the software products you use produced by companies that incentivize their engineers to be busy changing code will likely eventually get worse over time.
Complexity is natural when you don't know how to do something. But complexity does not need to be forever. See images of "SpaceX Raptor engine evolution" for this point in an image.
If you felt like this post described the situation around you, I encourage you to make a change in your environment. There are lots of meaningful things that are waiting for us to just build them, but many things being built right now are meaningless in the long run. Remember that you ultimately pay with your time.
Transitive dependency management is still hard.
Recently I started replacing SDL windowing with my own. This got me into
Wayland. When developing against the Wayland protocols in C or C++, you run
wayland-scanner to parse protocol definitions from XML files and generate
C code. This C code features many static inline functions, which the Jai
bindings generator won't touch, meaning we are left to handroll many convenience
function implementations on our own. I gave this a shot, but maintenance was
obviously going to be a huge chore, so instead I reimplemented wayland-scanner
in Jai, to emit Jai.
At first I did all of this in a single repository, the same one that I was building my own platform-abstracting code in. This was fine and well. But I figure the Jai wayland-scanner is useful. There are several other Jai implementations in existence, but I haven't seen a standalone one yet, or one that doesn't require hand-writing more code than my implementation requires (in my case, it's just forward declarations of opaque types). So I thought to extract wayland-scanner to share it, and then I spent the next couple hours figuring out how to sanely handle the transitive dependencies, as many others have done in this field.
The dependency graph looks like this.
/-> b -> c -> d
a
\-> d
And this is all it takes to get us into trouble. (The case of a -> b -> c and
also a -> c--that is, removing d--also demonstrates the trouble but is
easier to resolve, so we take the more involved case in this post.)
It doesn't really matter, but concretely these are the repos:
a: the game with platform abstraction code
b: Wayland bindings
c: wayland-scanner-jai
d: xml-jai (my own, not the more popular one by the same name)
We want xml-jai to be separate from the scanner, because if I want to parse
XML, I don't think to reach for wayland-scanner-jai. We want the scanner to
be separate from the bindings, because if I just want to use pre-generated
bindings, I don't want the code for the scanner or XML parser. Also, the
bindings contain a little bit of glue code required to get the scanner's output
compiling, and this code is out of scope of the scanner (following the scoping
in the original scanner implementation).
Jai currently has no package manager. While there are already community efforts mulling over an implementation, Jon and Thekla have no intent to build a first-party one. I sympathize with the former and latter.
My philosophy on dependencies is, whenever possible, don't have them. You can accomplish this in 3 ways, from least extreme to most.
The first is questioning whether you really need to do what the candidate dependency does in the first place. It's easier to say "just don't do that" than it is to actually not do that, but it's always worth exploring. For example, when programmatically loading Aseprite assets into my engine, I would have had to deal with JSON since that is what the first-party exporter supports. I explored writing a JSON parser in Jai, and that was interesting, but I wasn't satisfied with it. Instead, I extended the Aseprite-side export tool to emit a binary format that I easily parsed in Jai.
The second is reimplementing a minimal version of the dependency you have in mind. I did this with the above-mentioned XML parser. In about 100 lines of code, I have a parser robust enough to parse everything I need from the Wayland protocol definitions. The code is very simple, and it erased the need to grab a dependency from somewhere else.
The third is bringing the external dependency into your project verbatim. Yes, this is technically still "having a dependency," but the idea is that you avoid all of this complicated machinery required to dynamically fetch the external dependency at build time, with the right upstream source and version, etc.
With that said, the second and third approaches here still face the transitive dependency issue mentioned above once you want to share that part of the code with others, even potentially internally in the same project if your development style is one of those insane ones with hundreds of people living on their own feature branches for months. (If you haven't seen this, this is more common than you might think, for example in traditional automotive, where initial release cycles are on the order of years.) The third approach also has another problem--synchronizing patches--which I'll cover later.
Oh, also, how many XML parsers does the world really need? For real.
On the other hand, we have sophisticated dependency management solutions. Think Bazel, Cargo, pip, etc. with their protocols and remote registries. These solve real problems, but they also introduce new ones.
The biggest problem is one of end-user discipline or even just innocent coincidence, and that is dependency creep. You need this one dependency, and so you bring it in. Someone else needs another, and the diff to bring that one into the codebase is a one-line change in some dependency registry file. Without discipline, you see where this is going. More nefarious is one of your dependency's transitive dependencies. Maybe it only has a single transitive dependency, but then maybe when you pull in upstream changes, the dependency now has 5 new dependencies. This recurses down the dependency tree, with cases of "just adding one dependency" ending up making you suddenly transitively reliant on 10+ projects. In other words, a one-line change in your project is actually a step function jump in project complexity.
No one on your project is reviewing all the new commits across all dependencies, so you're succeptible to vulnerabilities or even malicious code--this becoming even more common in the budding age of agentic AI--in your dependencies.
There's also the amusing situation of asking a package manager for two dependencies, each of which wants a different version of the same transitive dependency. With strict configuration, you can ban this, and you will get a configure or build-time error. But we actually need to build our software, so at best you can leverage the package manager's compatibility resolver to find a version that satisfies both dependencies. Of course, you're still at the mercy of whoever wrote that shared dependency having gotten the versioning and compatibility correct. At worst you end up getting two incompatible versions of the same dependency anyway and using them under the hood.
These aren't digs at package managers. This is a real problem, "how do I build this thing without boiling the ocean" is a valid question, and these three situations are the only answers in the package manager world.
These points have been exhausted before, elsewhere.
Other nuisances are the machinery that is the dependency manager. It's got its own configuration language (ex: Blaze/Bazel's Starlark, Cargo's use of TOML, pip and company's use of ini-like formats, or TOML, or JSON, or even programmatic configuration through Python), caching strategy, proxy nuances, and version resolution algorithm. Now if you want to be a "foo-lang" expert, you also need to be an expert of some foo-lang build tool, even if you found yourself only needing 10% of that tool's functionality across your career. If you work for BigCorp, you will also probably end up with a FooLang Build Tool team.
I digress. The point at hand in this post is the challenge of different parts of a larger codebase depending on different versions of the same dependency. I'd really like to avoid that. Reiterating my above diagram:
/-> b -> c -> d
a
\-> d
How does one ensure that both versions of d are the same? This might not be
important, until you try to transact at a -> d using data produced at the
c -> d interface only to find that data that works with one version of d
doesn't work with another in your same codebase. Okay, so maybe the versions
don't have to be the same; they just have to be compatible, right? Enter API and
ABI compatibility, versioning, semantics guarantees, etc. and take the personal
productivity hit. Or, if you're a bigger company, reify all of that and end up
spending time and money addressing symptoms instead of causes and have it
justified to anyone who questions as adhering to "best practices."
Jai does have a code re-use mechanism, called "modules." In my concrete example, if you want wayland-scanner-jai to be buildable standalone, (let's say the project is managed with git) then you could depend on xml-jai via submodule. You could also just bring in a source copy of xml-jai. The submodule approach has the benefit of tracking exactly which version of xml-jai you depend on and also providing diffs of any changes you've made to your version relative to changes upstream. This allows you to easily bring in upstream changes or submit local fixes upstream.
I promised above I'd say something about it, so briefly, the source-copy approach has none of these benefits, and maintaining synchronization with the original source, if desired, is a chore. This chore only gets more complicated when you leave the project or codebase and someone else has to figure out the current relationship, if any, between your copy and upstream. On the other hand, you are free to do as you please and integrate what you want how you want in the source-copy, and your build very likely stays simpler.
Regarding the scanner, ideally it is buildable standalone, so that it can be tested standalone. That implies the scanner either has a source copy of xml-jai or a submodule for xml-jai. But now say in my game engine, I want to depend on the scanner, build the scanner as part of my project-top-level build procedure, and then run the scanner later in my build for ultimate developer convenience. Do I run
git submodule update --init
and should I add --recursive? That's not terribly different from the package
manager situation conceptually. So maybe I should bring in the xml-jai
dependency by myself and put it on the module search path in my game engine.
But now I'm faced with the compatibility problem again. Or maybe the scanner
has its own copy of xml-jai. How many times do I want to debug bespoke
implementations of things accomplishing common tasks and also maybe deal with
upstreaming fixes?
If I segue for a moment and take a more macroscopic look, we have platforms like Android which say, look, we can't fix this, use whatever version of whatever you want, and now your Android runtime has X versions of Y package in it. iOS is stricter, taking in places the approach of, if you want your software to run on version foo, then you need to build against version U of software/interface V.
Canonical takes a more traditional approach of combining together whatever
exists in the software and package ecosystem required to get the job done,
albeit somewhat dated versions. When preparing their next operating system
release, they select slightly older verions of software and packages that have
proven some degree of stability and compatibility in the field already. It's
a simple approach, with the downside being you're running on older software and
one botched apt install could leave your system in a broken state.
NixOS says, let's do declarative system configuration with atomic updates. Now the "works on my machine" issue is solved, the system-in-broken-state issue is mitigated, and clean rollbacks are possible.
But all of these have one thing in common, which is that they address symptoms instead of causes. I say that our field is still nascent in this regard.
It feels like C runs everywhere with no effort, but if you look under the hood of the various
libc distributions, you see all this #ifdef logic testing for host
properties, in order to get the user-facing unified interface to "just work."
The Windows API is by far the most stable and cross-(Windows-)platform interface
at that level, and it also does lots of work behind the scenes to match
user-side usage with the actual underlying platform implementation. Things in
the digital computing world have always been this way, and abstraction from
non-uniformity to some degree is good and necessary for productivity. The
solutions we have today in the form of package managers, OS configuration, etc.
are reactions to complexity growing at the level above lower-level unified
interfaces. The pattern has repeated itself. Perhaps most notably is the web
development ecosystem, which seems to have abstraction upon abstraction.
The current complexity of transitive dependencies is a natural phase of evolution of this field. Addressing causes will have us look at the complexity macroscopically and ask, what are the fundamental things we're all trying to do here, and how can we build unified interfaces to do those things? (Regarding unified interfaces, I'm talking about for very common things: display something to the user, get input from the user, play audio, dispatch work to a GPU, transact over the network, etc.) Currently we, especially bigger companies in the field, are incentivized to build walled gardens. Often times these walled gardens grow so large that the complexity problems from lower levels reemerge, and then a simplification and unification attempt is carried out in the walled garden, and this looks like progress, but it is often another stab at symptoms further up the stack of abstractions, not targeting causes in the opposite direction.
So when it comes to my project's dependencies, if I'm building a tool, a game, a packaged experience, then I do the following:
- attempt to avoid dependencies
- if unable, attempt to reimplement dependencies
- if unable, attempt to absorb dependencies
- if unable, resort to package-manager-like solutions
If I'm building a reusable library, then I try to depend only on unified low-level interfaces and solve problems people are actually having at scale.
Yes, alas, unified, or "standard," interfaces on *nix means and guarantees something different than on Windows, than on macOS. I am in favor of an alternative, but we'd need to develop it, and that's a subject for a different time.
And yeah, I guess this means I need to bring xml-jai into wayland-scanner-jai in my case. In actuality, my game engine doesn't need xml-jai itself, but I did use the example in this post as the basis for my reasoning.
Framing
This is a post written for progeny, to help them figure out what to do and why. I'll evolve this post as my understanding changes, but I will post new versions as new blog entries so that previous versions remain.
This post is more like an entry in my journal that I've made public, a post about finding my way in the world of game development.
Here is the curated commit log from this last week's development.
2026-02-03
feat: removed entity action timer; moves are now turn-blocking anims.
2026-02-03
feat: render entity index in graphical turn queue debug visualization
2026-02-03
feat: allow mult. entities in turn queue to move on same frame
2026-02-03
feat: introduce reaction to entity death after attack animations play
2026-02-06
feat: add component to support entity pos interp. during movement
This also updates the camera to move with the player as position is
interpolated, having the effect of a smooth-scrolling camera instead of
a camera that moves in units of whole tiles.
This morning I was thinking about the Cassandra Complex and general mentality. Over the last few years, recent popular podcasts (which I'll just not name) have repopularized the term. Often the speakers use the term as cathartic relief, to express some kind of frustration they have over a thought that they feel mocked, attacked, or persecuted for instead of rewarded for. For a long time, I thought, "yeah, that sucks...," but I realized even this is an acceptance of self-pity. Just reframe the situation, and you have a huge win on your hands. You want to be right and early. That is where you make outsized returns. So if you know things should be done differently, do so, even if it imposes a cost--not an immediate reward--in the short term, because when the long term pans out and the world catches up, you'll be many steps ahead and ready to launch that book, deploy that technology, release that game, mentor those people, etc. Even this is a grow-the-pie winning opportunity.
I missed the update two week's ago, and last week's update is late, so this post is a double feature! Recently I am spending a lot more time editing Twitch VODS to create a YouTube dev log of sorts. This amounts to me listening to me build my own game instead of spending time building the game, and these blog posts are also me talking about working instead of "working," so I will probably start to make these updates more concise.
I will still post small videos of gameplay directly in some of these updates which will serve as quick visual checkpoints in the journey, though. I will also still write the occassional long post when I have a topic I want to discuss in more detail.
This last week I focused on implementing basic animations.
This week I imlpemented an entity-component-system, a basic player attack, entity deletion, and text and textbox rendering.
This week I implemented A* and added support for multiple enemies to the game.