There is a nice version of software architecture that lives in diagrams.
Services have names. Databases have owners. Repositories have README files. Deployments have pipelines. Dashboards have labels. Everything looks fairly reasonable.
Then somebody asks a very simple question. Why does this service retry exactly five times? Nobody knows. A few minutes later someone says to ask Daniel because he added it years ago. That sentence started bothering me.
Not because asking people is bad. Talking to a teammate is often faster than digging through 40 commits and three abandoned wiki pages. The problem was different: we had no idea how often the system depended on this kind of human lookup.
So I started logging it. Every time work stopped because the next useful piece of information existed only in another person's head, it went into the log.
No complicated tooling at first. Just timestamp, component, question, person asked, time spent and whether the answer existed somewhere else.
After a few weeks, the log became much more interesting than expected. It was basically an undocumented dependency graph of the engineering team.
The repository owners were not always the people who actually understood the system
At first I expected the results to roughly match our repository structure.
Backend team owns backend repositories. Infrastructure people know Kubernetes. Data people understand pipelines. Sounds reasonable.
Reality was messier.
One engineer who had officially moved away from infrastructure still answered most questions about our ingress setup. Another person was the unofficial expert on a payment retry mechanism despite having contributed almost no recent code to that repository.
The strangest case was a service everyone considered boring. It had tests, dashboards, deployment scripts and documentation. On paper, it looked healthy. But whenever something unusual happened, the same engineer was contacted.
Why? Because the documentation explained what the service did, but not why several weird decisions existed. One Redis key had an expiration of 47 minutes. Not 45. Not an hour. The relevant configuration looked like this.
cache: session_ttl_seconds: 2820 refresh_before_expiration_seconds: 300 retry: max_attempts: 5 initial_delay_ms: 250 multiplier: 2.0 payments: reconciliation_window_minutes: 90 allow_duplicate_pending_records: true
Every value was technically documented. None of them was really explained.
The answer about 47 minutes eventually came from a teammate who remembered an old interaction between session expiration and a third-party authentication provider.
The value was not random. It was a workaround. A perfectly valid workaround, actually. But the important architectural dependency was invisible. The diagram effectively looked like this:
service → Redis → historical incident → one engineer's memory
That last edge did not exist in any architecture documentation. Once I started looking for these edges, they were everywhere.
Have you ever opened a configuration file and thought that changing one suspicious value would probably be fine, but still asked someone before touching it?
That hesitation is data. I had just never treated it as data before.
I turned ordinary questions into a knowledge graph
After collecting entries manually for a while, the spreadsheet became annoying.
There were repeated component names, slightly different question categories, people changing teams and questions involving several systems at once.
So I wrote a small Python script.
Nothing fancy. The idea was simply to model every question as an edge between a component and the person who could unblock it.
The raw dataset looked roughly like this.
Data format: CSV
timestamp,component,category,asked_person,resolution_minutes,documented 2026-04-03T10:14:00,payment-worker,behavior,alex,12,false 2026-04-03T15:41:00,auth-gateway,configuration,maria,7,true 2026-04-04T09:22:00,event-consumer,failure-mode,alex,28,false 2026-04-04T13:08:00,billing-db,migration,kevin,19,false 2026-04-05T11:52:00,payment-worker,history,alex,31,false 2026-04-05T16:17:00,auth-gateway,behavior,maria,14,false 2026-04-06T10:02:00,event-consumer,recovery,alex,42,false 2026-04-06T14:44:00,billing-db,configuration,kevin,8,true
Then I calculated a few simple metrics.
The first actual program used for the experiment was written in Python.
from collections import defaultdict, Counter from dataclasses import dataclass from pathlib import Path import csv @dataclass class Question: component: str category: str person: str minutes: int documented: bool def load_questions(path: str) -> list[Question]: questions = [] with Path(path).open(encoding="utf-8") as file: reader = csv.DictReader(file) for row in reader: questions.append( Question( component=row["component"].strip(), category=row["category"].strip(), person=row["asked_person"].strip(), minutes=int(row["resolution_minutes"]), documented=row["documented"].lower() == "true", ) ) return questions def analyze(questions: list[Question]) -> None: questions_by_person = Counter() minutes_by_person = Counter() experts_by_component = defaultdict(Counter) undocumented_by_component = Counter() for q in questions: questions_by_person[q.person] += 1 minutes_by_person[q.person] += q.minutes experts_by_component[q.component][q.person] += 1 if not q.documented: undocumented_by_component[q.component] += 1 print("\nKnowledge dependency by person") print("-" * 60) for person, count in questions_by_person.most_common(): minutes = minutes_by_person[person] print( f"{person:15} " f"questions={count:3} " f"resolution_time={minutes:4} min" ) print("\nComponent concentration") print("-" * 60) for component, experts in experts_by_component.items(): total = sum(experts.values()) top_person, top_count = experts.most_common(1)[0] concentration = top_count / total print( f"{component:25} " f"top_expert={top_person:12} " f"dependency={concentration:.0%} " f"undocumented={undocumented_by_component[component]}" ) if __name__ == "__main__": questions = load_questions("knowledge_log.csv") analyze(questions)
The percentage at the end became the useful part. Suppose ten questions about one subsystem appeared in the log and eight of them went to the same engineer.
For my experiment, that component had an 80 percent knowledge concentration. This is not some established industry metric. I made it for this experiment because it answered the question I actually cared about.
How dependent is this component on one person's knowledge? A repository could have six active contributors and still show a knowledge concentration above 80 percent.
That distinction turned out to matter much more than commit count. Git tells you who changed the code. It does not necessarily tell you who understands what happens when that code behaves strangely at 2 AM.
Our real bus factor was not one number
The classic version of bus factor is easy to understand. How many people can disappear before a project gets into serious trouble? Useful question, but after looking at the logs, it felt too broad.
Our system did not have one bus factor. It had dozens. The authentication flow was fine. Several people understood it. Our CI pipeline was also fine. Billing migrations were slightly uncomfortable.
One event-processing pipeline was terrifying. Almost every non-trivial question about it went to one person. The code itself was not particularly complicated. That was the funny part. Around 2,000 lines of Go. A Kafka consumer. PostgreSQL. Some retry logic. Nothing that should require an archaeologist.
But the service had accumulated years of context. Why one message type was intentionally ignored. Why retries stopped after a certain database error. Why events were processed out of order in one edge case. Why a metric with a scary name was actually harmless. You could read the code and eventually understand what happened.
Understanding why it happened was different. So instead of calculating one bus factor for the engineering team, I started thinking in terms of knowledge domains.
For every important subsystem, I wanted at least two people capable of answering three things:
What happens normally? What happens when it breaks in an unusual way? Why does the current design look like this? The third question was consistently the hardest.
Code is surprisingly good at describing what. It is much worse at preserving why. Commit messages help. Pull requests help. Architecture Decision Records help.
But only when somebody writes them while the context is still fresh. Six months later, nobody wants to reconstruct why a timeout changed from 10 seconds to 17. Yes, we had one of those too.
Then I removed the most important engineer from the graph
Once the dataset became large enough, curiosity won.
What would happen if the engineer receiving the most questions suddenly became unavailable?
Vacation is the less dramatic version of the usual bus-factor scenario, so I simulated that.
For every component, I removed its most frequently consulted engineer and checked whether somebody else had ever answered a question about the same component.
The second script was also written in Python.
from collections import defaultdict, Counter import csv def load_edges(filename: str): edges = defaultdict(Counter) with open(filename, newline="", encoding="utf-8") as file: reader = csv.DictReader(file) for row in reader: component = row["component"].strip() person = row["asked_person"].strip() edges[component][person] += 1 return edges def simulate_expert_loss(edges): results = [] for component, people in edges.items(): total_questions = sum(people.values()) primary_person, primary_questions = people.most_common(1)[0] remaining_people = { person: count for person, count in people.items() if person != primary_person } remaining_answers = sum(remaining_people.values()) dependency_ratio = primary_questions / total_questions if remaining_answers == 0: status = "critical" elif dependency_ratio >= 0.75: status = "high-risk" elif dependency_ratio >= 0.50: status = "fragile" else: status = "distributed" results.append( { "component": component, "primary_person": primary_person, "dependency_ratio": dependency_ratio, "backup_people": len(remaining_people), "status": status, } ) return sorted( results, key=lambda item: item["dependency_ratio"], reverse=True, ) def print_report(results): print( f"{'COMPONENT':28} " f"{'STATUS':12} " f"{'DEPENDENCY':12} " f"{'PRIMARY':14} " f"{'BACKUPS'}" ) print("-" * 85) for item in results: percentage = item["dependency_ratio"] * 100 print( f"{item['component']:28} " f"{item['status']:12} " f"{percentage:10.1f}% " f"{item['primary_person']:14} " f"{item['backup_people']}" ) if __name__ == "__main__": graph = load_edges("knowledge_log.csv") report = simulate_expert_loss(graph) print_report(report)
One subsystem immediately landed in critical.
There was literally no second person in the dataset who had answered a meaningful question about it.
Another looked healthy because four people had contributed code recently, but 82 percent of operational questions still went to one engineer.
That was the moment the experiment stopped feeling like a curiosity.
This was a reliability problem.
We spent plenty of time discussing database replication, redundant infrastructure, multi-zone deployments and failover strategies.
Meanwhile, some parts of the system had human single points of failure.
Imagine running three database replicas while keeping the explanation of a critical recovery procedure in one person's memory.
Technically redundant.
Operationally, not so much.
Documentation helped, but not in the way I expected
The obvious reaction was to document everything. That lasted about two days. Writing documentation for every question creates another problem: eventually you have a landfill of tiny documents nobody can find. Instead, I started separating questions into types.
Simple lookup questions were not especially dangerous. Where is the dashboard? Which queue does this worker consume? What environment variable enables debug logging? Those can usually be fixed with README files, links, service catalogs and better naming. Historical questions were more interesting. Why do we disable retries for this error? Why is this deployment intentionally sequential? Why does this worker sleep before acknowledging a message? Those deserved short decision records.
The third category was operational intuition. How do you know whether this spike is dangerous? When should we restart the consumer instead of waiting? Which alert usually means something else is broken? Those were difficult to document because experienced engineers often answered them with the most irritating phrase in software development.
It depends. Annoying answer. Often correct. For those cases, pairing worked better than documentation.
Someone unfamiliar with the subsystem would handle the next incident while the expert watched.
Much slower. Also much more useful. A runbook can tell somebody to execute a command. It cannot automatically teach them why executing that command is safe. That difference became painfully obvious during the experiment.
The weirdest metric turned out to be question latency
Eventually another number became interesting. Not how many questions were asked. How long people waited before asking them. Some developers asked for help after ten minutes. Others spent two hours digging through logs before messaging the person they already suspected knew the answer. Initially I considered the second behavior better.
Independent debugging. Good engineering. Then I looked closer. A surprising amount of that investigation was duplicated work. Someone would spend 80 minutes rediscovering a historical constraint that another engineer could explain in two sentences.
This changed how I thought about knowledge accessibility. A healthy system is not one where nobody asks questions. That would probably mean nobody talks. A healthier system is one where asking a person is optional rather than required. If reading the code takes fifteen minutes and asking Sarah takes two, asking Sarah is perfectly reasonable. If Sarah is the only possible path to the answer, that is different. That distinction became the main test. Could somebody reasonably reconstruct this answer without contacting one specific person? If yes, fine. If no, we had a knowledge dependency worth fixing.
What actually changed after the experiment
There was no giant documentation sprint. No huge knowledge-transfer meeting. No heroic rewrite.
Those solutions sounded expensive and, more importantly, temporary. Instead, whenever the same kind of question appeared twice, we tried to remove the reason for the third one.
Sometimes that meant adding one paragraph to a README. Sometimes renaming a configuration value. Sometimes linking a dashboard directly from an alert. Sometimes writing a tiny ADR. And sometimes the best solution was changing the code so the weird behavior stopped being weird.
That last one became my favorite. Documentation explaining surprising code is useful. Code that no longer surprises anyone is better.
We also started informally rotating ownership of smaller incidents. Not every production issue needed the person who knew the subsystem best. It felt inefficient at first. An incident that one engineer could solve in ten minutes occasionally took thirty. But a few weeks later, questions started spreading across more people. That seemed like a reasonable trade. Maybe the most useful lesson was that bus factor is not really about buses. It is about reconstructability. Can another engineer rebuild enough context to make a safe decision? Can they understand why a strange constraint exists? Can they recover the system without calling someone who happens to be asleep, hiking, sick or simply working somewhere else now? Those questions turned out to be much more useful than counting contributors.
A small experiment worth trying
If you work on a system that has existed for more than a year, try logging these interruptions for a couple of weeks. Do not build a platorm for it. A CSV file is enough. Record the component, question, person who answered it, resolution time and whether the answer could have been reconstructed from existing information. Then look for concentration. You may discover that the most important engineer for a subsystem is not its official owner. You may find that a repository with ten contributors effectively depends on one person. Or maybe your system is healthier than expected. That would be a pretty nice result too. The experiment changed one habit for me. Whenever someone explains an obscure part of the system now, there is another question immediately afterward:
Could the next person figure this out without asking us? If the answer is no, the conversation probably uncovered something worth fixing. Sometimes the fix is three lines of documentation. Sometimes it is a comment. Sometimes it is deleting a 47-minute timeout that nobody needs anymore. Those are surprisingly satisfying pull requests.