If AI Writes the Side Projects, How Do Engineers Level Up? An Experiment in Git Archaeology
Five years ago, writing some software in an unfamiliar language in my spare time might have had some value beyond hobbyism. I’d learn new syntax, new idioms, and hopefully some best practices. Now I'm reconsidering where to focus my efforts. AI agents can write better code than I can in most languages. Churning out working code isn’t anything special. The skills that seem to matter most now are architecture and judgement: the things that make an engineer senior1. You learn these skills best from complex projects with real production users.
In this post, I’m exploring the idea that using AI agents to map out the development of prominent open-source codebases can develop judgement in ways that would have required becoming a long-time contributor in the past. The kind of judgement I’m looking at is:
- Which interfaces and abstractions earn their place as a project evolves?
- Where do the trade-offs lie, even for sound decisions?
- How does the concurrency model hold up as things get complex and scale?
- What automation and processes can tame a project’s complexity?
This is the thesis I’m testing: While AI agents take with one hand by making hobby coding even less valuable, they can give with the other by helping us glean wisdom from open-source projects.
I chose to look at a Go project because while I’m not very familiar with the language, it's relevant to my current role as a platform engineer. Most of the technologies I use daily are written in Go. Notably: Docker, Kubernetes, OpenTofu (Terraform fork), and the OpenTelemetry collector. I’ve written some Go on weekends: Advent of Code and the kind of low-stakes projects that don't seem to matter now. I may have nearly a decade of commercial software engineering experience, but I lack Go-specific battle scars. For instance, I've never had someone complain that a Go interface I wrote years ago is now causing them problems, as I have with the public API of Python modules.
Below I ask an agent to summarise the state of etcd (the distributed key-value store underpinning Kubernetes) at key versions, then synthesise it into a narrative of how the system developed in response to user needs and production scale. This is a jumping-off point for exploration and thinking. I acknowledge the risk of regurgitating AI-generated text without truly understanding it, so I endeavour to explore the threads I write about independently.
What I Learned from etcd
I started with some AI-assisted git archaeology. I prompted2 Claude Code (Opus 5) to generate a report on each of four etcd versions, v0.1.0 to v3.7.1. I read the individual reports, which were overwhelming because they contained more plausible observations than I could ever hope to verify. To find a thread running through all versions, I used a second prompt3, along with the reports, to generate a summary. The only thing I’ll directly quote from the summary is this table. It shows, at a high level, how the project grew and when each API version arrived and left.
Non-test, non-generated, non-vendored Go lines, measured per tag
(git ls-tree -r <tag> filtered to *.go, excluding _test.go, *.pb.go, and
third_party/, Godeps/, cmd/vendor/, */vendor/):
| v0.1.0 | v2.0.0 | v3.0.0 | v3.7.1 | |
|---|---|---|---|---|
| Whole tree | 3,204 | 16,909 | 57,860 | 110,019 |
| Consensus library | 3,798 (vendored go-raft) |
2,034 (raft/) |
3,666 (raft/) |
0 (external module) |
| v2 keyspace state machine | 1,203 | 1,907 | 2,212 | 2,231 (no API) |
| v3 keyspace state machine | — | — | 3,346 | 3,962 |
| Watch | 129 | 434 | 988 | 1,180 |
| Lease / TTL | (inside store) | (inside store) | 651 | 1,522 |
What stands out most in this table is that the Raft consensus implementation has had three homes: first as a vendored library, then in the repository, and finally spun out into a separate repository.
Spinning Out a Pure Raft library
Raft4 is the consensus algorithm that keeps the keys and values stored in etcd consistent across multiple nodes. Raft means production Kubernetes clusters can deploy their data store across multiple nodes in a highly available configuration. Raft also underpins many of the guarantees etcd provides.
A thread Claude identified from the initial v0.1.0 release to the recent v3.7.1 was the Raft consensus seam, which, as of today, connects the etcd server to a pure logic Raft library that leaves I/O to the consuming system. The same library, extracted from etcd, is used by other distributed databases including CockroachDB and Dgraph.
In v0.1.0, there was a different external Raft library, and its interface wasn’t as clean. Raft wrote to the file system to update the log; it also called time.Sleep. By v2.0.0, this was replaced with an in-repo implementation: the I/O was factored elsewhere and time was represented as a logical tick rather than interacting with a system clock. (When attempting to understand the new Raft library, my puny meatbag brain found the simplified key-value store example code more helpful than current etcd source code.)
At the time of writing, the Raft library README says:
Most Raft implementations have a monolithic design, including storage handling, messaging serialization, and network transport. This library instead follows a minimalistic design philosophy by only implementing the core raft algorithm. This minimalism buys flexibility, determinism, and performance.
When writing software professionally, I try to extract business logic so it doesn’t contain side effects and I/O, favouring pure functions and dependency injection. Even in commercial software, this can be hard to achieve, and the payoff is often limited. For something like Raft, which has a PhD thesis devoted to it, I can imagine it was much harder. The payoff is greater too, as other distributed databases can and do use the system regardless of how they implement I/O.
Not all software components can be pure, however. In etcd, they didn't delete the messy parts; they moved them to the etcd server.
The Messy Details
In version 0.1.0, etcd passed go-raft a directory path to write log entries to. This was paired with a callback function that applied log entries to etcd’s state.
type Log struct {
ApplyFunc func(Command) (interface{}, error)
file *os.File
path string
entries []*LogEntry
results []*logResult
commitIndex uint64
...
}
Source code here.
The first external Raft library owned I/O and also owned the messy business of ensuring that all log entries were applied to the store. This is simpler in some ways because go-raft could commit changes to the log and apply them to the data store in the same function, managing concurrency with the same mutex.
From version 2.0.0, even before it became a separate library, the Raft component contained only the business rules for keeping multiple nodes’ state consistent. The etcd server was now responsible for ensuring that the database’s state reflected the WAL entries. At this point, though, not much could go wrong. As in version 0.1.0, the state lived in memory, so if a node died, it would rebuild the state from a combination of a snapshot and the WAL.
In version 3.0.0, the database state became durable. The release notes mention “new storage engine”. The part of this engine that stored key-value data on disk was Bolt (a fork of. While this change gave etcd better transaction isolation (the I in ACID) with MVCC, it also created a new durable record on every node: the key-value store in addition to the existing Raft WAL. This introduced a new problem when rebuilding an etcd node’s state: the etcd server needed to track the WAL offset that had so far been committed to the node’s key-value store. etcd committed to Bolt in batches (every 100ms or 10,000 writes), so after a crash the database could lag behind the log, and if it replayed already-applied entries, it could apply them twice.
This is the commit message for saving that offset to Bolt: “etcdserver: save consistent index into v3 storage […] This helps to recover consistent index when restart in the future.” Consistent index doesn’t mean that the index is consistent, so much as that it’s the index of the latest committed Raft WAL entry that’s been written to the etcd server’s on-disk state. At this point, the design is sound: the index is written in the same Bolt transaction as the data, so after a crash both are at the same point, and etcd knows exactly where to resume. From v3 onwards, data consistency rests on this being true in all situations.
Version 3.0.0 burdened the etcd server with ensuring that the consistent index agreed with what WAL entries had in fact been applied. It introduced a new class of potential bug which eventually manifested, resulting in error reports like this one. It is every Kubernetes user’s nightmare: “Some keys exists on node x and y but not on node z, and some on z but not on the other two, e.g. different pods list would be returned from different servers.” The root cause turned out to be this PR, which aimed to ensure the consistent index was saved on each commit. Unfortunately, there were a couple of unintended consequences:
- The in-memory consistent index value was shared state, read by any commit, not just the one carrying the matching data.
- This in-memory value was updated before the transaction was committed to the key-value state (Bolt), creating a window in which a concurrent commit could persist it (e.g. the periodic commit that runs every 100ms).
Saving the index and saving the data it pertained to no longer comprised an atomic operation.
Postmortem, Pragmatism and Priorities
The refactor that caused the bug was merged in May 2021 and shipped in v3.5.0 a month later. The first report of diverging etcd nodes arrived in December 2021. A maintainer confirmed the corruption in March 2022; the maintainers sent a public statement to the etcd and Kubernetes mailing lists, and the fix shipped in v3.5.3 in April 2022.
This wasn't the first time this class of bug surfaced. In August 2016, two months after 3.0.0 shipped, a maintainer fixed the same failure mode in a different place (#6247). A snapshot routine could save the consistent index to disk after apply had updated it, but before apply had written the matching data. Their commit message describes the 2022 bug almost exactly, and ends by noting there were other places that needed the same care, “but let us fix this first”. A 2020 bug in the auth store (#11652) was another variation on the theme. The danger was known and named for five years. Each fix patched a particular symptom. Nothing turned “the index is only saved alongside its data” into something the code enforced.
The 3.5.3 fix (#13854) finally did that. It split the in-memory value in two: an applying index for the entry currently in progress, and a consistent index updated only while holding the transaction lock, so it can reach disk only with the data it describes.
The postmortem says the maintainers were “not aware [of] any cases of users reporting a data corruption in production environment”. They took the issue seriously all the same; databases survive on being trustworthy. What’s interesting about the postmortem is that most of the issues they identified were related to people and processes rather than code. Processes around releases had grown lax. Maintainers had moved on. The automated test suite was flaky and incomplete. They reproduced the bug partly by luck, because one maintainer's /tmp was mounted to a real disk rather than memory. The fix took over a week and involved multiple attempts.
The work that followed the postmortem is what I find most instructive. They haven't undertaken a grand refactor of the EtcdServer god struct, which survives to this day, nor any of the other code smells the agent identified in its reports. I think this is because they had more important priorities. Instead, they built machinery for confidence. They reworked corruption checking. A robustness suite now injects failures and checks every run's history for linearisability. The suite’s README lists known correctness bugs, including those it has caught.
Conclusion
This post ended up being more about software engineering than Go. I think that's fine. The judgement I was hunting for is mostly language-agnostic.
Some of this journey was reassuring in a way. Even a high-profile open source project that many depend on has TODO comments in the codebase, and fossils of old implementations that haven’t been cleaned up. Even here, it took a critical bug to restore release discipline and reliable tests. There was no instant fix. It took careful, coordinated work, much of which was later automated.
I don’t want to overstate what I’ve learned. The initial raw AI reports were hard to follow and simply covered too much ground. I was out of my depth, and all I had were reams of plausible observations. The meat of this post came from a dialogue with Claude, where I tried to cut away everything but the most essential threads of etcd’s story. Without that, this post would likely never have been published.
In domains I’m paid to work in, I guide the agent as much as it guides me. While jumping into an unfamiliar domain and relying heavily on the agent is fine for an experimental piece of writing like this, I wouldn’t feel comfortable being so far out of my depth if I were building software for others. Domain expertise still matters. After reading this, you wouldn't hire me to build a new database in Go. You’d pick a database engineer who can recite Designing Data-Intensive Applications from cover to cover and has shipped Go in production. I’ve gained some second-hand wisdom, and some trivia to bore people with. More valuably, I’ve gained a lasting suspicion of any invariant that lives only in tribal knowledge. Such invariants are even more dangerous now that agents can be dropped into a complex codebase with none of that context.
Was it more useful than a toy coding project? Yes, but not by much. There are no shortcuts to expertise. Real fluency in a language still means working up from basic to advanced material until one can read unfamiliar code and run its logic in one's head, rather than relying on a maintainer's postmortem or an AI agent to explain it. That skill matters more now, not less. We're shipping more code we haven’t read, and sooner or later, even teams running fully automated software factories will need someone who can dive into an unfamiliar codebase and understand what it's doing. The scarcer that skill becomes, the more it will be worth.
So the premise I started with was only half right. Hand-written side projects haven’t entirely lost their value. They're just less impressive. They’re like low-intensity cycling: nobody’s excited by it, but it builds the cardiovascular base that makes the hard sessions possible. Digging through a codebase like etcd is the hard session. Without the base, AI was my e-bike. I covered the distance, but my legs didn’t do most of the work. That’s fine for getting somewhere, but not ideal for getting fitter.
1. I’ve chosen the word senior because “expert” is hard to pin down. For instance, managers regularly judge seniority in the industry, though the technical bar for “senior” differs by org; some levelling frameworks try to formalise it. These frameworks cover much more than technical skill, but insofar as they cover the technical side, this from the senior (L50) section of this framework is a good example: “Has the skills to proficiently work on complex and unknown problems under pressure (e.g. debugging complex production issues at speed, or leading the resolution of critical or major incidents)”. Notice it doesn’t mention writing code. This kind of skill isn't automatable yet (though with enough telemetry, an agent can help investigate production issues).
2. The first prompt, run at v0.1.0, v2.0.0, v3.0.0 and v3.7.1: “Explore this codebase and outline its public APIs, its fundamental architecture and software design choices in a Markdown document: etcd-0.1.0.md. References to source code should support all points; priority should be given to where important abstractions are declared/implemented and where they earn their place and, if relevant, where they break down.”
3. The second prompt, used to generate a summary: “Read the markdown documents in the working tree. These all outline the design and trade-offs of etcd at a particular point in time. Generate another document that traces the development of the five most important systems or abstractions over the lifetime of the project. Once one area has been chosen, the source code at the four documented versions should be double-checked and referenced. Pay attention to trade-offs, management of complexity and how abstractions break down as well as design successes.”
4. I’m not a distributed systems expert, so I’m not going to try to summarise Raft beyond saying all nodes maintain write-ahead logs from which state can be built, all writes go through a single leader, and it employs a leadership election mechanism to survive faults. For a more detailed discussion of the Raft algorithm, you can read the original paper. To get some intuition of how leadership and elections work, there’s a web-based simulation here.