Thoughtless Engineering: When Technical Activity Becomes Detached from Engineering Judgment
I was recently interviewing a candidate for a full-stack software engineering position when I noticed something that has stayed with me. It was not one bad answer, one missed concept, or one weak moment in an otherwise ordinary technical conversation. It was a pattern. The candidate could speak in the vocabulary of software engineering — test coverage, services, scalability, language choice, logs, integration tests — but when pressed to explain the reasoning behind those words, the substance disappeared.
That is what made the interview memorable.
On the candidate’s resume was a bullet point that initially sounded impressive: they had “boosted test coverage across our AWS Lambda functions from 15% to 84%, significantly improving code reliability and maintainability.” At first glance, this looks like exactly the kind of accomplishment one might want to see from a software engineer. It names a concrete metric. It suggests ownership. It implies concern for quality. It frames testing as a contributor to reliability and maintainability. But as soon as I asked basic follow-up questions, the claim became less clear.
What did “across our Lambda functions” mean? Was 84% an average across all Lambdas? Was each Lambda brought up to 84%? Was this line coverage, branch coverage, function coverage, or something else? How many Lambda functions were there? How large were they? Were they thin wrappers around AWS calls, or did they contain meaningful business logic? Which parts of the system were newly tested? What kinds of defects did the new tests protect against? Why was 84% the right place to stop? These are not nitpicks. They are the questions that turn a metric into an engineering claim.
After clarification, I learned that the candidate was referring to average coverage across seven Lambda functions. That helped establish scope, but it did not answer the more important question: why did the increase matter? If someone claims that moving from 15% to 84% coverage significantly improved reliability and maintainability, I expect them to explain the mechanism. What risk was reduced? What behavior was now protected? What critical paths were exercised? What kinds of regressions had become less likely? Which parts remained uncovered, and why were they acceptable?
Instead, when I asked why not continue toward 100%, the candidate gave an answer that revealed the deeper problem. He said that 100% coverage does not matter because coverage can be gamed. He said extra tests make the codebase more complicated for the next developer. He said that if there are usually four input variations, he tests those and does not worry much about the others.
There are fragments of truth in that answer. It is true that 100% coverage can be gamed. It is true that coverage alone does not prove correctness. It is true that bad tests can become clutter. It is true that not every theoretical input deserves equal testing effort. But those truths were not organized into a mature testing philosophy. They were used as exits from reasoning.
The issue was not that the candidate rejected 100% coverage. A strong engineer might reasonably reject 100% coverage as a blanket goal. The issue was that he had no defensible stopping rule. He wanted to use the movement from 15% to 84% as evidence of improved reliability, while simultaneously dismissing the significance of higher coverage without explaining why the marginal value stopped at 84%. That is not engineering analysis. That is using a number when it is convenient and rejecting the same kind of number when it becomes inconvenient.
This interview became a useful case study for a broader failure mode I have started calling Thoughtless Engineering.
By Thoughtless Engineering, I do not mean ordinary mistakes, lack of experience, or unfamiliarity with a particular tool. I also do not mean that every engineer must produce a dissertation before making a practical decision. Engineering often requires heuristics, judgment under uncertainty, and imperfect trade-offs. Rules of thumb are useful. Experience matters. Speed matters.
The problem begins when heuristics become slogans, when slogans replace analysis, and when technical vocabulary is mistaken for technical judgment.
Thoughtless Engineering is what happens when engineering activity becomes detached from engineering reasoning. It is the habit of jumping from a vague problem to a familiar implementation choice without walking through requirements, constraints, failure modes, alternatives, trade-offs, and lifecycle consequences. It is the tendency to say “use Go” before identifying the bottleneck, to say “we have 84% coverage” before explaining what behavior is actually protected, to say “service boundary” without defining the contract, to say “just look at the logs” without discussing observability, or to dismiss edge cases without understanding their potential system-level impact.
The common problem is not the specific word being used. Go can be a good language choice. Python can be a good language choice. Microservices can be appropriate. A monolith can be appropriate. High test coverage can be valuable. Less-than-perfect coverage can be entirely defensible. Logs are useful. Integration tests are necessary. But none of these things are self-justifying. A programming language is not an architecture. A coverage percentage is not a correctness model. A service boundary is not a contract. A log file is not an observability strategy. A common-case input set is not a risk analysis.
What distinguishes thoughtful engineering is the ability to connect these artifacts to the system being built. A thoughtful engineer can explain why a metric matters, why a boundary exists, why a test strategy is sufficient, why a language is appropriate, why a risk is acceptable, and what would cause that judgment to change. They reason from problem to solution. They can move between local implementation details and system-level consequences. They understand that software does not run in a vacuum; it operates inside dependency graphs, organizational constraints, operational environments, and long-lived maintenance realities. The candidate’s answers bothered me because they repeatedly skipped that reasoning.
The testing conversation was only the first example. Later, when discussing scalability for a FastAPI backend, React frontend, database, containerized deployment, and eventual OpenShift environment, the candidate immediately jumped to the claim that if the application had many users, we would not use Python and would instead use Go. Again, there is a plausible version of that argument. Go has strengths. Python has limitations. Runtime characteristics matter. Concurrency models matter. Latency and throughput matter.
But “many users” is not enough information to justify a rewrite in another language. What is the workload? What is the bottleneck? Is the system CPU-bound, I/O-bound, memory-bound, database-bound, network-bound, or dominated by model inference time? What are the latency requirements? What is the SLA? Are requests synchronous or asynchronous? Can the service scale horizontally? Would caching help? Would a queue help? Would separating model execution from request routing help? Would database tuning help? Would the migration cost and new defect risk outweigh the performance gains? Without those questions, “use Go” is not a scalability strategy. It is a reflex.
The same pattern appeared around service boundaries and microservices. Terms like “service boundary” were invoked as if decomposition were inherently mature. But service boundaries are not good merely because they exist. A bad boundary can increase coupling, multiply failure modes, complicate observability, and turn a simple system into a distributed mess. A meaningful service boundary should follow from responsibility, cohesion, contracts, protocols, ownership, failure isolation, and the way the larger system composes its capabilities. Again, the vocabulary was present. The reasoning was not.
That is the central contention of this essay: Thoughtless Engineering is not a failure to know enough technical nouns. It is a failure to form technical arguments.
This matters because software systems are not merely collections of code. They are systems of behavior. Their reliability, performance, maintainability, and evolvability are produced by interactions among components, people, tools, requirements, infrastructure, and time. A local decision that seems harmless can create system-level fragility. A rare edge case can trigger cascading failure. A poorly chosen abstraction can make future change expensive. A vague service boundary can turn into years of operational confusion. A shallow metric can create the illusion of rigor while concealing the absence of understanding.
The goal of this essay is not to ridicule one candidate. The interview is simply the entry point. The larger goal is to use that experience as a case study in what not to do: how not to talk about testing, how not to reason about scalability, how not to invoke microservices, how not to use heuristics, and how not to mistake implementation details for engineering thought.
More importantly, I want to describe the positive standard: what thoughtful engineering looks like.
Thoughtful engineering begins with the problem, not the tool. It asks what the system is supposed to do, what constraints it operates under, what failures matter, what risks are acceptable, what trade-offs exist, and how a decision will age over the lifecycle of the system. It understands that testing is not about line execution alone, but about behavioral correctness. It understands that edge cases are not automatically irrelevant just because they are rare. It understands that scalability is not a language choice. It understands that microservices are not small programs, but bounded systems organized around coherent responsibilities. It understands that observability is not the presence of logs, but the system’s ability to explain itself under stress.
In short, thoughtful engineering puts the thinking back into engineering. That is the standard I was looking for in the interview. And that is the standard the candidate failed to meet.
The First Symptom: Ambiguous Metrics and Unsupported Claims
The first sign of trouble was not the candidate’s opinion about test coverage. It was the ambiguity of the claim itself.
The resume bullet said that he had “boosted test coverage across our AWS Lambda functions from 15% to 84%, significantly improving code reliability and maintainability.” That is the kind of sentence that looks strong on a resume because it has the appearance of precision. It contains numbers. It names a technical practice. It suggests measurable improvement. It connects the work to desirable engineering outcomes: reliability and maintainability.
But a sentence can look precise while still being vague. “Boosted test coverage from 15% to 84%” sounds concrete, but the moment you ask what the number refers to, the precision starts to dissolve. Does 84% mean average coverage across all Lambda functions? Does it mean each Lambda was individually brought up to at least 84%? Does it mean aggregate coverage after combining all the functions into one measurement? Was the 84% weighted by lines of code, or was it a simple average across functions regardless of size? Was it line coverage, branch coverage, statement coverage, function coverage, or some blended metric from a test tool? Were generated files excluded? Were infrastructure wrappers included? Were only unit tests counted, or were integration tests included in the coverage number? These questions matter because the same number can mean very different things depending on how it was produced.
Suppose there are seven Lambda functions. One Lambda contains 2,000 lines of domain logic, input validation, branching behavior, and state transitions. The other six are thin wrappers that each contain a few dozen lines of glue code. If someone says the “average coverage” across the seven functions is 84%, that might conceal the fact that the six trivial Lambdas are well covered while the important one remains largely untested. Conversely, if the critical Lambda is thoroughly tested and the uncovered code is mostly thin framework glue, the same number might understate the value of the work. Without knowing the distribution, the metric is hard to interpret. This is why ambiguity in a technical claim is not a minor communication flaw. It prevents evaluation.
When an engineer presents a metric, especially in an interview, they are not merely reporting a number. They are asking the listener to accept a conclusion. In this case, the conclusion was that the work “significantly improved code reliability and maintainability.” That is a much stronger claim than “I wrote tests.” It asserts that the tests had meaningful engineering value. It implies that the system became safer to change, less likely to regress, and easier to maintain. But the number alone does not establish that.
Coverage is a proxy. It is not the thing itself. It is a measurement that may or may not correlate with the engineering property we actually care about. The thing we care about is not whether lines were executed during a test run. The thing we care about is whether important behavior is specified, exercised, and protected against regression. We care whether the tests meaningfully constrain the program. We care whether they clarify intended behavior. We care whether they make future modification safer. We care whether they expose defects before production does.
A test suite can execute a large percentage of the code while proving very little. It can call functions without asserting meaningful outcomes. It can test happy paths while ignoring failure modes. It can cover lines but miss branches. It can assert that a function returns “something” without checking whether the returned value is correct. It can mock away the very behavior that matters. It can encode the current implementation so tightly that it becomes hostile to refactoring. It can be high-coverage and low-value.
At the same time, a lower-coverage test suite can sometimes be quite valuable if it targets the right behavior: core invariants, high-risk paths, historically fragile areas, business-critical transformations, boundary conditions, failure handling, and integration seams. This is not an argument against coverage. Coverage is useful. But coverage is useful as a diagnostic, not as a conclusion.
That was the first conceptual issue I wanted to see the candidate navigate. A thoughtful engineer should be able to say something like:
“The 84% figure was average line coverage across seven Lambda functions. The number mattered because the previous tests barely touched the core business logic. Most of the improvement came from adding tests around validation, request normalization, error handling, and the main decision paths. The remaining uncovered code was mostly logging, thin AWS adapter code, and a few low-risk branches that we decided were not worth testing directly.”
That kind of answer would not require perfection. It would not require the candidate to have achieved 100% coverage. It would not require a rigid philosophy. But it would show that the candidate understood the relationship between the metric and the underlying engineering claim.
What I heard instead was much thinner. After I asked for clarification, the candidate explained that the number was an average across seven Lambda functions. That answered one question, but it did not answer the more important one. Average of what? Across code of what kind? Testing which behaviors? Protecting against which risks? Improving maintainability in what way?
The word “average” itself can be dangerous in engineering conversations because it often hides the shape of the distribution. Average coverage across seven functions tells me very little unless I know whether the coverage is evenly distributed, whether the uncovered portions are low-risk or high-risk, and whether the tested portions correspond to meaningful behavior.
Averages often create the illusion of understanding. They compress variation into a single number. Sometimes that compression is useful. Other times it hides the only facts that matter. If one Lambda is responsible for authorization decisions and another only formats a response, their coverage percentages should not have equal interpretive weight. If one function contains the code most likely to cause a production incident, that function matters more than the numerical average. If one function sits on a critical path for downstream systems, its untested behavior may matter more than several fully covered auxiliary functions. This is why mature engineering requires more than measurement. It requires judgment about what the measurement means.
The same problem applies to the phrase “significantly improving code reliability and maintainability.” That is a strong causal claim. It says the test coverage increase caused an improvement in reliability and maintainability. But how was that established? Did defect rates go down? Did deployment confidence improve? Did the team catch regressions before production? Did the tests document behavior that had previously been implicit? Did the test suite enable refactoring? Did onboarding become easier? Were incidents reduced? Were high-risk paths now protected?
There are many ways the claim could be true. But the candidate needed to explain at least one of them. Reliability and maintainability are not magic properties that appear when a coverage number crosses some threshold. Reliability comes from the system behaving correctly under the conditions that matter. Maintainability comes from future engineers being able to understand, modify, and validate the system without unreasonable risk. Tests can support both of these properties, but only if they are designed around meaningful behavior and organized in a way that clarifies the system.
For example, tests can improve reliability by exercising known failure modes: invalid input, missing fields, malformed upstream responses, timeout behavior, retry logic, idempotency, authorization failures, duplicate requests, and boundary conditions. They can improve maintainability by making the intended behavior explicit, preserving invariants, and allowing future developers to refactor with confidence. But simply increasing coverage does not tell me that any of this happened.
This is where resume metrics can become misleading. They compress a story into a number and then rely on the reader to supply the missing reasoning. A candidate says “15% to 84%,” and the listener is supposed to infer discipline, rigor, and improved quality. But in an interview, I am not only evaluating whether the candidate did the work. I am evaluating whether they understood the work.
There is a difference between participating in an improvement and being able to explain why it mattered.
An engineer might be assigned a task to raise coverage, write a number of tests, satisfy a team quality gate, and report the new percentage. That is useful work. But if the engineer cannot explain what kinds of behavior the tests protect, why the chosen tests were valuable, what risks remained, or how the metric relates to reliability, then I do not yet know whether they exercised engineering judgment. They may have completed a task without understanding the deeper rationale behind it.
That distinction matters in hiring. A resume bullet is an invitation to ask for the reasoning behind the accomplishment. If a candidate chooses to highlight test coverage as evidence of quality, they should be prepared to discuss testing philosophy. If they claim improved reliability, they should be prepared to discuss failure modes. If they claim maintainability, they should be prepared to discuss how the tests made the code easier to understand or change. If they cite a percentage, they should be prepared to explain what the percentage does and does not mean.
The ambiguity of the original claim therefore revealed more than imprecise wording. It revealed a possible gap between the appearance of rigor and the reality of rigor. This is one of the recurring features of Thoughtless Engineering: it often looks disciplined at first glance. It has numbers. It has tools. It has processes. It has familiar phrases. It has the outward form of engineering. But when you ask how the number connects to the system, or how the tool connects to the requirement, or how the process connects to risk reduction, the explanation is missing. The artifact is present. The reasoning is absent.
That is why I do not treat the ambiguity as nitpicking. Ambiguity is often the first symptom of unclear thought. If an engineer cannot clearly state what was improved, how it was measured, and why the improvement mattered, then the metric is not functioning as evidence. It is functioning as decoration.
In this case, the candidate’s claim had three unsupported layers. First, the measurement was underspecified. I did not know what kind of coverage was being measured, how it was aggregated, or how it was distributed across the seven Lambdas. Second, the scope was unclear. I did not know whether the Lambdas contained critical logic, simple orchestration, AWS glue code, or a mixture of all three. Third, the causal claim was unproven. I did not know how the increase in coverage improved reliability or maintainability, because the candidate did not connect the tests to specific behaviors, risks, defects, or maintenance outcomes. Each of those gaps matters independently. Together, they turn an impressive-sounding bullet into a weak engineering claim.
A better candidate would have treated the resume bullet as the beginning of a technical explanation, not the end of one. They would have unpacked the metric. They would have described the original problem. They would have explained what was risky about the previous state of the codebase. They would have described the testing strategy used to improve it. They would have acknowledged the limitations of coverage as a metric. They would have explained why the final number was sufficient relative to the system’s risk profile. Most importantly, they would have shown that they understood coverage as one input into a broader theory of confidence.
That is the educational point here. Metrics are not bad. Engineering organizations need metrics. Coverage, latency, error rates, throughput, availability, defect counts, incident frequency, deployment frequency, and mean time to recovery can all be useful. But metrics are dangerous when they are detached from interpretation. A metric should provoke questions, not end them. What does it measure? What does it omit? How can it be gamed? What distribution does it hide? What behavior does it illuminate? What decision does it inform? What would we do differently if the number moved? What other evidence do we need before drawing a conclusion? Those are engineering questions. Without them, metrics become a kind of theater.
This is why the test coverage claim was the first symptom. It showed an engineer using a metric as a badge of quality without being able to articulate the quality model behind it. The candidate wanted the number to carry the argument. But numbers do not carry arguments by themselves. They need context, interpretation, and judgment.
A coverage percentage can tell us that code was executed during tests. It cannot, by itself, tell us that the system is reliable. It cannot tell us that the tests assert the right behavior. It cannot tell us that the uncovered code is low-risk. It cannot tell us that future developers will find the code easier to change. It cannot tell us that rare but dangerous failure modes have been considered. It cannot tell us that the system is now meaningfully safer.
For that, I need the engineer. I need the engineer to explain what the number means. I need them to connect the metric to the architecture, the domain, the risks, the failure modes, and the maintenance burden. I need them to distinguish between executing code and validating behavior. I need them to understand that a metric is not a substitute for judgment. The candidate did not do that. And that was the first symptom of the larger disease.
The Deeper Problem: No Defensible Stopping Rule
The ambiguity of the metric was the first symptom. The deeper problem appeared when I asked a simple follow-up question: why stop at 84%?
This was not meant as a demand for 100% coverage. I was not looking for a candidate to say that every line of every Lambda function must be covered by tests before code can be considered acceptable. In fact, I would have been skeptical of that answer too. A rigid obsession with 100% coverage can be just as thoughtless as dismissing coverage altogether. The point of the question was not to defend a particular number. The point was to see whether the candidate had a principle.
If 84% coverage was meaningful, why was it meaningful? If increasing coverage from 15% to 84% improved reliability and maintainability, why did the improvement stop being worth pursuing after 84%? What changed between the low-coverage state and the high-coverage state? What remained uncovered? Was the remaining 16% low-risk? Was it unreachable code, logging, boilerplate, framework glue, or thin AWS integration code? Or did it include meaningful branches, error paths, edge cases, and domain logic?
These are the questions that determine whether a stopping point is defensible. A strong engineer does not need to believe that 100% coverage is always necessary. But they should be able to explain why additional testing has diminishing value in a particular context. They should be able to say something about risk, cost, criticality, code shape, and the kinds of behavior already covered. They should be able to distinguish between code that is uncovered but irrelevant, code that is uncovered but low-risk, and code that is uncovered because nobody bothered to think hard about it. The candidate did not do that.
Instead, he said that 100% coverage does not matter because coverage can be gamed. He said there is no point chasing it. He said extra tests make the codebase more complicated for the next developer. He said that if there are normally four different input variations, he tests those and does not worry about the others. Again, there are partial truths embedded in that answer.
It is true that 100% coverage can be gamed. A test can execute a line of code without meaningfully validating behavior. A test can call a function and assert nothing important. A test can lock in implementation details while ignoring the actual contract. A test can exist merely to satisfy a tool. Anyone who has spent time in real codebases has seen high-coverage test suites that inspire little confidence.
It is also true that tests have maintenance costs. Tests must be read, updated, organized, debugged, and occasionally deleted. A poorly designed test suite can become brittle. It can make refactoring harder. It can force developers to preserve incidental details that should have been allowed to change. Tests are not free. It is also true that common cases matter. If a system regularly receives four primary classes of input, those cases should almost certainly be tested. A test strategy that ignores ordinary usage is not serious.
So the issue is not that the candidate said false things. The issue is that he used true things in an unprincipled way.
- “Coverage can be gamed” is not an argument against testing. It is an argument for better tests.
- “Tests have maintenance cost” is not an argument against additional tests. It is an argument for evaluating the marginal value of each test.
- “Common inputs matter” is not an argument for ignoring uncommon inputs. It is an argument for understanding the input space.
A mature answer would not have stopped at slogans. It would have turned those partial truths into a framework. For example, the candidate could have said:
“I do not think 100% coverage should be a default goal because coverage is only a proxy for confidence. In this case, we stopped at 84% because we had covered the core business logic, validation paths, and known failure modes. The remaining uncovered code was mostly logging, adapter code, and branches that were either not reachable under normal configuration or had low risk relative to the cost of testing them directly. We reviewed the uncovered portions and made an explicit decision that additional tests would not materially improve confidence.”
That answer would have been reasonable. I might still ask follow-up questions, but I would understand the principle. The candidate would be showing me that he does not worship coverage, but he also does not dismiss it. He would be showing that he understands coverage as a tool for confidence, not as a trophy. What he gave instead was not a framework. It was a set of evasions.
The most important missing element was a stopping rule. A stopping rule is the reasoning that tells you when enough work has been done for a particular objective. In testing, a stopping rule does not have to be a universal number. It can be based on risk. It can be based on code criticality. It can be based on coverage of behavioral classes. It can be based on failure modes. It can be based on historical defect patterns. It can be based on regulatory constraints, safety concerns, or operational consequences. It can even be based on time and budget, as long as the trade-off is acknowledged honestly.
But there has to be some principle that explains why stopping is rational. Without a stopping rule, the number 84% is arbitrary. It may be better than 15%, but we do not know why. It may represent a meaningful increase in confidence, or it may represent the point at which someone got tired of writing tests. It may reflect thoughtful risk reduction, or it may reflect a team quality gate. It may mean the important behavior is protected, or it may mean the easy code was covered and the hard code was left alone. The percentage alone cannot tell us.
This is why the candidate’s answer was so weak. He wanted the increase from 15% to 84% to be treated as evidence of engineering rigor, but he could not explain what made 84% sufficient. He wanted to claim the benefit of the metric without accepting the burden of interpreting the metric. That is a recurring pattern in Thoughtless Engineering.
A number is invoked when it creates credibility. Then, when the number is questioned, the engineer retreats into the claim that numbers do not really matter. But that is not acceptable. If the number does not matter, then it should not be used as evidence. If the number does matter, then the engineer should be able to explain how it matters and where its usefulness ends. You cannot have it both ways.
If 15% to 84% is meaningful, then the logic of the improvement must be explainable. If 100% is unnecessary, then the logic of stopping short must also be explainable. The candidate’s answer failed because it treated the upward movement in coverage as self-evidently good while treating further upward movement as self-evidently wasteful. But both claims require justification. That is the inconsistency.
The jump from 15% to 84% was presented as a serious improvement. The jump from 84% to anything higher was dismissed as pointless. But what distinguishes one from the other? Why was the first increase valuable and the second wasteful? At what point did the marginal value change? What evidence supported that judgment?
A thoughtful engineer should be able to reason about that transition. They might say that early tests covered previously untested core behavior, while later tests would only exercise trivial code. They might say that the risk curve flattened after certain paths were covered. They might say that branch coverage revealed all meaningful decision points had been exercised. They might say that mutation testing, bug history, or code review gave additional confidence. They might say that the uncovered code was better validated through integration tests than unit tests.
Any of those answers would show an attempt to reason. But simply saying “100% can be gamed” does not answer the question. It changes the subject.
The same problem appeared in his comments about additional tests making the codebase more complicated. This is the kind of claim that sounds pragmatic, but becomes shallow under pressure.
Of course tests can make a codebase more complicated. Anything added to a codebase can make it more complicated. Production code can make a codebase more complicated. Configuration can make a codebase more complicated. Documentation can become stale and confusing. Abstractions can become excessive. Dependency injection can become ceremony. Logging can become noise. None of that tells us whether a particular addition is justified. The relevant question is not whether tests add volume. The relevant question is whether they add more clarity than cost.
A good test suite can make a system easier to understand because it shows what the system promises to do. It gives concrete examples of behavior. It documents edge conditions. It preserves invariants. It tells the next engineer what must remain true after a change. In that sense, tests are not merely extra code. They are part of the system’s executable specification.
Bad tests do the opposite. They obscure intent. They duplicate implementation details. They fail for irrelevant reasons. They make small changes expensive. They create fear without confidence. The distinction is not “more tests versus fewer tests.” The distinction is “better tests versus worse tests.”
That is why the candidate’s statement did not follow. More test functions do not automatically make a codebase harder to reason about. Poorly structured tests can. Redundant tests can. Over-mocked tests can. Brittle tests can. Tests that assert implementation details instead of behavior can. But well-designed tests often reduce cognitive burden by making the intended behavior explicit.
If a candidate tells me they avoided writing tests because they did not want future developers to read “a bunch of test functions,” I hear someone who may not understand the communicative role of tests. Future developers are not helped by the absence of tests if the result is a system whose intended behavior is implicit, fragile, and undocumented. They are helped by tests that explain the behavioral contract clearly enough that they can modify the code without guessing.
The candidate’s answer also revealed a narrow view of input variation. He said that if there are normally four input variations, he tests those and does not worry about the others. Again, common variants matter. No serious test strategy ignores the common case. But an engineer should be able to explain why those four variants are representative. Are they equivalence classes? Are they derived from domain analysis? Are they based on production telemetry? Are they the result of schema constraints? Are they the only valid inputs? Are other inputs impossible by construction? Are they rare but harmless? Rare but catastrophic? Invalid but likely? Valid but unusual? Without that reasoning, “four normal inputs” is not a testing strategy. It is an anecdote.
A mature engineer does not need to test every possible input. But they do need to reason about the input space. They should understand equivalence classes, boundary conditions, invalid inputs, malformed inputs, missing values, nulls, empty collections, duplicates, maximum sizes, encoding issues, timing conditions, and dependency failures. They should be able to distinguish between cases that are unimportant because they are truly outside the system’s contract and cases that are uncommon but still valid. This matters because uncommon does not mean irrelevant.
Some cases are rare because they are impossible under the system’s design. Those may not need direct testing. Some cases are rare because they are prevented by upstream validation. Those may need contract tests or boundary tests. Some cases are rare because they occur only under load, dependency failure, or unusual user behavior. Those may be exactly the cases that cause production incidents. Some cases are rare but high-impact. Those may deserve more attention than common low-impact cases. This is the difference between frequency and risk.
The candidate spoke as if the normality of an input determined its importance. But importance is not just a function of frequency. It is also a function of consequence. A rare case that causes silent data corruption, authorization bypass, duplicate side effects, downstream failure, or cascading retries may matter far more than a common case whose behavior is simple and harmless.
That is why “we test the usual four” is not enough. The engineer must explain what the unusual cases mean. Are they outside the contract? Are they rejected? Are they normalized? Are they handled as degraded behavior? Are they logged and surfaced? Are they retried? Are they impossible by type, schema, or validation? Are they pushed into integration tests? Are they monitored in production? These are the questions that separate thoughtful testing from casual sampling.
The deeper issue, then, was not only that the candidate stopped at 84%. It was that he did not appear to understand what would make any stopping point rational. He had no visible model of test sufficiency.
Test sufficiency is not the same as test quantity. It is about whether the tests provide enough confidence relative to the risks of the system. That confidence may come from unit tests, integration tests, property-based tests, contract tests, static typing, schema validation, code review, runtime checks, observability, canary releases, or production telemetry. Testing is one part of a larger assurance strategy.
But when a candidate claims that coverage improved reliability, I expect them to speak in those terms. I expect them to explain not only what they tested, but why those tests were the right ones. I expect some awareness of the trade-off between confidence and cost. I expect some distinction between critical behavior and incidental code. I expect some model of what was left uncovered and why. The candidate did not provide that.
Instead, his answer gave the impression that 84% was simply where the effort stopped, and that any request to justify the remainder could be deflected by saying coverage can be gamed. That is not enough, especially in an environment where correctness matters.
In my own work, especially in contexts where the consequences of error are serious, I do not expect engineers to be absolutists. I expect them to be principled. I do not need them to say “always test everything.” I need them to say, “Here is how I decide what must be tested, what can be left alone, and what other controls give me confidence.”
That is the difference between pragmatism and laziness. Pragmatism is not the refusal to do more work. Pragmatism is disciplined trade-off reasoning under constraints. It understands the ideal, the cost of approaching the ideal, the risk of stopping short, and the available alternatives. Laziness often borrows the language of pragmatism while skipping the analysis.
The candidate’s answer sounded pragmatic on the surface: do not chase 100%, do not burden future developers, focus on common inputs. But without a risk model, without a stopping rule, and without an explanation of what the tests actually protected, the answer was not mature pragmatism. It was a rationalization.
That distinction is important. Engineering is full of trade-offs. We rarely get perfect information, perfect designs, perfect tests, or perfect reliability. We make decisions under time pressure, budget pressure, organizational pressure, and uncertainty. But trade-offs are only meaningful when the things being traded are named. What are we gaining? What are we giving up? What risk remains? Who bears that risk? What evidence do we have? What assumptions are we making? What would cause us to revisit the decision? A candidate who can answer those questions is thinking like an engineer. A candidate who says “100% can be gamed” and stops there is not.
This is why I found the answer so concerning. It was not merely that he failed to mention a testing technique. It was not that he forgot a term like branch coverage, mocks, fixtures, or equivalence partitioning. Those gaps can be taught. The more troubling issue was the absence of a reasoning structure. The candidate did not seem to understand that the burden of an engineering claim is not satisfied by naming a metric or dismissing an extreme. The burden is satisfied by explanation.
Why this number? Why these tests? Why these inputs? Why this residual risk? Why this stopping point? Why is the remaining untested behavior acceptable? Why does this improve maintainability? Why should the next engineer trust this suite? Those were the questions the candidate needed to answer. He did not.
And that is why the stopping point mattered so much. The number 84% was not the problem. The absence of a defensible explanation was the problem. In a stronger candidate’s hands, 84% could have represented careful, risk-based judgment. In this interview, it felt like a number placed on a resume because it looked good.
That is Thoughtless Engineering in miniature: a visible artifact without the reasoning that gives it meaning.
Tests as Executable Specifications, Not Clutter
One of the candidate’s most revealing comments was that he did not want the next developer to come in after him and have to read through “a bunch of test functions.” In his view, more tests meant more complexity. More test functions meant more code to inspect, more files to navigate, and more material for a future maintainer to understand.
At a superficial level, I understand the concern. Tests are code. They can accumulate. They can be written badly. They can become redundant, brittle, slow, confusing, or overfit to implementation details. A large test suite is not automatically a good test suite. I have seen tests that made refactoring harder rather than easier. I have seen tests that failed for irrelevant reasons, mocked away the important behavior, asserted incidental details, or existed only because someone was trying to satisfy a coverage gate. So the concern is not entirely baseless. But the conclusion does not follow.
The fact that bad tests can create clutter does not mean that tests, as such, are clutter. The fact that some tests are poorly organized does not mean that well-designed tests make a system harder to maintain. The fact that test code must be read does not mean that reading it is wasted effort. Often, the opposite is true. In a healthy codebase, tests are one of the best places to understand what the system is supposed to do. That is the distinction the candidate missed.
Good tests are not merely extra functions attached to the side of the codebase. They are executable specifications. They describe the expected behavior of the system in a form that can be run, checked, and preserved over time. They show future engineers what the code is meant to do, not merely what it happens to do today. They encode assumptions. They protect invariants. They mark edge conditions. They preserve lessons learned from past bugs. They give a developer confidence that a change has not silently broken behavior someone else depended on. In that sense, tests are not just a quality mechanism. They are a communication mechanism.
A future developer reading a well-structured test should be able to learn something important about the system. They should be able to see examples of valid input, invalid input, expected output, boundary behavior, failure behavior, and domain assumptions. They should be able to infer what the original engineer considered important enough to protect. They should be able to distinguish intended behavior from accidental behavior. They should be able to make a change and quickly discover whether they violated the system’s contract.
That is not clutter. That is institutional memory. Production code tells you how the system currently behaves. Tests tell you which behavior is supposed to be preserved.
That difference matters. A codebase without good tests may appear simpler because there are fewer files and fewer functions. But that simplicity can be deceptive. The complexity has not disappeared. It has merely become implicit. It lives in tribal knowledge, in assumptions no one wrote down, in production incidents no one wants to repeat, in edge cases only one engineer remembers, and in fear of changing code because no one knows what might break.
A codebase with no tests can look clean in the same way an undocumented machine can look clean: fewer labels, fewer warnings, fewer visible constraints. But that does not make it easier to operate safely.
The candidate seemed to equate maintainability with having less to read. I think that is a serious misunderstanding. Maintainability is not the absence of text. It is the ability to understand, modify, validate, and evolve a system without unreasonable risk. Sometimes maintainability requires more code, not less. It requires tests, comments, documentation, type definitions, schemas, explicit interfaces, and well-named abstractions. The question is not whether these things add volume. The question is whether they add clarity. A well-written test suite adds clarity.
It can clarify what the system accepts:
Given a valid request with these fields, the service should normalize the payload and produce this result.
It can clarify what the system rejects:
Given a malformed request, the service should return a validation error rather than failing downstream.
It can clarify boundary behavior:
Given an empty result set, the service should return an empty list, not null, not an exception, and not a partial response.
It can clarify failure behavior:
Given that the external dependency times out, the service should retry according to policy and then return a controlled failure.
It can clarify invariants:
Given any successful transformation, the output should preserve these fields and never produce an invalid state.
These are not decorative tests. They are behavioral claims. They help future developers understand the system’s intent.
This is especially important because software often outlives the context in which it was written. The original engineer leaves. The team changes. Requirements shift. The business logic becomes familiar to no one. The reason for a strange branch disappears from memory. A future developer sees a conditional and wonders whether it is necessary. Without tests, they guess. With good tests, they have evidence. A good test suite answers questions a maintainer will inevitably ask:
- Can I refactor this function safely?
- What behavior must remain stable?
- What inputs are considered valid?
- What happens when this dependency fails?
- Is this branch intentional or accidental?
- Is this edge case part of the contract?
- If I remove this code, what breaks?
This is why tests are often the first place I look when trying to understand an unfamiliar codebase. Not because tests are always perfect, but because good tests reveal the behavioral shape of a system more directly than implementation code does. The implementation may contain optimizations, adapters, framework details, error handling, and historical sediment. Tests, when written well, show what outcomes the implementation is accountable for. That accountability is central to maintainability.
Without tests, future developers inherit a system of claims that are not mechanically checked. They can read the code, but reading code is not the same as knowing which behavior matters. They can infer intent, but inference is fragile. They can manually test, but manual testing is inconsistent. They can deploy and observe, but production is an expensive place to discover misunderstandings. Tests reduce that uncertainty.
They do not eliminate it. They do not prove correctness in the mathematical sense. They do not replace design thinking, code review, static analysis, observability, or operational discipline. But they give the system a memory. They allow engineers to preserve important behavior across change.
That is why the phrase “regression protection” matters. A regression is not just a bug. It is the reintroduction of a problem the team had already solved, or the accidental breakage of behavior the system already promised. Regressions are especially frustrating because they represent organizational forgetting. A good test suite prevents some of that forgetting. It turns a discovered issue into a permanent guardrail.
If a production bug occurs because an empty input caused an unexpected failure, a thoughtful engineer does not merely patch the code. They add a test that captures the lesson: this case matters, this behavior is expected, and this failure should not return. The test becomes a small piece of durable knowledge.
This is one of the reasons I object to the idea that tests are merely something the next developer has to read. The next developer is precisely the person the tests are for. They are the person who needs to know which assumptions are safe to change and which are not. They are the person who benefits from having examples of intended behavior. They are the person who should be protected from accidentally repeating old mistakes.
Of course, this depends on test quality. A disorganized test suite can become a burden. Tests with unclear names, excessive setup, irrelevant mocks, duplicated assertions, and hidden dependencies can obscure more than they reveal. A thousand tests that all verify trivial implementation details may create a false sense of safety while making refactoring painful. In that case, the solution is not to devalue testing. The solution is to improve test design. Bad tests are not an argument against tests. They are an argument against bad tests.
A thoughtful engineer should understand what makes tests maintainable. Test names should describe behavior, not implementation trivia. Test setup should be clear. Assertions should be meaningful. Related cases should be grouped coherently. Fixtures should reduce noise without hiding important details. Mocks should be used to isolate boundaries, not to fabricate confidence. Tests should be organized around behavior, invariants, and risk rather than around arbitrary line coverage goals.
The structure of the test suite matters because the test suite itself becomes part of the system’s architecture. If tests are scattered, redundant, and obscure, then yes, they become difficult to maintain. But if they are organized around the domain and the system’s behavioral contracts, they become a map.
This is the difference between test accumulation and test design. Test accumulation happens when engineers add tests reactively, without organizing principles, often to increase a metric or patch over a bug. The suite grows, but its explanatory power does not. Test design happens when engineers ask what behavior needs to be specified, what risks need to be controlled, what invariants need to be preserved, and how future developers will understand those guarantees.
The candidate’s comment suggested that he viewed tests mostly as accumulation. More tests meant more stuff. More files. More functions. More burden. But that is not how a mature engineer should think about it.
A mature engineer understands that tests can reduce the cognitive load of future work. They do this by making implicit behavior explicit. They reduce the amount of guessing required. They allow developers to change implementation while preserving externally important behavior. They make the cost of change more predictable. They help separate intended behavior from accidental behavior.
This is especially important in codebases that support real systems, where correctness matters and where future maintenance is inevitable. In those environments, the next developer is not helped by a lack of tests. The next developer is helped by a codebase that explains itself. Tests are one way the codebase explains itself. They say:
- This input is valid.
- This input is invalid.
- This edge case is intentional.
- This failure mode must be handled.
- This invariant must hold.
- This behavior changed once and should not regress.
- This dependency is unreliable and must be isolated.
- This output is part of the contract.
- That is a much richer view than “a bunch of test functions.”
The candidate’s framing also ignored a deeper point: maintainability is not just about reading less code; it is about making correct change easier. A system is maintainable when engineers can modify it with confidence. Confidence does not come from smallness alone. It comes from understanding and verification. A small untested codebase can be harder to maintain than a larger well-tested one. The small codebase may be full of hidden assumptions. The larger codebase may have explicit contracts, clear tests, and well-defined behavior. The first may look cleaner in a file tree. The second may be safer to evolve.
This is why I am skeptical when engineers treat tests primarily as a burden. It often reveals a short-term view of software. In the short term, writing fewer tests may feel faster. There is less code to write, less code to organize, and fewer failures to investigate. But in the long term, the absence of tests often shifts cost onto future maintainers. It makes refactoring riskier. It makes onboarding harder. It makes regressions more likely. It makes behavior less explicit. It makes the system depend more heavily on human memory. That is not maintainability. That is debt.
It may be reasonable to decide that a specific test is not worth writing. It may be reasonable to avoid testing trivial code directly. It may be reasonable to prefer integration coverage for certain behaviors. It may be reasonable to delete tests that no longer provide value. But these decisions should come from a considered testing strategy, not from a general suspicion that tests make life harder for future developers.
The thoughtful view is more precise: Tests impose maintenance cost, but good tests also reduce maintenance risk. The engineering question is whether the value of the behavioral guarantee exceeds the cost of preserving the test. That is the trade-off. Not “tests good” or “tests bad,” but whether a given test meaningfully improves confidence, documents intent, protects against regression, or clarifies behavior.
In the candidate’s answer, I did not hear that trade-off. I heard a vague aversion to test volume. That worried me because it suggested he was reasoning about tests as artifacts rather than as specifications. He saw the next developer reading test functions, but he did not seem to see the next developer relying on those tests to understand the system, change the code, and avoid breaking behavior. That is a shallow view of maintainability.
The best test suites I have worked with did not make the codebase feel more complicated. They made the codebase feel safer. They gave me examples. They told me what mattered. They allowed me to refactor with feedback. They exposed misunderstandings early. They turned domain expectations into executable form. They helped distinguish between a harmless internal change and a broken behavioral contract. That is what I would have wanted the candidate to explain. He could have said:
“I try to avoid tests that only exist to inflate coverage, because those can create maintenance burden. But I do value tests that document behavior and protect important contracts. My goal is not fewer tests or more tests in the abstract. My goal is a test suite that helps the next developer understand what the code promises to do.”
That would have been a strong answer. It would have shown that he understands both sides of the trade-off. It would have acknowledged that tests have cost without reducing them to clutter. It would have shown respect for the future maintainer. Instead, he spoke as though the mere presence of additional tests made the codebase harder to understand.
That is the Thoughtless Engineering pattern again. A real concern is converted into an overbroad heuristic. Bad tests can be clutter, therefore more tests are clutter. Coverage can be gamed, therefore coverage is not important. Common cases are frequent, therefore uncommon cases can be ignored. Each statement begins from something partially true and then skips the reasoning needed to apply it responsibly.
Software engineering is full of these partial truths. They are dangerous because they sound practical. They often resemble wisdom. But practical wisdom is not the same as reflexive dismissal. A mature engineer does not reject a practice because it has failure modes. A mature engineer understands the failure modes and then applies the practice carefully.
Tests have failure modes. That does not make tests clutter. Tests are one of the ways a codebase communicates its intended behavior across time. They help future engineers understand what matters, change what does not, and preserve what must remain true. They are not a substitute for design, but they are one of the clearest expressions of design intent. They are not proof of correctness, but they are a practical defense against forgetting. They are not free, but neither is ignorance.
A candidate who sees tests primarily as extra code to spare the next developer from reading has misunderstood the role tests play in long-term engineering quality. The next developer does not need fewer clues. They need better ones.
Correctness Is Semantic, Not Merely Executable
The candidate’s answers about testing also exposed a deeper misunderstanding about correctness itself.
He spoke about tests as though their main purpose was to exercise code paths: cover the normal inputs, avoid unnecessary tests, do not chase 100%, and do not create clutter. But software correctness is not fundamentally about whether code executes. It is about whether the system’s behavior corresponds to its intended meaning. That distinction matters.
A program can execute successfully and still be wrong. It can return a value and still violate the domain. It can handle an input without throwing an exception and still produce an invalid state. It can pass through every line of code and still fail to preserve the contract that other systems depend on. It can “work” in the narrow mechanical sense while being semantically incorrect.
This is why coverage alone is never enough. Coverage can tell us that a line was executed. It cannot tell us that the behavior was right. It cannot tell us that the output made sense, that the invariant was preserved, that the state transition was valid, that the failure mode was acceptable, or that the system did what its users and dependent systems reasonably expect it to do.
Correctness is semantic before it is mechanical. By semantic correctness, I mean that the implementation preserves the intended meaning of the system. The code does not merely run; it does the thing it is supposed to do, under the conditions it is supposed to handle, while respecting the assumptions, contracts, and invariants that define the system.
This is where good testing becomes more than line execution. A good test asks: given the purpose of this system, is this behavior correct? It does not merely ask whether a function was called or a branch was entered. It asks whether the result is meaningful relative to the domain.
For example, suppose a service receives a request to load a scenario configuration from a database, run a model, and return analysis results to a frontend. A superficial test might verify that the endpoint returns a 200 response for a common valid request. That is not useless, but it is shallow. A more meaningful test asks whether the returned analysis corresponds to the right scenario, whether the configuration was interpreted correctly, whether missing optional fields were handled according to the domain rules, whether invalid combinations were rejected, whether partial results were represented honestly, and whether downstream consumers can rely on the response shape.
- The first kind of test verifies that the code runs.
- The second kind of test verifies that the system means what it says.
- This is the difference between executable code and correct behavior.
The same distinction appears in error handling. Weak engineers often divide the world into two crude categories: the happy path and everything else. The happy path is what they test. Everything else becomes an “edge case,” a nuisance, or an exception. But that is not how real systems work. Not every unusual case is exceptional. This is one of the most important distinctions in software design: a true exception is different from expected-but-unusual behavior.
A true exception is a violation of an assumption, invariant, precondition, contract, or environmental guarantee that the system depends on. It is a condition under which the intended computation cannot proceed normally. Corrupted data may be exceptional. An unreachable dependency may be exceptional. A malformed upstream payload may be exceptional. An authentication failure may be exceptional in a context where the system requires verified identity before proceeding. An impossible state may be exceptional because its existence means a deeper invariant has already been broken.
By contrast, expected-but-unusual behavior is behavior that may be rare, inconvenient, or unpleasant, but still belongs within the intended operating envelope of the system. It is not a violation of the system’s meaning. It is part of the domain. An empty search result is usually not an exception. If a user searches for something and nothing matches, the system has not failed. It has found no results. That is a valid outcome, and it should be modeled as one.
A duplicate request may not be exceptional. In distributed systems, duplicate delivery, retries, repeated submissions, and idempotent operations are often part of the normal reality. Treating every duplicate as a bizarre error may reflect a weak understanding of the environment.
A missing optional field is not necessarily exceptional. If the field is genuinely optional, then its absence should be represented in the model. The system should have an explicit behavior for that case. A boundary-value input may be rare but completely valid. A user at the maximum allowed size, a request with an empty list, a timestamp at a boundary, or a value of zero may be uncommon, but if the system claims to accept it, then it belongs to the domain. These cases may be weird. They may be uncommon. They may be annoying. But they are not automatically exceptions. This matters because the way we classify a case determines how we design and test the system.
If something is truly exceptional, the system should usually fail clearly, preserve invariants, provide useful diagnostic information, avoid corrupting state, and recover or contain the failure if possible. The test should verify the failure semantics. Does the system reject the corrupted input? Does it avoid writing partial state? Does it surface the error in a controlled way? Does it log enough context? Does it prevent the failure from propagating incorrectly?
If something is expected-but-unusual, the system should not treat it as a surprise. It should model the case explicitly. The test should verify the intended behavior. Does an empty result return an empty list? Does a duplicate request produce the same result without repeating side effects? Does a missing optional field lead to a default, a null representation, a validation warning, or some other defined outcome? Does a boundary value behave according to the contract? This distinction is not academic. It is central to whether software is designed well.
When engineers fail to distinguish exceptional behavior from unusual but valid behavior, systems become messy. Expected domain cases get scattered across the codebase as ad hoc exception handling. Rare but legitimate states become surprising runtime branches. The code becomes full of defensive checks because the model does not represent reality clearly. Tests become awkward because the system itself has not decided what these cases mean. A better design makes the distinction explicit.
If an empty search result is normal, model it as a normal result. If duplicate requests are expected, design the operation to be idempotent. If a field is optional, represent it explicitly. If a dependency can be unavailable, define the degraded mode. If an input violates the contract, reject it at the boundary. If a state should be impossible, use types, schemas, validation, or construction rules to make it impossible or at least detectable. Good tests then follow from good semantics. They do not merely cover branches. They verify the meaning of the branches.
This is what I was trying to get at with the familiar phrase “not a bug, a feature.” The phrase is often used jokingly, but it points to a serious issue: whether a behavior is wrong depends on what the system is intended to do. A surprising behavior is not automatically a bug. It might be intended behavior. Conversely, a behavior that does not crash the system is not automatically correct. It might silently violate the domain. The question is always: what does the behavior mean relative to the system’s contract?
Suppose a model-serving endpoint receives a scenario configuration with a missing optional parameter. If the domain says that omitted parameter should default to a known value, then returning a result with that default may be correct. If the domain says the parameter is required for scientific validity, then silently defaulting may be dangerously wrong. The code may execute in both cases. Only the semantics tell us which behavior is correct.
Or consider duplicate requests. If the operation is a read-only analysis, duplicate requests may be harmless. If the operation triggers a stateful side effect, duplicate requests may create corrupted state, double billing, duplicate jobs, or inconsistent downstream behavior. Again, the issue is not merely whether the code runs. The issue is what the system promises about repeated actions.
This is why a mature engineer should not be satisfied with “I tested the four common inputs.” Common inputs matter, but they do not define correctness by themselves. Correctness depends on the structure of the domain: valid cases, invalid cases, boundary cases, exceptional cases, and unusual-but-expected cases. A thoughtful engineer should be able to explain how those categories were identified.
Are the four common inputs representatives of equivalence classes? Are they the only valid inputs according to the schema? Are they common because production telemetry showed they dominate usage? Are the remaining inputs invalid? Are they impossible? Are they valid but rare? Are they rare but high-impact? Are they handled elsewhere in the system? Are they protected by a contract with an upstream service? Without that reasoning, selecting common inputs is just sampling. Testing should be organized around meaning. It should reflect the system’s behavioral contract. That means asking questions like:
- What must always be true?
- What may vary?
- What is allowed?
- What is forbidden?
- What is rare but valid?
- What is invalid and should be rejected?
- What is impossible if the system is well-formed?
- What happens when an external guarantee fails?
- What behavior do downstream systems rely on?
These questions lead to better tests because they lead to better understanding. They turn testing from a coverage exercise into a correctness exercise.
This is also where invariants matter. An invariant is something that must remain true across operations, states, or transformations. Invariants are among the most important things to test because they express the deep commitments of a system. For example, a transformation should never produce an output with a missing required identifier. A state transition should never move directly from an initial state to a completed state without passing validation. A financial calculation should preserve certain accounting relationships. A scientific model pipeline should never report results as valid if required inputs were absent. An authorization system should never grant access without satisfying the required policy. These are not merely lines to cover. They are truths the system must preserve.
If a test executes the code but does not check the invariant, then the test may provide very little confidence. It proves that something happened, not that the right thing happened.
This is one of the reasons I found the candidate’s answer so shallow. It seemed to treat tests as examples of execution rather than assertions of meaning. He did not discuss invariants. He did not discuss contracts. He did not discuss the difference between normal domain variation and true exceptional behavior. He did not explain why the inputs he tested were representative of the system’s intended behavior. He did not explain what kinds of invalid or unusual cases were considered and rejected.
He spoke as if the purpose of testing was to cover enough ordinary cases to feel comfortable. But correctness is not comfort. Correctness is conformance to intended behavior.
In real systems, especially systems with downstream dependencies, the intended behavior must be explicit. If it is not explicit, it will be invented accidentally by the implementation. That is dangerous because implementations accumulate historical accidents. A branch added for one customer becomes part of the contract. A default added for convenience becomes relied upon by another service. A silent failure becomes normal because no one noticed it. A missing field is tolerated in one place and rejected in another. Over time, the system’s behavior becomes a sedimentary record of unexamined decisions. Tests can either reinforce that confusion or clarify it.
Bad tests often lock in accidental behavior. They assert whatever the current implementation does, even if no one has decided whether that behavior is correct. Good tests force the engineer to name the expected behavior and justify it. They ask: is this what the system should do, or merely what it happens to do? That is a valuable discipline.
It is also why design and testing cannot be cleanly separated. If the domain model is vague, the tests will be vague. If the contracts are unclear, the tests will be arbitrary. If the system does not distinguish expected-but-unusual behavior from exceptional behavior, the tests will either ignore important cases or encode inconsistent responses to them.
A strong testing philosophy therefore begins before the test file is written. It begins with understanding the system’s semantics.
- What are the core concepts?
- What states can exist?
- What transitions are valid?
- What inputs belong to the domain?
- What dependencies are trusted?
- What failures are recoverable?
- What guarantees does this service provide to others?
- What does correctness mean here?
- Only after those questions are answered can tests become meaningful.
This is the deeper reason coverage can be misleading. Coverage counts execution. It does not count understanding. It does not know whether the assertion corresponds to the contract. It does not know whether the behavior is intentional. It does not know whether the edge case is impossible, invalid, rare-but-normal, or catastrophic. It does not know whether the test protects a real invariant or merely touches code. The engineer has to know that. And that is what I did not hear from the candidate.
I did not hear a semantics-first view of correctness. I heard a coverage-first view, followed by a dismissal of coverage when it became inconvenient. I heard common-case testing without a theory of input space. I heard concern about test volume without a theory of behavioral specification. I heard a shallow distinction between what usually happens and what supposedly does not matter. But software correctness lives precisely in those distinctions.
It lives in knowing when an empty result is a valid answer. It lives in knowing when a duplicate request is harmless and when it is dangerous. It lives in knowing when a missing field should default, when it should warn, and when it should fail. It lives in knowing when a corrupted payload means the system must stop immediately. It lives in knowing when a dependency failure should trigger retry, fallback, degradation, or user-visible failure. It lives in knowing that boundary values are often where assumptions reveal themselves.
A thoughtful engineer does not treat these as random edge cases. They treat them as part of the system’s meaning. That is why I say correctness is semantic, not merely executable. The code must run, of course. But running is not enough. The program must preserve the meaning of the domain, the promises of the interface, the invariants of the model, and the expectations of the systems and people that depend on it.
Good tests are one of the primary ways we make those meanings explicit. They do not simply ask, “Did the code execute?” They ask, “Did the system do what it means to do?”
Design Quality Determines Testing Burden
One of the deeper issues beneath the candidate’s comments was the assumption that testing exists primarily to compensate for complexity after the software has already been designed. Under that view, the sequence is straightforward: engineers build the system, the system accumulates branches and exceptional cases, and then the team writes enough tests to cover those cases. More complexity produces more tests, and the test suite grows alongside the codebase. There is some truth in that sequence, but it is incomplete because it treats the existing design as fixed. It assumes that every branch, every nullable value, every awkward combination of inputs, and every special-case exception is an unavoidable fact of the problem rather than, at least sometimes, a consequence of poor modeling.
A more thoughtful engineer asks an earlier and more important question: why does this complexity exist at all? Before adding another test for another niche branch, it is worth asking whether the branch should exist, whether the interface should allow that state, whether the data model is carrying distinctions the domain does not actually need, or whether the system is repeatedly defending itself against conditions that could have been ruled out at the boundary. Testing burden is not independent of design quality. In many cases, the difficulty of testing a system is evidence that the system itself has too many poorly constrained states.
This is what I mean when I say that testing is downstream of design. By the time engineers begin writing tests, many of the most important decisions have already been made. The shape of the interfaces, the clarity of the domain model, the visibility of dependencies, the strength of contracts, and the number of representable states all determine how much uncertainty remains. A system with weak abstractions and permissive inputs naturally produces more branches, more defensive code, and more tests. A system with clear boundaries, normalized inputs, explicit states, and constrained transitions is easier to reason about because there are fewer meaningful ways for it to behave.
Consider a function that accepts six optional parameters, several of which interact with one another. Some combinations are valid, some are invalid, some trigger defaults, and others produce special behavior. The test matrix quickly grows because every combination appears to require consideration. It is tempting to conclude that the solution is simply to write more tests. But the more fundamental question is whether the interface is exposing too much ambiguity. Perhaps those parameters should be grouped into a structured object with validation rules. Perhaps mutually exclusive options should be represented as separate types. Perhaps required information should be mandatory rather than encoded as a collection of nullable fields. Perhaps the input should be normalized once at the boundary so that the core system only operates on a small set of valid internal forms.
Each of those design changes reduces the number of possible states the system must handle. The test suite becomes smaller not because the team lowered its standards, but because the design itself made entire categories of behavior impossible. That is a far more powerful form of correctness than detecting every invalid combination after the fact.
The principle is sometimes summarized as making invalid states unrepresentable. Even when a language or framework does not let us achieve that perfectly, the underlying goal remains useful. If the domain only allows an order to be Pending, Paid, Cancelled, Refunded, or Completed, then representing those states explicitly is usually better than combining a collection of booleans such as isPaid, isCancelled, and isRefunded. The boolean representation creates combinations that may not make sense, forcing every function to remember which combinations are valid. The explicit-state representation narrows the model and therefore narrows the testing problem.
The difference is not cosmetic. In the first design, complexity is distributed across every caller, every conditional, and every test. In the second, the model itself carries more of the burden. The software does not rely on every future engineer remembering the same unwritten rules. It encodes those rules in the structure of the system.
This is why testability is often a consequence of architecture rather than a property added afterward. Systems are difficult to test when their responsibilities are entangled, their dependencies are hidden, their inputs are loosely specified, and their outputs are ambiguous. A test suite built around such a system becomes complicated because the system is complicated in ways that are difficult to control. Engineers then reach for more mocking, more setup, more fixtures, more branching, and more defensive assertions, but the tests are reflecting a design problem rather than creating one.
I have seen this pattern repeatedly in mature codebases. A system begins with a straightforward model, but over time special cases accumulate. A configuration flag is added for one customer, then another override is introduced for a new workflow, then a fallback path is added because a dependency occasionally fails, and eventually several combinations of those behaviors interact in ways no one fully understands. The test suite grows in response, but the growth does not necessarily produce confidence. Instead, the tests become a map of historical accidents. The team ends up preserving complexity because the tests prove that the complexity exists, not because anyone can still explain why it should.
At that point, writing more tests may be necessary, but it is not sufficient. The stronger engineering move is to distinguish essential complexity from accidental complexity. Essential complexity comes from the problem domain itself. Scientific modeling, financial systems, distributed coordination, and other serious domains often contain genuine complexity that cannot be designed away. Accidental complexity comes from implementation choices, unclear boundaries, weak contracts, and historical layering. Thoughtful engineering does not pretend all complexity can be removed, but it does refuse to treat every existing complication as inevitable.
This distinction should shape testing strategy. When complexity is essential, the test suite should be comprehensive because the domain truly contains many meaningful cases. When complexity is accidental, the better long-term solution may be redesign. Otherwise, the team spends increasing amounts of effort proving the behavior of a structure that should have been simplified.
Exception handling is one area where this becomes especially visible. A system with a large exception surface often has unclear boundaries. Inputs arrive in inconsistent forms. Downstream code repeatedly checks for missing values. Different modules interpret the same failure differently. Every layer catches exceptions because no single layer owns the failure semantics. The result is a combination of brittle code and a sprawling test suite that tries to simulate every possible path.
A better design asks where the uncertainty belongs. Input validation should happen at defined boundaries. Data should be normalized before it enters the core domain. Return types should communicate ordinary failure explicitly when failure is part of expected behavior. Truly exceptional conditions should be distinguishable from routine negative outcomes. Contracts should define what callers may rely on. Dependencies should be isolated so that failures are handled consistently rather than scattered throughout the system.
These choices reduce what I would call the exception surface area: the set of places where unexpected or invalid conditions can enter, propagate, or be interpreted inconsistently. A smaller exception surface makes the system easier to test because there are fewer places where behavior can diverge unexpectedly.
Cleaner interfaces are especially important. An interface that accepts almost anything and leaves interpretation to the implementation transfers complexity inward. Every caller can create a new variation, and the callee must defend against all of them. A stricter interface narrows the contract and clarifies responsibility. It may reject malformed input early, require explicit variants, or expose separate operations for different behaviors. The stricter design can appear less flexible, but that loss of flexibility is often a gain in correctness.
The same applies to return types. A function that sometimes returns data, sometimes returns None, sometimes throws an exception, and sometimes produces a partially valid object creates an enormous testing burden because every consumer must understand all four cases. A more explicit return type can make those possibilities visible and force callers to handle them deliberately. Again, the test suite improves because the design improved.
This is why a thoughtful engineer does not view every testing problem as a request for more tests. Sometimes the correct response is to change the code so that fewer behaviors are possible. Sometimes the right answer is to move validation. Sometimes it is to remove a configuration option. Sometimes it is to split one overloaded abstraction into two clearer ones. Sometimes it is to centralize error handling. Sometimes it is to redesign the contract entirely.
None of this means tests become less important. It means tests become more meaningful. When the design is clear, tests can focus on important behavior instead of compensating for representational ambiguity. They can verify invariants, contracts, and genuine domain variation rather than exploring a combinatorial explosion created by loose interfaces and poorly constrained state.
This is the positive relationship between design and testing. Good design makes the important behaviors easier to name. Once the behaviors are easier to name, the tests become easier to organize. A clear abstraction suggests clear invariants. A clear contract suggests obvious boundary cases. A constrained state model reduces the number of impossible combinations the suite must consider. The architecture and the testing strategy reinforce one another because both are grounded in the same understanding of the system.
What I found missing in the candidate’s answers was any awareness of this relationship. Testing was discussed as something layered on top of an implementation, not as something shaped by the implementation’s conceptual quality. He did not discuss whether design choices could eliminate branches, narrow inputs, clarify error semantics, or reduce the number of states the system needed to support. The burden of complexity was accepted rather than questioned.
That is the more mature habit I was looking for. A strong engineer certainly knows how to add tests around a complex system, but they also know when the right move is to simplify the system itself. They understand that every additional representable state increases the number of behaviors that must be reasoned about, maintained, and verified over time. They do not confuse exhaustive testing with good design.
Good engineers do not merely test around complexity. They try to remove unnecessary complexity from the system’s state space. That is not a way of avoiding rigor. It is one of the deepest forms of rigor, because it reduces the number of ways the software can be wrong before testing even begins.
External Dependencies: “We Test It Later” Is Not Enough
The next part of the interview moved from internal logic to code that depends on external systems. I asked the candidate how he approached testing interactions with APIs and other services outside his control. His answer was that they generally did not test those interactions in unit tests and instead relied on integration testing later in the process. As a first response, this was not unreasonable. It is generally a mistake to treat unit tests as a place to verify the correctness of software owned and maintained by somebody else. I do not need a unit test to prove that Amazon S3, PostgreSQL, Stripe, Redis, or some external HTTP service implements its own behavior correctly. Those systems have their own owners, contracts, test suites, and operational guarantees. But stopping at “we test it later in integration” avoids the more important question, which is how our own software behaves around that dependency.
The distinction between testing an external dependency and testing our interaction with an external dependency is fundamental. The external system itself is not the primary object of the unit test. Our assumptions, transformations, validation, retry logic, error handling, timeout behavior, and recovery strategy are. If my application calls an external API, I do not need to prove that the API can return a valid response in some abstract sense. I need to prove that my code interprets a valid response correctly, rejects a malformed response safely, handles a timeout according to policy, avoids corrupting state after a partial failure, and behaves predictably when the dependency is unavailable. Those are responsibilities that belong to my system, even though the triggering conditions originate elsewhere.
This is where dependency isolation becomes important. A well-designed system should make it possible to separate domain logic from the mechanics of calling a remote service. That separation creates what testing literature often calls a seam: a place where the real dependency can be replaced by a controlled substitute. The substitute might be a mock, stub, fake, monkey patch, in-memory implementation, or virtualized service. The specific technique matters less than the architectural principle. The point is to create an environment in which the behavior of the code we own can be exercised deterministically, without depending on a live network, an external account, a particular database state, or the uptime of somebody else’s service.
That is the conceptual depth I was looking for in the candidate’s answer. I wanted to hear how the dependency entered the application, whether it was wrapped behind an interface, whether requests and responses were normalized at the boundary, and whether failure conditions could be simulated deliberately. I wanted to know where retries were implemented, whether timeouts were explicit, whether malformed payloads were distinguishable from ordinary negative responses, and whether downstream logic could be tested without making a real network call. These are not merely test-implementation details. They reveal whether the application has been designed with clear boundaries and whether external uncertainty has been contained rather than allowed to leak throughout the codebase.
Unit tests and integration tests serve different purposes, and neither is a substitute for the other. A unit test asks whether a piece of logic behaves correctly in a controlled environment. An integration test asks whether separately developed components actually work together under realistic conditions. Integration tests are necessary for verifying protocol compatibility, authentication, serialization, database mappings, network configuration, deployment assumptions, and other concerns that only emerge when real systems interact. But integration tests are usually too expensive and too coarse-grained to explore the full behavioral space around a dependency. Creating deterministic scenarios for intermittent failures, malformed responses, repeated timeouts, duplicate messages, or partial success is often cumbersome in a real integrated environment. These are precisely the conditions that controlled substitutes allow us to explore efficiently.
For example, suppose a service calls an external analysis engine. An integration test can verify that the request reaches the engine, that authentication works, that the payload is accepted, and that the response can be parsed. Those are important checks. But the application still needs unit-level verification of its own behavior when the engine returns a result with missing fields, responds too slowly, produces an error code, returns a payload that satisfies the schema but violates a domain assumption, or becomes unavailable after part of the local workflow has already completed. The integration environment may eventually expose some of these situations, but relying on it as the only place to discover them makes feedback slower, diagnosis harder, and coverage of failure behavior less systematic.
This is also why mocking is frequently misunderstood. Its purpose is not simply to avoid making network calls or to make tests run faster, although both benefits are real. Its deeper purpose is to create a controlled experiment. By replacing the external dependency with a known substitute, we can vary one condition at a time and observe how our own code responds. We can force a timeout on demand, return malformed JSON, simulate an authentication failure, or produce an unexpected sequence of responses. That control allows us to test behavior that would otherwise be difficult, expensive, or unreliable to reproduce. It also improves diagnosis, because when every dependency is controlled and a test fails, the defect is more likely to be located in the code under test rather than somewhere in a large and unstable integration environment.
A stronger answer would also have mentioned contract testing. Unit tests with mocks are useful, but they create a risk of their own: the mock may represent what we think the external service does rather than what it actually promises to do. Contract tests address that risk by verifying that the consumer and provider still agree on request shape, response shape, required fields, error semantics, and compatibility expectations. In a distributed system, that agreement is one of the most important things to preserve. Integration tests tell us whether the systems happen to work together in a particular environment at a particular time. Contract tests give us a more explicit way to detect when one side has changed in a way that violates the other side’s assumptions.
Service virtualization is useful for similar reasons when the external dependency is too complex, expensive, unavailable, rate-limited, or operationally difficult to include in routine testing. A virtualized service can reproduce realistic response patterns and failure conditions without requiring the actual dependency to be present. Again, the goal is not to pretend that the dependency does not matter. The goal is to separate the kinds of confidence we need. We want unit-level confidence that our logic responds correctly to defined conditions, contract-level confidence that our assumptions match the provider’s guarantees, and integration-level confidence that the real systems communicate successfully in an actual environment.
The candidate’s answer remained at the procedural level. He described when the team tested external interactions, but not how the boundary was designed or what each test layer was intended to prove. Saying “we do not test that in unit tests; we test it later in integration” tells me where an activity occurs in the development process. It does not tell me whether the engineer understands responsibility, isolation, failure simulation, or the architecture of the dependency boundary. It does not tell me whether the application can distinguish between an external failure and an internal defect. It does not tell me whether the team can reproduce difficult failure modes before they happen in production.
This distinction is especially important because external services are often where systems become fragile. Networks are slow, dependencies fail, payloads drift, authentication expires, rate limits change, and assumptions that seemed stable turn out to be conditional. If the architecture tightly embeds those uncertainties inside domain logic, the testing burden expands and failures become harder to contain. If the dependency is isolated behind a clear interface with explicit contracts and controlled failure semantics, both testing and future replacement become easier. The same seam that supports unit testing also supports migration, substitution, resilience, and long-term maintainability.
What disappointed me, then, was not that the candidate preferred integration tests for validating real external behavior. That part was reasonable. What disappointed me was that the answer stopped before reaching the concepts that make the strategy defensible. There was no discussion of dependency isolation, controlled substitutes, seams, contract testing, service virtualization, timeout policy, retry behavior, malformed responses, or degraded operation. The answer described a sequence of testing activities without demonstrating an architectural understanding of why those activities were separated or what confidence each layer was supposed to establish.
That is why I characterized the answer as procedural rather than conceptual. A procedural answer says, “We test this later.” A conceptual answer says, “We isolate the dependency so that unit tests can verify our behavior under controlled success and failure conditions, contract tests can verify our assumptions about the interface, and integration tests can verify that the real systems work together.” The second answer demonstrates not only knowledge of testing techniques, but also an understanding of system boundaries and responsibility.
That was the depth I was looking for, and it was missing.
From Edge Cases to Tail Risk
The candidate’s remark that he tested the four input variations he normally encountered, while not worrying much about the others, revealed more than a narrow approach to test selection. It suggested an average-case conception of software reliability: identify the cases that occur most often, verify that the program handles them, and treat the remainder as progressively less important because they are progressively less common. There is an understandable practical instinct behind this approach. Engineering effort is finite, and it would be absurd to spend equal time on every conceivable input or failure. The problem is not that the candidate prioritized common behavior. The problem is that he appeared to use frequency as the primary, and perhaps only, measure of importance.
That assumption becomes increasingly dangerous as software moves from isolated programs into distributed systems. In a small application, an unusual event may genuinely remain unusual in every meaningful sense. In a system composed of many services, databases, queues, caches, external APIs, and network calls, however, rare component-level behavior is repeatedly sampled across a very large number of interactions. Each request may pass through several components, and each component creates another opportunity for delay, partial failure, malformed data, contention, or unexpected state. What looks negligible when one function is examined in isolation may become routine when the whole system executes millions of requests across hundreds of dependencies.
Jeffrey Dean and Luiz André Barroso describe this phenomenon in their 2013 paper The Tail at Scale. Their argument is that large distributed services cannot be understood adequately through averages because end-to-end performance is frequently determined by the slowest participating component. A service may have excellent median latency and apparently acceptable behavior at the ninety-ninth percentile when considered alone. Yet when a user request fans out across tens or hundreds of machines, the chance that at least one subrequest falls into the slow tail rises sharply. The user does not experience the average machine. The user experiences the completion time of the composed request, which may be governed by whichever dependency responds last.
The importance of this observation goes beyond latency. The paper is specifically concerned with responsiveness in large-scale systems, but its broader lesson applies to reliability, correctness, and operational risk. Composition changes the meaning of rarity. If a single component misbehaves one time in a thousand, that may seem insignificant. If one system-level operation depends on a hundred such components, and the platform executes millions of those operations, the supposedly unusual behavior becomes an ordinary operational concern. A local outlier can become a recurring system-level condition, not because the probability of the local event changed, but because the architecture creates so many opportunities for the event to occur.
This is the point the candidate’s common-input reasoning failed to address. He treated uncommon cases as though their low frequency made them marginal. In distributed systems, however, the significance of a case depends on more than its frequency within a single function. It also depends on how often the function is invoked, how many other components participate in the same workflow, whether the condition propagates, and what the system does in response. A malformed response that appears once in one hundred thousand calls may still occur several times a day at scale. A rare timeout may become common from the perspective of an endpoint that fans out across many downstream services. An unusual duplicate message may be operationally harmless if the consumer is idempotent, or it may create repeated side effects and inconsistent state if the system was designed only for the nominal path.
This is why the term “edge case” can be misleading. It encourages engineers to place a wide range of behaviors into a single conceptual category defined primarily by rarity. Yet uncommon cases are not all alike. Some are impossible because the system’s types, schemas, or boundaries rule them out. Some are invalid inputs that can be rejected cheaply and safely. Some are valid but infrequent domain conditions. Some arise only under load or partial dependency failure. Others may be statistically rare but capable of causing severe or cascading damage. Grouping all of these together as edge cases conceals the very distinctions that should determine testing priority.
A thoughtful engineer should therefore ask not only whether a condition is common, but also how it interacts with the surrounding system. Does it violate an invariant? Does it cause a retry? Can that retry increase load on an already degraded dependency? Can it produce duplicate side effects? Does it leave partial state behind? Can it block unrelated work by consuming a shared thread pool, connection pool, queue, or concurrency limit? Can a local failure spread across service boundaries? These questions turn test selection from a simple exercise in sampling common inputs into a risk analysis grounded in architecture.
Consider a Lambda function that calls a downstream service. Most requests complete quickly, but a very small percentage experience high latency. If the function simply fails one isolated request and returns a controlled error, the tail event may have a limited impact. If the caller retries immediately, however, the behavior changes. The retry creates more traffic for the same dependency that is already responding slowly. As more requests time out and retry, concurrency increases, queues grow, and downstream capacity is consumed more rapidly. What began as a rare latency outlier may become a feedback loop. The importance of the original event lies not in its frequency but in the way the system amplifies it.
This is also why Dean and Barroso emphasize tail-tolerant design rather than relying solely on efforts to eliminate all variation. In large systems, some degree of latency variability is unavoidable. Hardware pauses, resource contention, background activity, scheduling delays, queueing, and network behavior all introduce outliers. The engineering objective is therefore not merely to optimize the average path. It is to build mechanisms that prevent individual slow components from dominating the entire operation. Techniques such as hedged requests, redundant execution, cancellation of unnecessary work, careful queue management, and latency-aware routing are examples of designs that modify the system’s response to the tail.
The broader principle is that rare events deserve attention when the architecture gives them leverage. An uncommon input that is rejected cleanly at the system boundary may require little additional concern. An equally uncommon input that reaches a critical state transition, triggers an expensive operation, or interacts with several dependencies may deserve extensive testing. The relevant unit of analysis is not the input in isolation but the path it activates through the system. Testing priorities should reflect that path, the consequences of failure, and the extent to which the resulting behavior can spread.
None of this implies that every theoretical edge case should be tested. Such a standard would be impossible to satisfy and would waste enormous amounts of engineering effort. The lesson of The Tail at Scale is not that rare events are automatically more important than common ones. It is that average-case reasoning is insufficient in composed systems. Engineers must identify which tails can dominate end-to-end behavior and which uncommon events can acquire significance through repetition, fan-out, dependency chains, or feedback loops. The goal is not exhaustive testing but deliberate testing informed by system structure.
This distinction is particularly important when an engineer claims to have improved reliability. Reliability is not established merely by showing that the system handles its four most common inputs. A credible reliability argument should address the conditions under which the system is most likely to fail or degrade. It should consider timeouts, malformed responses, partial failures, retries, duplicate invocation, resource exhaustion, inconsistent state, and degraded dependencies. It should explain which of these conditions are relevant to the architecture, how they are contained, and which tests provide confidence that the containment mechanisms work.
The candidate did not demonstrate that kind of reasoning. His answer remained local and frequency-driven. He appeared to think of an input as important when it was common and unimportant when it was unusual, without considering whether an uncommon case might occupy a critical point in the larger system. He did not discuss fan-out, dependency behavior, propagation, or the way repeated low-probability events become normal at scale. This was not merely a gap in testing technique. It was a gap in systems thinking.
That distinction is central to the larger argument of this essay. Thoughtless Engineering tends to optimize what is immediately visible: the average request, the common input, the individual function, or the local metric. Thoughtful engineering asks how local behavior composes. It recognizes that users experience the whole system, not the median performance of its parts, and that system-level behavior can be determined by the interactions among individually rare events.
The candidate’s statement about four common inputs therefore mattered because it revealed the boundary of his reasoning. He understood that test effort must be prioritized, but he did not articulate how architecture, amplification, and consequence should shape that prioritization. He treated rarity as a reason to look away. A systems engineer understands that rarity is often a reason to look more carefully—not in every case, but wherever the structure of the system allows a rare event to dominate the whole.
- Dean, Jeffrey, and Luiz André Barroso. “The Tail at Scale.” Communications of the ACM, vol. 56, no. 2, 2013, pp. 74–80.
Systems Risk, Convexity, and Nonlinear Failure
The discussion of tail risk leads to a more general question about engineering judgment: when an uncommon event occurs, how does the system respond? Frequency matters, but frequency alone is not a sufficient measure of risk. An event that happens regularly but causes negligible disruption may deserve less attention than an event that occurs once in a million operations but corrupts data, disables a critical service, or initiates a cascading failure across dependent systems. The candidate’s answer about testing four common input variations never reached this distinction. His reasoning implicitly equated uncommon behavior with low-priority behavior, without considering whether the consequences of those uncommon cases might be disproportionate to their frequency.
This is where Nassim Nicholas Taleb’s work on fragility and convexity provides a useful theoretical framework. Taleb’s central distinction is between an uncertain event and a system’s exposure to that event. We may describe the disturbance as (x) and the system’s response as (f(x)). In practical terms, (x) might be the duration of a dependency outage, the intensity of a traffic spike, the number of duplicated messages, the latency of a downstream request, or the size of a malformed payload. The function (f(x)) describes what those disturbances do to the system: how much downtime they cause, how much state they corrupt, how many dependent services they affect, or how much recovery work they generate. Taleb’s important observation is that it is often more productive to modify the response function than to believe we can accurately predict every disturbance. We may not know exactly when or how a shock will occur, but we can design the system so that its response is bounded rather than catastrophic.
This framework translates naturally into software engineering because software systems are full of uncertain events whose consequences are shaped by architecture. A dependency will eventually slow down. A message will eventually be delivered twice. A process will eventually crash after completing only part of an operation. A deployment will eventually introduce an incompatible assumption. A queue will eventually receive traffic faster than its consumers can process it. The engineer may not be able to predict the exact occurrence, timing, or combination of these events, but the engineer can influence whether the system absorbs them, contains them, or amplifies them.
The mathematical language requires some care because “convexity” and “concavity” depend on what is being measured. If the vertical axis represents system health, utility, or service quality, a fragile system often has a concave response to increasing stress: each additional unit of disturbance causes a larger reduction in system value. If the vertical axis instead represents damage or loss, the same system has a convex harm function: damage accelerates as the disturbance grows. Throughout this discussion, I am using “downside convexity” to mean that the harm grows more than proportionally to the initiating event. A dependency that is unavailable for two seconds causes four seconds of disruption; an outage twice as long causes ten times the operational damage; a small rise in latency pushes queues past a threshold and produces a much larger collapse in throughput. The precise terminology matters less than the structural fact: the relationship between disturbance and harm is nonlinear.
This is the defining characteristic of a fragile system. A fragile system does not merely suffer when stressed. It amplifies stress. Small disruptions remain manageable until they cross a threshold, after which damage accelerates. Local failures interact with retry logic, queueing, shared resources, synchronous dependencies, and operational procedures in ways that make the resulting harm much larger than the initiating event. The system may appear reliable during ordinary operation because the relevant nonlinearities are invisible at low stress. Once the wrong combination of conditions occurs, however, the architecture reveals that it was relying on stability rather than creating resilience.
Consider a collection of AWS Lambda functions that jointly implement a service. One Lambda calls another service and receives an unusually slow response. The first invocation times out, so the caller retries. The retry appears reasonable when viewed locally because transient failures often disappear on a second attempt. But the dependency is already under strain, and the retry creates additional work at exactly the wrong moment. More callers begin timing out, and each initiates its own retry. The increased request volume consumes more connections, execution slots, memory, and downstream capacity. Latency rises further, producing additional timeouts and therefore additional retries. A rare slow response has now become a feedback loop.
The original event may have been statistically insignificant. Perhaps one request in tens of thousands entered the slow tail. But once retry behavior and resource contention interacted, the system transformed a small disturbance into a much larger operational event. Queue depth increased, concurrent executions expanded, downstream throttling began, and dependent services experienced degraded performance. What began as one local timeout could ultimately create a service-wide outage. The important engineering fact is not merely that the original event was rare. It is that the system’s response to the event was convex in harm.
This example also shows why local correctness is insufficient. Each component may appear to behave reasonably in isolation. The caller correctly retries a failed request. The Lambda platform correctly launches additional invocations. The downstream service correctly throttles traffic that exceeds its limits. The queue correctly preserves work that has not yet completed. No individual component necessarily contains an obvious defect. The failure emerges from the interaction among locally sensible mechanisms. That is precisely why systems risk cannot be evaluated only by examining individual functions or counting their covered lines.
The concept of blast radius becomes useful here. Blast radius describes how far the consequences of a local failure can propagate. A low-frequency event with a tightly bounded blast radius may be an acceptable risk. A function fails, one request returns an error, the operation is safely retried later, and no persistent state is damaged. The same frequency becomes much more concerning when the failure can exhaust shared resources, trigger duplicate side effects, corrupt downstream state, or prevent unrelated workloads from making progress. The probability of the initiating event may be identical in both systems, but the risk is not, because the architectures transform the event differently.
This is one reason I wanted the candidate to go beyond statements about “normal” inputs. Input frequency says very little about blast radius. A malformed request may be common and harmless because it is rejected immediately at the boundary. A rare combination of otherwise valid inputs may be much more dangerous if it bypasses validation, enters a costly execution path, and triggers an inconsistent update across several services. The correct priority cannot be inferred simply by counting how often each case appears. It requires understanding where the case enters the system, which invariants it can violate, which components depend on the resulting behavior, and whether the architecture contains or amplifies the consequences.
The distinction between probability and exposure is therefore essential. Engineers often devote enormous effort to estimating how likely an event is while paying less attention to the shape of the system’s response. Taleb’s framework suggests reversing some of that emphasis. Rather than assuming we can assign reliable probabilities to every complex failure mode, we should ask how to make the system less sensitive to errors in those probability estimates. If an event is more common than expected, does the system degrade gradually, or does it collapse? If a dependency remains unavailable longer than anticipated, are losses bounded? If retries arrive simultaneously, does backpressure slow the system in a controlled way, or does the additional work accelerate the outage? The objective is not perfect prediction. The objective is to reduce dependence on perfect prediction.
This perspective connects directly to The Tail at Scale. Dean and Barroso argue that large online systems must create a predictably responsive whole from less predictable parts, because temporary high-latency episodes that appear unimportant in smaller systems can dominate performance at scale. Their proposed tail-tolerant techniques do not rely on eliminating every source of variability. Instead, they alter how the larger system responds to variability through mechanisms such as redundancy, hedging, cancellation, and latency-aware coordination. The architecture is designed to prevent one slow component from determining the outcome of the entire request.
The broader systems lesson is that resilience does not mean preventing every local failure. That is impossible in any sufficiently complex system. Resilience means controlling how local failures affect the whole. Timeouts place limits on how long one component may hold another hostage. Circuit breakers prevent repeated calls to an already failing dependency. Backpressure keeps producers from overwhelming consumers. Idempotency limits the damage caused by retries and duplicate delivery. Bulkheads prevent one workload from exhausting resources needed by unrelated workloads. Queues absorb temporary variation, provided they are bounded and monitored. Graceful degradation allows the system to preserve its essential function when auxiliary capabilities are unavailable. Each of these mechanisms modifies the response function by reducing amplification and bounding loss.
Testing should reflect the same objective. A risk-based test strategy should not merely sample inputs according to their observed frequency. It should deliberately probe the places where the response may become nonlinear. What happens when three retries occur instead of one? What happens when the downstream service is slow rather than completely unavailable? What happens when a timeout occurs after a remote side effect has succeeded but before the caller receives confirmation? What happens when a queue is nearly full? What happens when several individually tolerable failures occur together? These tests are valuable because they examine the structure of the damage function, not merely the frequency distribution of ordinary requests.
Architecture and testing therefore play complementary roles. Testing discovers and verifies how the system responds under stress. Architecture changes the response so that stress produces bounded, predictable consequences. A test may reveal that retry behavior causes request amplification; the architectural response may be exponential backoff, jitter, retry budgets, idempotency keys, circuit breaking, or a redesign that removes the synchronous dependency entirely. Writing a larger number of retry tests without changing the amplifying design would provide information but not resilience. Conversely, adding resilience mechanisms without testing their interaction may simply introduce new failure paths. Thoughtful engineering requires both.
This is also where a distributed collection of services can reveal itself as a distributed monolith. A system may be divided into many Lambdas, containers, or independently deployed services while remaining tightly coupled in behavior. If one component’s failure immediately prevents all others from making progress, if every request traverses a long synchronous chain, or if retries and resource exhaustion propagate across nominal boundaries, then physical decomposition has not produced meaningful failure isolation. The architecture has separated deployment units without reducing systemic exposure. From a fragility perspective, the important question is not how many services exist, but whether the boundaries interrupt or transmit shocks.
A robust service boundary should reduce blast radius. It should make failure semantics explicit, constrain resource use, and prevent one service’s internal instability from becoming another service’s uncontrolled instability. If a boundary merely adds a network call while preserving synchronous assumptions and shared failure modes, it may increase fragility rather than reduce it. More components create more opportunities for delay, partial failure, version disagreement, and operational uncertainty. Decomposition is valuable only when the resulting architecture manages those new risks deliberately.
The candidate did not distinguish between frequency of occurrence and magnitude of consequence. That is a serious gap in systems engineering. His emphasis on common inputs suggested that test priority should follow ordinary usage, but he offered no corresponding model of impact, propagation, or nonlinear harm. He did not ask whether an uncommon case could violate a critical invariant, initiate retries, overload a dependency, corrupt shared state, or create an outage far larger than the original disturbance. Without that analysis, a testing strategy can look efficient while systematically ignoring the cases that matter most.
The proper lesson is not that every rare event deserves exhaustive testing or that engineers should design for every scenario they can imagine. Such a standard would be impossible and wasteful. The lesson is that rarity does not settle priority. Priority emerges from the interaction of probability, consequence, and system structure. An event deserves attention when the system is highly exposed to it, particularly when the resulting harm grows nonlinearly or propagates across boundaries.
A thoughtful engineer therefore asks more than “How often does this happen?” They ask what the event touches, what assumptions it violates, how far it can propagate, what feedback loops it can activate, how recovery behaves, and whether the architecture bounds the resulting loss. They look for thresholds, shared resources, synchronous chains, retry amplification, inconsistent state transitions, and other places where a small disturbance can produce a large outcome. They do not assume that the common case defines the risk profile.
That is the deeper theoretical problem exposed by the interview. The candidate treated testing as a matter of covering representative inputs. I was looking for an engineer who could reason about exposure: an engineer who understood that the same event can be harmless in one architecture and catastrophic in another, that local reliability does not guarantee system reliability, and that good design changes not only whether failures occur but also what failures are allowed to become.
Scalability Is Not a Language Choice
The same reasoning failure appeared again when the interview moved from testing to scalability. I described an application with a FastAPI backend that exposes endpoints for running analytical models, a React frontend that submits scenario configurations and displays results, and a database used to store and retrieve those configurations. The major components run in separate containers, with an eventual plan to deploy them in OpenShift. We were discussing, at a deliberately high level, how such an application might scale if usage increased. Before we had established the number of users, the request rate, the cost of model execution, the latency requirements, or the location of any actual bottleneck, the candidate responded that if the system had “tons of users,” we would not use Python and would simply use Go.
There is a version of that argument that could be reasonable. Go has attractive properties for certain classes of server workloads. It offers an efficient concurrency model, relatively predictable runtime behavior, straightforward deployment, and strong performance for many networked services. Python also has real limitations, particularly in CPU-bound workloads, low-latency execution paths, and situations where interpreter overhead becomes significant relative to the work being performed. I was not troubled because the candidate mentioned Go. I was troubled because he treated the choice of language as though it were the scalability analysis itself.
At the point when he made the recommendation, we had not defined what “scale” meant. That omission is not incidental. A system does not become meaningfully scalable or unscalable in the abstract. It scales relative to a workload and a set of requirements. Ten thousand users who each submit one long-running model job per month create a different engineering problem from ten thousand users who generate continuous interactive traffic. A service that must respond within twenty milliseconds presents a different problem from one in which model execution legitimately takes several minutes. A workload dominated by database reads behaves differently from one dominated by CPU-intensive simulation, GPU inference, large file transfer, or calls to external services. Without characterizing the workload, saying that one language “will not scale” is little more than guesswork.
The first questions should therefore concern demand and behavior, not implementation fashion. How many concurrent users do we expect? What is the arrival rate of requests? Are requests bursty or evenly distributed? How expensive is each model run? Are model executions synchronous, or can they be submitted as jobs and processed asynchronously? What portion of the workload is CPU-bound, I/O-bound, memory-bound, or limited by an external dependency? How much data moves between the frontend, backend, and database? Are identical scenarios executed repeatedly? Does the application need interactive latency, high throughput, or both? What service-level objective are we trying to satisfy, and what level of degradation is acceptable during peak demand?
Until those questions are answered, the phrase “tons of users” has almost no architectural content. It creates the emotional impression of scale without specifying the engineering problem. The candidate responded to that impression with a familiar implementation heuristic: Python is slow, Go is fast, and therefore serious scale requires Go. That is not systems analysis. It is an association among technical stereotypes.
A scalability discussion should begin with measurement or, if the system does not yet exist at scale, with an explicit workload model. The objective is to identify which resource becomes constrained as demand rises. That constrained resource is the bottleneck, and different bottlenecks require different interventions. If the backend spends most of its time waiting for a database query, rewriting the HTTP layer in Go may improve little. If model inference or scientific computation dominates request time, then the language used to route the request may represent a negligible fraction of total latency. If the frontend sends several unnecessary requests for every user action, the API design may be a larger problem than the backend runtime. If requests repeatedly load the same scenario data, caching may provide more value than a rewrite. If the database is serialized around a small number of heavily contended rows, adding faster application servers may simply increase pressure on the actual bottleneck.
This is why scalability is a property of the whole system rather than the programming language. The user does not experience Python, Go, React, PostgreSQL, a load balancer, or OpenShift independently. The user experiences the combined behavior of the request path. That path includes network transit, request parsing, authentication, routing, database access, serialization, model execution, dependency calls, response construction, and frontend rendering. Optimizing one part of the path without knowing its contribution to the whole can produce technically impressive work with almost no meaningful effect.
Suppose, for example, that a typical request spends five milliseconds inside the FastAPI routing layer, fifty milliseconds retrieving configuration from the database, and twenty seconds running a model. Rewriting the five-millisecond portion in a faster language might reduce it to one millisecond, producing an almost invisible improvement in end-to-end latency. The system may still fail under load because the model workers are saturated, the database connection pool is exhausted, or synchronous requests remain open for too long. In that case, the more appropriate architecture might separate request handling from model execution, place work onto a queue, return a job identifier to the frontend, and scale the worker pool independently according to demand. That change would address the structure of the workload rather than merely replacing one runtime with another.
OpenShift also changes the available option space. A containerized application deployed into an orchestration platform can often be replicated horizontally. If the FastAPI service is stateless, multiple instances can process requests behind a router or load balancer. Horizontal pod autoscaling can increase or decrease the number of replicas according to observed load, provided the scaling signal is meaningful and downstream systems can support the additional traffic. Model execution can be separated into its own deployment and scaled according to queue depth, CPU utilization, memory pressure, or another workload-specific measure. The frontend can be served independently, while the database and other stateful components follow their own scaling strategies.
None of this guarantees that the existing Python application will meet every possible requirement. Horizontal scaling does not cure inefficient algorithms, shared-state contention, unbounded memory use, or a database that cannot sustain increased load. It does, however, demonstrate why a language rewrite should not be the first conclusion. There are many architectural interventions available before replacing the implementation wholesale: separating synchronous request handling from asynchronous work, introducing bounded queues, caching repeated results, reducing unnecessary database calls, improving indexes, batching operations, limiting request fan-out, adding backpressure, scaling independent components separately, and profiling the model-serving path. A mature engineer should preserve that option space until evidence narrows it.
Middleware and routing were part of the response I gave during the interview because request routing is one place where efficiency and scale can be handled without replacing the entire application. A reverse proxy, ingress controller, service mesh, or dedicated gateway can distribute traffic, enforce rate limits, terminate connections, perform authentication, and route requests among replicas. These components do not make inefficient application code irrelevant, but they allow the architecture to absorb more demand and separate concerns. The important point is not that middleware always solves scalability. It is that system design offers multiple levers, and the appropriate lever depends on the constraint.
An experienced engineer should also ask what service-level agreement or service-level objective the system must satisfy. “Scalable” has no stable meaning without a target. Does the endpoint need to return within one hundred milliseconds at the ninety-fifth percentile? Is a five-second response acceptable? Can requests be queued during bursts? Must the system support one hundred concurrent users, ten thousand, or one million? Is temporary degradation acceptable if core analysis remains available? What availability is required? What is the maximum tolerable error rate? A design that is more than adequate for one set of objectives may be completely inappropriate for another.
The absence of an SLA in our hypothetical discussion should have made the candidate cautious. Instead, it encouraged certainty. He proposed a major implementation change despite having almost none of the information required to justify it. That reversal is characteristic of Thoughtless Engineering: uncertainty does not produce investigation; it produces a stronger attachment to a familiar heuristic.
The migration cost of a rewrite makes this especially concerning. Rewriting a working backend in another language is not merely a technical substitution. It is an organizational and operational project. Existing behavior must be rediscovered and reproduced. Tests may need to be rewritten or adapted. Libraries and integrations must be replaced. Deployment pipelines, monitoring, debugging practices, and operational runbooks may change. The team must acquire or deepen expertise in the new ecosystem. During the transition, the organization may have to maintain two implementations, reconcile behavioral differences, and decide how to migrate traffic safely.
A rewrite also creates new defects. This point is frequently ignored in discussions that compare languages only through benchmarks. The existing application contains years of accumulated behavior, including behavior that may not be documented anywhere except the code and production environment. Reimplementing it creates opportunities to misunderstand edge cases, contracts, serialization rules, error semantics, database transactions, and dependency interactions. Even if the new language provides better runtime performance, the migration may temporarily reduce reliability and consume engineering capacity that could have been used to improve the current system.
That does not mean rewrites are never justified. Sometimes profiling reveals that the runtime itself is the limiting factor. A latency-critical service may need more predictable pauses, lower overhead, better concurrency, or more efficient memory use than the existing implementation can provide. A CPU-bound component may be unable to meet its throughput target despite algorithmic optimization and horizontal scaling. In those circumstances, a selective reimplementation can be entirely rational. The important word is selective. The evidence may justify rewriting one model-execution component, one serialization-heavy service, or one high-throughput path without discarding the entire backend.
That approach reflects a broader principle of experienced engineering: prefer incremental and reversible interventions when uncertainty is high. Measure the system, identify the constrained resource, make the smallest change likely to address it, and observe the result. A cache can be removed. A worker pool can be resized. A queue can be introduced gradually. One computational kernel can be rewritten and benchmarked behind a stable interface. A complete language migration is a much larger and less reversible bet.
This does not make incrementalism an absolute rule. Large architectural changes sometimes become necessary. But the burden of proof should rise with the cost, risk, and irreversibility of the intervention. The candidate’s recommendation had the opposite relationship to evidence: the more vague the scenario, the more sweeping the proposed solution.
The Python-versus-Go framing also obscures the possibility of heterogeneous systems. Mature engineering organizations rarely insist that every subsystem use the same language regardless of workload. They use different tools where different constraints apply. A Python API may provide orchestration and rapid development, while a performance-critical computational component is implemented in C++, Rust, Go, Java, or another suitable language. A Python service may call optimized native libraries whose execution cost is not governed primarily by the Python interpreter. A model-serving layer may use specialized runtimes, while the control plane remains in a language chosen for productivity and ecosystem support.
The question is therefore not whether Python or Go is universally suitable for production. The question is which parts of the system have which requirements. Language choice should follow from the workload, the runtime characteristics that matter, the available libraries, the skills of the team, the deployment environment, and the cost of change. It should emerge from the analysis rather than substitute for it.
This was the same pattern I had already seen in the testing conversation. The coverage percentage was used as a proxy for reliability without explaining the causal relationship. Here, the programming language was used as a proxy for scalability without identifying the bottleneck. In both cases, a visible technical artifact carried more weight than the underlying system model. The candidate spoke as though naming the technology demonstrated the analysis, when the real engineering work had not yet begun.
“Use Go” is not a scalability strategy. It is a hypothesis about one implementation lever. Like any hypothesis, it may eventually prove correct, but it must be tested against evidence. We need to know where time is spent, which resources saturate, how demand behaves, what service objectives apply, and whether less disruptive changes can meet them. Until then, a language rewrite is not a conclusion. It is one option within a much larger trade space.
A thoughtful engineer preserves that trade space long enough to understand the system. They begin with requirements and measurements, distinguish local performance from end-to-end behavior, and recognize that architecture often dominates language choice. They understand that scaling a request router, a model worker, a database, and a frontend are different problems. They consider migration risk alongside theoretical performance. Most importantly, they do not confuse a familiar implementation preference with a reasoned systems decision.
That was the distinction the candidate failed to demonstrate. He answered a question about architecture with the name of a language. I was looking for an engineer who would first ask what problem the architecture actually had.
Heuristics, Slogans, and the Anti-Python Reflex
The candidate’s immediate jump from “many users” to “we should use Go instead of Python” was not only a premature implementation choice. It was also an example of a broader problem: the misuse of engineering heuristics. I do not object to heuristics. In fact, much of competent engineering depends on them. We rarely have complete information, unlimited time, or the ability to evaluate every possible design from first principles. Experienced engineers carry compressed lessons from previous systems, failures, and trade-offs. Those lessons help them identify likely risks and narrow an otherwise enormous option space. The problem begins when a heuristic stops functioning as a provisional guide and hardens into a universal law.
“Be cautious about Python in CPU-bound, latency-critical execution paths” is a defensible heuristic. It identifies conditions under which Python’s runtime characteristics may matter. It encourages the engineer to investigate interpreter overhead, concurrency constraints, garbage collection, memory consumption, and the cost of crossing language boundaries. It does not claim that Python is always unsuitable. It tells the engineer where to look.
“Production systems should not use Python” is fundamentally different. It treats “production” as though it were a single workload category and assumes that one implementation constraint dominates all others. Production systems include internal automation, data pipelines, web APIs, orchestration services, scientific applications, machine-learning platforms, monitoring systems, administrative tools, model-serving layers, financial analytics, and latency-sensitive execution engines. Their requirements differ dramatically. Some are throughput-sensitive, some are latency-sensitive, some are mostly waiting on networks or databases, and some spend nearly all their time inside optimized native libraries. A rule that treats all of these systems alike is not a useful compression of experience. It is an abandonment of analysis.
A useful heuristic tells you where to look first. A bad heuristic tells you what must be true before you have looked.
The distinction matters because heuristics are always conditional, even when the condition has been forgotten. Most engineering rules of thumb originated in a real class of problems. “Avoid premature optimization” reflects the fact that engineers frequently spend time optimizing code that does not materially affect system performance. “Prefer composition over inheritance” reflects recurring problems with rigid class hierarchies and hidden coupling. “Do not use Python for latency-critical paths” reflects genuine runtime and performance constraints. These statements can be valuable because they summarize repeated experience, but they remain useful only when the engineer remembers the type of problem from which the lesson emerged.
Thoughtless Engineering preserves the slogan and discards the conditions.
The result is a kind of technical reflex in which familiar words substitute for inspection. Python becomes synonymous with slow. Go becomes synonymous with scale. Microservices become synonymous with good architecture. High coverage becomes synonymous with reliability. The engineer no longer asks whether the workload is CPU-bound, whether a network call dominates latency, whether the database is saturated, or whether the computational work happens inside native extensions. The heuristic answers the question before the system has been examined.
This is often presented as pragmatism. The engineer believes they are avoiding overanalysis by applying a proven rule. Sometimes that is exactly what a heuristic allows us to do. But there is a difference between using experience to form an initial hypothesis and using experience to prevent further inquiry. A mature engineer might begin with the suspicion that Python could become a bottleneck in a particular service, then profile the system and revise the judgment according to evidence. An immature engineer begins with the same suspicion and treats it as the conclusion.
My own experience at Bloomberg gave me a very different understanding of how serious engineering organizations choose languages. We had many production systems written in Python. Python was not used indiscriminately, nor was it treated as the correct tool for every subsystem. It was used where its strengths—developer productivity, library availability, readability, integration capabilities, and rapid iteration—were valuable relative to the actual requirements of the work. We did not use it for every pricing engine, every high-frequency path, or every component with an extremely tight response-time requirement. Where latency, determinism, or computational performance imposed stronger constraints, other languages and runtimes were used.
That is not inconsistency. It is engineering judgment.
A mature organization does not need one language to prove its seriousness. It needs an architecture in which different components can be implemented according to different demands. The appropriate language for an orchestration service may not be the appropriate language for a pricing engine. The appropriate language for a research workflow may not be the appropriate language for a market-data feed handler. A system that performs substantial work in databases, remote services, GPUs, or optimized numerical libraries may not be limited primarily by the language of its control layer. The question is not whether Python is suitable for “production.” The question is whether it is suitable for the specific responsibility, workload, latency budget, operational environment, and team that will own the component.
This is why I found a coworker’s response to my Bloomberg example especially revealing. When I mentioned that many production systems there used Python while more latency-sensitive components used other tools, someone dismissed the example by saying that it was simply “a bunch of finance bros not knowing engineering.” The remark was astonishing because it did not challenge any technical claim. It did not ask what those Python systems did, how they performed, what their service objectives were, or why their teams had selected that language. It replaced one slogan with another: finance people do not understand engineering, so evidence from financial systems can be ignored.
That response revealed the same failure mode as the anti-Python reflex. It substituted a social stereotype for systems analysis.
One does not have to romanticize the finance industry to recognize that modern financial infrastructure contains serious engineering challenges. Pricing, market data, order management, risk calculation, regulatory reporting, distributed data processing, fault tolerance, auditability, security, and operational continuity place substantial demands on software systems. Different parts of that environment have very different performance and correctness requirements. Some paths are measured in microseconds; others are batch-oriented, analytical, administrative, or integration-heavy. The idea that an entire industry can be dismissed as technically unserious because of a caricature about “finance bros” is not merely rude. It is intellectually lazy.
More importantly, the dismissal ignored the actual content of the example. I had not claimed that Python was appropriate for every financial workload. I had described a heterogeneous engineering environment in which languages were chosen according to subsystem requirements. That is precisely the kind of discriminating judgment the slogan-driven engineer claims to value. Yet because the example conflicted with an existing identity-based belief about Python, the evidence was rejected without examination.
This is how technical preferences become tribal identities. Engineers begin to use tools not merely as means but as signals about the kind of engineer they believe themselves to be. Go may signal seriousness, scalability, and systems competence. Rust may signal safety and modern low-level rigor. Python may be associated with scripting, data science, or perceived informality. Java may be associated with enterprise bureaucracy. These associations contain fragments of historical truth, but they become destructive when they replace discussion of actual requirements.
Once a tool becomes part of identity, criticism and evidence are processed differently. A benchmark showing one language performing well becomes proof of universal superiority. A production system using another language successfully is dismissed as an exception, incompetence, or evidence that the workload was not “real” enough. Engineers stop comparing trade-offs and begin defending tribes.
Thoughtful engineering resists this tendency because it treats tools as contingent. Languages have properties, ecosystems, limitations, and costs. Those facts matter. But the suitability of a language is always relational: suitable for what task, under what constraints, for which team, within which architecture, and at what migration cost? No language possesses scalability, maintainability, safety, or production readiness in isolation. Those properties emerge from the interaction among the implementation, workload, architecture, operations, and organization.
Heuristics should therefore remain defeasible. A defeasible rule is one we are prepared to revise when the facts of the system do not support it. I may begin with the heuristic that a CPU-intensive service with a strict latency budget should probably not be implemented primarily in Python. But if profiling shows that nearly all computation occurs inside optimized native libraries, the heuristic may not apply in the way I expected. I may begin with the heuristic that a Go service will use fewer resources than an equivalent Python service. But if the team lacks Go expertise, the workload is modest, and the development cost matters more than infrastructure savings, the trade-off may still favor Python.
Defeasibility is not indecision. It is a recognition that engineering claims have domains of validity.
Experienced engineers are often more comfortable with this conditional reasoning because they have seen apparently universal rules fail. They have watched “temporary” systems become permanent, supposedly scalable architectures collapse under organizational complexity, microservices increase coupling rather than reduce it, and rewrites reproduce the same problems in a new language. Experience does not eliminate heuristics; it teaches engineers to hold them with the appropriate degree of confidence.
This is one reason I associate Thoughtless Engineering with premature certainty. The thoughtless engineer often sounds decisive because the heuristic has already eliminated ambiguity. Python is slow. Go scales. Microservices decouple. More tests create clutter. The statements appear strong because they contain no conditions. But removing conditions from an engineering claim does not make it stronger. It usually makes it less accurate.
The mature engineer’s answer may initially sound less dramatic. They may say that Python is probably sufficient for the current workload, that the runtime could become a concern if the service becomes CPU-bound, and that profiling should determine whether selective reimplementation is warranted. That answer is less satisfying to someone seeking a technological slogan, but it is more useful because it identifies the variables that would change the decision.
The broader lesson is not that Python deserves defense as a language. Python has limitations, and pretending otherwise would simply replace one form of thoughtlessness with another. The lesson is that no tool should be accepted or rejected solely through inherited reputation. The engineer must connect the tool’s actual properties to the responsibility of the subsystem. They must understand where performance matters, where development speed matters, where ecosystem support matters, where safety matters, and where operational simplicity matters. Different parts of the same system may produce different answers.
Serious engineering organizations use heterogeneous tools because serious systems contain heterogeneous demands. That heterogeneity is not evidence of architectural disorder by itself. It can be evidence that the organization has resisted the temptation to force every problem into one implementation ideology. The challenge is to manage the operational and cognitive cost of multiple languages without denying that different workloads may genuinely benefit from different tools.
The anti-Python reflex is therefore useful as a case study because it illustrates how a reasonable caution can decay into an unsupported absolute. The original insight—that Python may be unsuitable for some performance-sensitive workloads—is legitimate. The slogan—Python should not be used in production—is not. The first encourages investigation. The second prevents it.
That is the distinction I was trying to draw in the interview and in the later conversation with coworkers. I was not defending Python as the answer to every problem. I was defending the idea that language choice should be derived from the problem. The candidate’s answer moved in the opposite direction. He began with the language and projected its assumed properties onto a system whose requirements had not yet been established.
That is not experience compressed into a heuristic. It is a heuristic standing in for experience.
Naming the Pattern: Thoughtless Engineering
By this point, the individual examples begin to look less like isolated weaknesses and more like expressions of the same underlying habit. The ambiguous test coverage claim, the lack of a defensible stopping rule, the dismissal of uncommon inputs, the shallow treatment of external dependencies, the reflexive preference for Go, and the casual invocation of service boundaries all share a common structure. In each case, a visible technical artifact stood in for the reasoning that should have justified it. A metric replaced a theory of reliability. A language replaced a scalability analysis. A service boundary replaced an architectural argument. A log file replaced an observability strategy. A handful of common inputs replaced a risk model.
I have come to call this pattern Thoughtless Engineering.
Thoughtless Engineering is an engineering posture in which visible technologies, trends, metrics, and implementation details substitute for disciplined reasoning about requirements, system structure, trade spaces, lifecycle, and failure modes. It is not defined by the use of any particular tool or the rejection of any particular practice. Python is not thoughtless. Go is not thoughtful. Microservices are not thoughtful. Monoliths are not thoughtless. High test coverage is not inherently rigorous, and low test coverage is not inherently careless. The thoughtlessness lies in the absence of the argument connecting the choice to the problem.
Thoughtless Engineering is engineering with the causal chain removed.
A legitimate engineering decision has a structure. There is a problem to be solved, a set of stakeholders, a collection of objectives, a set of constraints, and a range of possible interventions. Those interventions have consequences, costs, risks, and dependencies. The engineer’s task is to move through that structure deliberately enough to explain why one design is preferable to another under the circumstances. The final implementation choice may be simple, but the reasoning that supports it should be recoverable.
Thoughtless Engineering collapses that chain. It moves directly from the impression of a problem to a familiar answer. The system needs to scale, so rewrite it in Go. The code needs to be reliable, so increase coverage. The application feels too large, so create service boundaries. Something failed in production, so inspect the logs. Most users provide four common inputs, so test those four. These statements may occasionally produce the correct action, but correctness reached by reflex is not the same as engineering judgment. A guess does not become an argument merely because the guess uses technical vocabulary.
This is why I do not define Thoughtless Engineering as bad coding. Bad code may be the result, but the failure begins earlier. A person can write syntactically clean, idiomatic, well-formatted code while still making thoughtless engineering decisions. The functions may be small, the abstractions fashionable, the tests plentiful, and the deployment fully automated. None of that guarantees that the system was designed around the right requirements, that its risks are understood, or that its architecture supports the objectives it is supposed to achieve.
Conversely, code that looks inelegant in isolation may reflect a careful response to difficult constraints. A system may contain awkward compatibility logic because it must support a long-lived external contract. A service may remain in Python because model execution dominates runtime and a rewrite would create enormous migration risk for negligible performance gain. A larger application may remain a monolith because its parts are tightly coherent, the team is small, and distributing it would create more operational complexity than organizational independence. Surface appearance is not enough to distinguish thoughtful engineering from thoughtless engineering.
Nor is Thoughtless Engineering simply a lack of technical knowledge. Knowledge gaps are normal. No engineer understands every database, runtime, protocol, testing technique, or architectural pattern. An engineer can be unfamiliar with monkey patching, contract testing, tail latency, or OpenShift and still demonstrate excellent judgment by asking the right questions, identifying uncertainty, and reasoning carefully from first principles. In many interviews, I would prefer a candidate who admits they do not know a term but works methodically through the problem over a candidate who confidently recites fashionable concepts without understanding their role.
The important distinction is not how many concepts an engineer can name, but how they use concepts when reasoning. Knowledge becomes engineering judgment only when it is connected to context. A person may know that Go has strong concurrency support, but the engineering question is whether concurrency in the application layer is the relevant constraint. A person may know that microservices encourage independent deployment, but the engineering question is whether the organizational and domain boundaries justify distributing the system. A person may know that coverage can be gamed, but the engineering question is what evidence actually supports confidence in correctness.
Thoughtless Engineering is also not merely junior engineering. It is certainly common among less experienced engineers because experience often teaches caution. Engineers who have lived through failed rewrites, brittle microservice migrations, misleading dashboards, cascading retries, and high-coverage systems that still failed in production are more likely to distrust slogans. They have seen apparently simple interventions create unexpected consequences. They have learned that the answer depends on the workload, the organization, the operating environment, and the shape of the risk.
But seniority does not guarantee thoughtfulness. Experienced engineers can become attached to heuristics that worked in previous environments and apply them mechanically to new ones. They can mistake familiarity for universality. They can impose an architecture because it resembles one they succeeded with before, even though the current problem has different constraints. Senior engineers may be especially dangerous when thoughtless because their confidence carries authority and their preferences can reshape entire systems.
Thoughtless Engineering is therefore best understood as a failure of reasoning rather than a stage of career development. It can appear wherever technical decisions become detached from explicit models of the problem.
At the requirements level, it appears when teams begin discussing frameworks, clouds, databases, or deployment platforms before agreeing on what the system must accomplish. A request for a reporting capability becomes a dashboard project before anyone determines which decisions the reports are supposed to support. A need for better reliability becomes a mandate for more tests without identifying the dominant failure modes. A demand for scalability becomes an infrastructure expansion without a workload model or service-level objective.
At the trade-study level, Thoughtless Engineering appears when alternatives are considered only rhetorically. One option is labeled modern, another legacy; one is scalable, another slow; one is cloud native, another monolithic. The labels do the work that comparison should have done. There is no serious accounting of cost, reversibility, operational burden, team expertise, failure characteristics, or migration risk. The decision may still be presented as a trade-off, but the trade space was never meaningfully explored.
At the architecture level, it appears when patterns are adopted for their reputations rather than for the properties they provide. Microservices are introduced because monoliths are assumed to be immature. Event-driven architecture is chosen because synchronous calls are treated as inherently unsophisticated. Kubernetes is deployed because production is assumed to require orchestration at that level. Service boundaries are drawn without discussing responsibility, ownership, contracts, data consistency, latency, or failure isolation. The architecture becomes a collage of approved patterns rather than a coherent response to the system’s needs.
At the implementation level, Thoughtless Engineering appears when local convenience is allowed to define system behavior. A developer adds a retry because the dependency sometimes fails, without considering retry amplification. Another adds an optional parameter instead of clarifying the domain model. A third catches a broad exception because the failure is inconvenient to classify. Each decision may be understandable in isolation, but no one examines how the accumulation changes the state space, the exception surface, or the operational behavior of the system.
At the testing level, it appears as metric chasing and example accumulation without a correctness model. Teams optimize for line coverage because the number is visible, even if the tests assert little. Engineers test the most obvious cases without asking whether those cases represent the domain. Edge behavior is ignored because it is rare, or exhaustively tested because coverage policy demands it, with no attempt to prioritize according to impact. Integration tests are deferred to a later stage without clear seams, contracts, or responsibility boundaries. Testing becomes an activity rather than an assurance strategy.
At the observability level, Thoughtless Engineering appears when the existence of telemetry is confused with the ability to understand the system. Engineers say that they can “look at the logs” without addressing whether requests can be correlated across services, whether latency distributions are visible, whether error rates can be separated by dependency, or whether saturation and queue growth can be detected before failure. Dashboards accumulate, but no one knows which indicators correspond to user impact or which thresholds should trigger action. The system produces data without producing knowledge.
At the maintenance level, it appears when current implementation speed is valued while future change is treated as somebody else’s problem. Interfaces are left vague because only one caller exists today. Tests are skipped because the original developer understands the code. Operational procedures remain undocumented because the team knows them informally. Dependencies are tightly embedded because replacement seems unlikely. Thoughtless Engineering discounts future costs precisely because those costs are difficult to see at the moment of decision.
At the lifecycle level, it appears when systems are designed as though deployment were the end of engineering rather than the beginning of operation. There is little discussion of versioning, migration, deprecation, ownership, monitoring, rollback, data retention, security updates, capacity growth, or eventual replacement. The initial build receives detailed attention, while the years of maintenance that follow are treated as an abstraction.
The interview examples fit this pattern clearly. “Eighty-four percent coverage” was offered without a risk model, a behavioral explanation, or a clear account of what remained uncovered. The metric was expected to carry the claim of reliability by itself. “Use Go” was proposed before the workload, bottleneck, or service-level objective had been defined. The language was expected to carry the claim of scalability by itself. “Service boundary” was invoked without a discussion of contracts, responsibility, cohesion, failure isolation, or the cost of distributing the system. The phrase was expected to carry the claim of architectural maturity by itself.
The same pattern appears in “just look at the logs.” Logs are a technical artifact, not an operational understanding. Without structured context, correlation, metrics, traces, and an explicit model of what healthy and unhealthy behavior look like, logs may simply provide a large volume of evidence after a failure has already occurred. Treating their existence as an observability strategy is another example of replacing a capability with one of its components.
The statement about testing four normal input variations followed the same structure. It offered a visible selection rule—test what is common—without explaining whether those inputs represented equivalence classes, whether rare cases could violate critical invariants, or whether low-frequency events could produce disproportionate system-level consequences. The engineer had a sampling heuristic but no articulated theory of risk.
Across all these examples, the central failure was not that the proposed action was necessarily wrong. Increasing coverage may have been useful. Go may eventually have been the right language for a particular component. Service boundaries may have been appropriate. Logs may have contained valuable diagnostic information. The four common inputs certainly deserved testing. The problem was that none of those conclusions emerged from an explicit analysis. They appeared as self-justifying answers.
This is what makes Thoughtless Engineering difficult to detect. It often produces plausible decisions. Sometimes it even produces successful systems. A team may choose a fashionable architecture and happen to benefit from it. An engineer may apply a simplistic heuristic to a problem that genuinely fits the heuristic. The absence of reasoning is therefore not always revealed immediately by failure.
The weakness becomes visible when circumstances change. Requirements expand, load patterns shift, dependencies fail in unfamiliar ways, the team grows, or the original assumptions stop holding. A thoughtfully designed system has explicit assumptions that can be revisited. A thoughtlessly designed system has only inherited choices. Future engineers know what was selected but not why it was selected, making it difficult to determine which parts may safely change.
That loss of rationale is one of the most expensive consequences of Thoughtless Engineering. When the causal chain is missing, every future change becomes an exercise in archaeology. Engineers must infer intent from code, configuration, deployment topology, and accidental behavior. They cannot distinguish essential constraints from historical leftovers. A language choice becomes permanent because nobody knows which performance requirement motivated it. A service remains separate because nobody remembers what boundary it was intended to protect. A test suite preserves implementation details because nobody can distinguish the real contract from incidental behavior.
Thoughtful engineering leaves behind more than software. It leaves behind reasons.
Those reasons do not always require long design documents or formal proofs. They can appear in clear tests, architectural decision records, explicit contracts, meaningful interfaces, benchmark results, operational objectives, code comments, or well-structured review discussions. What matters is that the connection between problem and solution remains intelligible.
The positive contrast is therefore not between slow analysis and rapid execution. Thoughtful engineers can make decisions quickly because experience gives them useful heuristics. The difference is that their heuristics remain connected to assumptions and can be challenged by evidence. They know when a rule applies, what it is protecting against, and what facts would invalidate it. They can explain not only what they would do, but why they would do it and what they expect the decision to accomplish.
Thoughtless Engineering cannot provide that explanation because the decision arrived before the analysis. It has the artifact but not the argument, the pattern but not the principle, and the implementation but not the model of the system. It can describe what was built, but not the causal chain that made the design appropriate.
That is the pattern I saw during the interview. The candidate did not simply miss several technical concepts. He repeatedly offered conclusions without exposing the reasoning that should have produced them. Each answer was plausible enough to sound experienced until it was examined closely. Once pressed, the vocabulary remained, but the structure underneath it disappeared.
That absence is what I mean by Thoughtless Engineering.
Microservices, Macro Services, and the Misuse of “Service Boundary”
Another phrase the candidate used repeatedly during the interview was “service boundary.” It appeared as a kind of architectural punctuation mark: when a system seemed too large, complex, or internally connected, the implied solution was to introduce a boundary and separate part of the functionality into another service. The term was often contrasted with “monolith,” as though a monolith represented architectural immaturity and a service boundary represented progress. What was missing was any principled explanation of why a boundary belonged in one place rather than another, what responsibility the new service would own, which dependencies the separation would remove, or what new forms of coordination and failure the boundary would introduce.
That omission matters because “service boundary” is not self-explanatory. A boundary is not good merely because it divides code, deployment units, or teams. It is useful only when it reflects a meaningful distinction in responsibility and creates a system that can be understood, operated, and changed more coherently than before. Without that justification, a service boundary may be nothing more than an arbitrary network hop placed between two pieces of code that remain tightly dependent on one another.
I think part of the confusion around microservices comes from the usual contrast class. People are taught to think in terms of “microservices versus monoliths,” but the term monolith carries so much cultural and implementation baggage that it often obscures the underlying design question. It evokes a giant repository, one deployment artifact, a shared database, a large team, slow releases, and tightly coupled code. Sometimes those associations are accurate, but they are not the conceptual opposite of a microservice.
A more useful contrast is between a microservice and what I would call a macro service.
A macro service is a system with broad responsibility. It performs many distinguishable functions, coordinates many interlocking capabilities, and often answers the question “What does this system do?” with a list. It may authenticate users, manage configuration, run analysis, generate reports, send notifications, persist results, and expose administrative functionality. None of those responsibilities is necessarily implemented badly. The defining feature is that many different kinds of service have accumulated inside one system boundary.
A microservice, by contrast, has a narrow and coherent responsibility. Its internal implementation may still be complicated. It may contain many modules, thousands of lines of code, multiple data structures, and significant domain logic. The important point is not physical size. The important point is that its complexity is organized around one intelligible class of service. When asked why the service exists, an engineer should be able to provide a concise answer that identifies the capability it owns.
A microservice is not a small program. It is a narrowly responsible system.
This distinction immediately improves the way we ask questions about service design. The familiar question “How big should a microservice be?” is usually unhelpful because size is not the primary variable. Lines of code, number of endpoints, database tables, and team size can all provide clues, but none of them defines the boundary. A service may be physically large because its domain is inherently rich while still possessing a clear and unified purpose. Another service may contain only a few hundred lines of code yet represent a poor boundary because it has no meaningful responsibility of its own and exists only to forward requests or mirror another service’s internal abstractions.
The better question is: What responsibility should this service own?
That question forces us to reason about the domain rather than the deployment topology. It asks which concepts belong together, which policies must change together, which data is governed by the same rules, and which behavior can evolve independently. It also asks what the service excludes. A useful boundary does not merely identify what belongs inside; it clarifies which responsibilities belong elsewhere.
Once responsibility becomes the organizing principle, other concepts associated with microservices begin to follow naturally. High cohesion is not an arbitrary rule imposed after decomposition. It is the consequence of placing behavior, data, and policy that serve the same purpose inside the same system. If a service owns one coherent responsibility, its internal parts should largely make sense in relation to that responsibility.
Loose coupling follows for similar reasons. Services with distinct responsibilities should not need intimate knowledge of one another’s implementations. They may depend on each other’s capabilities, but that dependence should pass through explicit interfaces rather than shared internal assumptions. The boundary is valuable because it allows each service to change within its area of responsibility without forcing coordinated changes throughout the entire system.
This is the theory. In practice, many supposed microservice architectures fail to achieve it.
A team may split one application into several repositories and containers while preserving the same coupling that existed before. One service cannot complete a request without synchronously calling three others. Several services share the same database schema. A change to one field requires coordinated deployments across the entire platform. Business rules are scattered across multiple components, and no service has clear ownership of the underlying concept. Each unit is independently deployable in theory, but in reality the system must move as one.
That is a distributed monolith.
The phrase is useful because it distinguishes physical distribution from meaningful independence. A distributed monolith has the operational costs of distributed systems—network failures, serialization, versioning, latency, tracing, deployment coordination, and partial availability—without gaining the principal architectural benefits of independent responsibility and evolution. It has more boundaries but not better boundaries.
This is why splitting a system into services does not automatically reduce complexity. It often relocates complexity from inside components to between components.
Inside a macro service, coordination may happen through ordinary function calls, shared transactions, common memory, and direct access to internal data structures. These mechanisms can create coupling, but they are comparatively visible and immediate. Once the system is decomposed, the same coordination must occur through network calls, asynchronous messages, schemas, retries, timeouts, distributed transactions, replication, and explicit consistency models. The complexity has not vanished. It has changed location and form.
That relocation can be worthwhile when the boundaries align with meaningful responsibilities. Independent deployment, team ownership, failure isolation, and localized change may justify the operational cost. But when the boundaries are arbitrary, the architecture acquires complexity without gaining independence. The result is architectural theater: the outward appearance of modern distributed design without the underlying properties that would make the design valuable.
Container counts, repository counts, endpoint counts, and deployment units can all create the impression that a system has been decomposed thoughtfully. None of them demonstrates that the decomposition is sound. A system can contain fifty tiny services and still be tightly coupled. It can also contain one larger application with strong internal modularity, clear interfaces, and well-separated responsibilities. The number of processes says little about the quality of the architecture.
This is one reason I reject the simplistic contrast between microservices and monoliths. The contrast encourages people to treat distribution as the objective. It suggests that the architectural journey moves from one large thing toward many small things and that the latter arrangement is inherently more mature. The macro-service contrast makes the actual concern clearer. The question is whether one system owns too many distinct responsibilities and whether separating some of them would create more coherent, independently manageable systems.
That framing also helps explain when a macro service may be entirely appropriate. If the responsibilities change together, share strong transactional requirements, are owned by one small team, and have no meaningful need for independent scaling or deployment, keeping them together may be the more thoughtful choice. A well-structured macro service can contain modules with strong internal boundaries without paying the cost of network distribution. There is no virtue in turning a function call into an HTTP request unless the new boundary provides something valuable.
This point is frequently lost in architectural discussions because microservices have become associated with scale, maturity, and modernity. Engineers sometimes assume that a large or important system should therefore consist of many services. But organizational and operational scale matter as much as traffic scale. A small team responsible for dozens of independently deployed services may spend more time coordinating releases, debugging network behavior, maintaining infrastructure, and resolving ownership disputes than improving the actual product.
A service boundary therefore needs a positive justification. It might allow one capability to scale independently because its workload differs dramatically from the rest of the system. It might isolate a high-risk operation so that failure cannot easily spread. It might align ownership with a team that possesses specialized domain knowledge. It might protect a stable contract while allowing the implementation to evolve independently. It might separate data governed by different consistency, security, or regulatory requirements.
What it should not do is exist merely because someone recognized an opportunity to draw a box.
The candidate’s repeated use of “service boundary” never reached this level of reasoning. I did not hear a discussion of cohesion, coupling, ownership, scaling characteristics, data authority, failure isolation, or independent evolution. The phrase appeared as a marker of architectural sophistication rather than the conclusion of a boundary analysis.
That is another example of Thoughtless Engineering. The visible artifact is present: the services are separated. The causal argument is missing: why this boundary, around this responsibility, with these operational consequences?
A thoughtful engineer should be able to explain what becomes easier after the separation and what becomes harder. Perhaps deployments become more independent, but consistency becomes more difficult. Perhaps one service can scale separately, but the request path acquires additional latency. Perhaps the boundary improves team ownership, but observability and incident response become more complex. Perhaps the service can fail independently, but only if upstream systems are actually designed to tolerate that failure.
These are not secondary details. They are the trade.
Every service boundary creates at least two realities. It creates a conceptual boundary between responsibilities, and it creates a technical boundary across which information, control, and failure must pass. The first may simplify the system’s meaning. The second almost always increases operational complexity. A sound architecture gains enough from the conceptual boundary to justify the technical cost.
This is also why “do one thing well” must be interpreted carefully. A microservice is not literally a system with one function, one endpoint, or one method. Even a narrowly responsible service needs persistence, authorization, monitoring, deployment logic, retries, migrations, and other supporting mechanisms. “One thing” refers to a coherent domain purpose, not to the absence of internal complexity.
A notification service, for example, may support multiple channels, templates, delivery policies, retries, preferences, and status tracking. That is a substantial amount of functionality. It can still be a microservice if those capabilities are all organized around the coherent responsibility of delivering and tracking notifications. By contrast, a tiny service that validates one field for another service may be too narrowly extracted to possess meaningful independence. Its existence may add operational overhead while providing no real boundary of responsibility.
The important concept is not smallness. It is integrity of purpose.
This provides a useful test for proposed service boundaries. Can the service’s purpose be stated clearly without describing the internals of several other services? Does it own a coherent set of rules and data? Can it evolve without requiring coordinated modification across the system? Does its interface express a stable capability rather than expose its internal implementation? Can its failure be understood and contained in terms of its responsibility?
If the answer to those questions is no, then the service may be physically separate without being conceptually independent.
That is the distinction the candidate never articulated. He seemed to understand that modern architectures contain boundaries, but not that boundaries require reasons. He treated decomposition as an architectural good in itself, when decomposition is only useful if it produces coherent systems that can stand in meaningful relation to one another.
A macro service concentrates complexity internally. A microservice architecture distributes responsibility but introduces complexity in coordination. Neither is automatically superior. The engineering question is which arrangement best aligns responsibility, change, operation, and risk for the system at hand.
Thoughtful engineering preserves that distinction. It does not ask how many services the architecture should contain. It asks where responsibility naturally begins and ends, and whether making that boundary operationally real creates more value than cost.
Without that reasoning, “service boundary” is not an architectural concept.
It is a buzzword attached to a line on a diagram.
Abstraction, Interfaces, Contracts, and Protocols
Once a system has been divided into services, the quality of the architecture depends heavily on what those services mean to one another. It is not enough to place functionality in separate containers, expose a few endpoints, and call the resulting separation a service boundary. The moment two parts of a system communicate across a boundary, the architecture acquires a new obligation: the relationship between those parts must be made explicit. The abstraction must be coherent, the interface must express it clearly, the contract must define what other systems may rely on, and the protocol must specify how the interaction behaves over time.
These were the concepts I expected to hear when the candidate repeatedly invoked “service boundaries.” A boundary is not simply the place where one codebase ends and another begins. It is where assumptions become promises. Inside a single process, engineers can sometimes get away with ambiguous responsibilities because implementation details remain nearby. One module can inspect another module’s internal state, reuse its types, call its private functions, or compensate for undocumented behavior. Those practices may create coupling, but the coupling can remain hidden because everything exists inside the same technical and organizational context.
A distributed boundary removes that convenience. Once the interaction crosses a network, a queue, a process boundary, or an independently deployed component, every ambiguity becomes more expensive. A vague data structure becomes a versioning problem. An undocumented exception becomes a failed request. An implicit ordering assumption becomes a race condition. A shared interpretation of a field becomes schema drift. A function call that once failed immediately becomes a remote operation that may succeed, fail, time out, complete twice, or finish after the caller has already given up.
Microservices make abstraction concrete because the abstraction must survive separation.
The first question, therefore, is not what endpoint a service should expose. The first question is what kind of service it is. This is the question of abstraction. A service abstraction identifies the coherent role the system plays within the larger architecture. It distinguishes essential capability from incidental implementation. An identity service is not defined by the fact that it happens to expose an HTTP endpoint or store records in a particular database. It is defined by the responsibility it owns: establishing, verifying, and representing identity according to a set of policies. A notification service is not merely a process that calls an email provider. Its abstraction concerns the acceptance, routing, delivery, and tracking of notifications across supported channels.
A clear abstraction determines what belongs inside the service and what should remain outside. It tells us which operations are natural, which data the service should own, which policies it should enforce, and which changes should be internal implementation details. If the abstraction is vague, the boundary will be vague as well. A service called “user service,” for example, may gradually absorb authentication, authorization, profile management, preferences, billing information, notification settings, analytics attributes, and administrative controls. The name sounds coherent because all of these things concern users, but the abstraction is too broad to guide ownership or change. The interface becomes a miscellaneous collection of operations because the service has no precise answer to the question of what kind of service it is.
This is why high cohesion begins at the conceptual level. A cohesive service is not merely one whose source files are related. Its data, operations, policies, and guarantees all derive from the same governing abstraction. When that abstraction is strong, many design decisions become easier. The service can expose behavior that reflects its actual responsibility rather than leaking incidental details of its implementation. When the abstraction is weak, the interface usually becomes a mirror of whatever code and data structures happen to exist internally.
The interface is the external expression of the abstraction. It describes how other systems may interact with the capability without requiring knowledge of how that capability is implemented. An interface may consist of HTTP endpoints, event schemas, command messages, query operations, library functions, or some combination of these. Its technical form matters, but the deeper purpose is to preserve the service’s role while hiding the details that should remain free to change.
A good interface does not simply expose everything the service can do internally. It exposes the operations that make sense from the perspective of the service’s responsibility. If the abstraction concerns payments, the interface might support authorization, capture, refund, and status queries. It should not require callers to understand the service’s internal workflow tables, retry implementation, provider-specific identifiers, or private state transitions unless those concepts are genuinely part of the public domain. The interface should allow the service to improve or replace those internals without forcing every consumer to change.
This is where dependency direction becomes important. A service should not force its consumers to depend on its internal representation more than necessary. When an interface exposes database models, framework-specific types, private error codes, or implementation-specific workflow stages, dependency begins to point in the wrong direction. Consumers become coupled to how the service happens to work rather than to what it promises to provide. The service may still be independently deployed, but it is no longer independently evolvable.
A clean interface narrows that dependency. Consumers depend on a stable capability and a stable vocabulary. The implementation remains free to change behind the boundary, provided it continues to satisfy the public meaning of the interface. This is one of the principal reasons abstractions exist: they create a controlled separation between what must remain stable and what may evolve.
An interface alone, however, is not a contract. An endpoint specification may tell me that I can send a request to /analysis with a particular JSON body and receive a response with certain fields. That describes the mechanical shape of the interaction, but it does not necessarily tell me what the fields mean, what guarantees accompany the response, which errors may occur, or what behavior I should expect under failure. The contract is the larger set of commitments that other systems may safely rely on.
A contract includes the accepted inputs, the meaning of outputs, validation rules, error semantics, consistency guarantees, authorization requirements, idempotency behavior, compatibility expectations, and sometimes latency or availability commitments. It defines what counts as successful behavior and what kinds of failure are part of the normal interface. It also defines what the service does not promise. These negative guarantees matter because consumers often build dependencies around assumptions that were never intentionally offered.
Consider a service that accepts a request to run an analytical model. The interface might specify the request and response schemas. The contract must answer deeper questions. Does acceptance mean the model has completed, or only that a job has been created? If the same request is submitted twice, will the service create two jobs or return the existing result? Is the operation idempotent only when an idempotency key is supplied? Can results be partial? What does a timeout mean: that execution failed, that the caller stopped waiting, or that the final outcome is unknown? How long are results retained? Can the consumer assume that a successful response corresponds to a persisted result? What happens when the requested model version is unavailable?
Without answers to those questions, consumers will invent their own. One client may retry every timeout and accidentally create duplicate work. Another may interpret an accepted request as completed processing. A third may assume that missing fields indicate zero values rather than partial results. The service may appear to work during ordinary testing while its consumers quietly accumulate incompatible assumptions.
That is why contracts are not bureaucratic additions to interfaces. They are the mechanism through which independently developed systems preserve shared meaning.
Error semantics are an especially important part of the contract. A service should distinguish among invalid requests, unauthorized requests, unavailable dependencies, internal defects, conflicts, exhausted capacity, and outcomes that are negative but entirely valid within the domain. If every failure becomes a generic server error, callers cannot respond intelligently. They may retry conditions that will never succeed, fail permanently on conditions that were temporary, or hide domain outcomes that should have been shown to users.
The error model must also describe uncertainty. In a local function call, an exception often means that the operation did not complete. Across a network boundary, that inference may be false. A caller may time out after the remote service has already committed a side effect. The absence of a response does not necessarily imply the absence of an outcome. If the contract does not address this possibility, retry behavior can produce duplicates, conflicting state, and other failures that emerge only under abnormal conditions.
This leads naturally to protocol. An interface describes available interactions, and a contract describes their guarantees, but a protocol describes how those interactions unfold over time. Many service relationships cannot be understood as isolated request-response pairs. They are sequences with ordering rules, state transitions, acknowledgments, retries, expirations, and recovery behavior.
A job-processing service, for example, may define a protocol in which a client submits a job, receives an identifier, polls or subscribes for status updates, retrieves results after completion, and eventually acknowledges or discards them. The protocol must define valid state transitions, whether updates may be repeated or delivered out of order, how cancellation behaves, what happens after expiration, and how consumers recover if they miss an event. The individual endpoints or messages are only pieces of the interaction. Correctness belongs to the sequence.
This distinction becomes even more important in asynchronous systems. A message schema may describe an event, but the protocol must address delivery semantics. Can the message be delivered more than once? Can messages arrive out of order? Can a consumer process them concurrently? Is acknowledgment required? What happens when processing fails after a side effect has occurred but before acknowledgment is recorded? Is ordering guaranteed globally, per entity, or not at all? These questions determine whether the consumer must be idempotent, whether it needs deduplication, and whether state transitions can be safely reconstructed.
Engineers sometimes speak casually of guarantees such as “exactly once,” but distributed systems make such claims difficult and highly conditional. More commonly, the architecture offers at-least-once delivery, at-most-once delivery, or effectively-once outcomes created through idempotency and deduplication. A thoughtful engineer should understand that these are not details to be decided after implementation. They are properties of the protocol that influence storage, error handling, retry strategy, and the behavior of every consumer.
Versioning is another place where weak abstractions and informal contracts become operational problems. Independently deployed services rarely change simultaneously. One producer may publish a new field while older consumers are still running. A consumer may begin relying on behavior that a provider considers incidental. A schema may evolve in a way that remains syntactically valid while changing the meaning of a value. Without a compatibility strategy, independent deployment becomes fictional because every change requires coordinated release.
A sound service boundary therefore includes a theory of evolution. Which changes are backward compatible? May fields be added? May existing fields change meaning? How are old versions deprecated? How long are consumers allowed to migrate? Are unknown fields ignored or rejected? Can the provider support multiple versions simultaneously? Does the contract include semantic guarantees that cannot be captured in a schema alone?
This is one reason contract testing is so valuable. A schema can verify that a payload has the expected shape, but contract tests can verify that the assumptions made by consumers remain compatible with the behavior offered by providers. They help expose drift before deployment and turn otherwise informal expectations into executable checks. Contract testing does not eliminate the need for integration tests, but it gives service relationships a level of explicitness that ordinary endpoint testing often lacks.
Communication guarantees also shape the system’s failure behavior. A synchronous interface creates temporal coupling: the caller and provider must both be available at the same time. An asynchronous interface may reduce that coupling but introduce delayed failure, queue accumulation, duplicate delivery, and eventual consistency. Neither approach is inherently superior. The correct choice depends on the service’s semantics, latency requirements, consistency needs, and tolerance for partial availability.
A thoughtful engineer should be able to explain those trade-offs. If a workflow is synchronous, what happens when the dependency is slow? Are timeouts bounded? Are retries safe? Can failure be degraded gracefully? If the workflow is asynchronous, how does the caller learn the outcome? What state is visible while processing continues? How are failed messages handled? How does the system distinguish delayed work from lost work? These questions belong to the protocol and contract, not merely to deployment configuration.
The candidate’s use of “service boundary” did not include any of this reasoning. I heard the language of decomposition without a corresponding account of abstraction, interface design, guarantees, or evolution. There was no discussion of what a service promised, how consumers should interpret failures, how compatibility would be preserved, or how the interaction behaved across time. The boundary appeared to be a structural fact rather than a relationship that needed to be designed.
That is a serious omission because arbitrary service decomposition can make conceptual ambiguity operationally expensive. When two modules inside one application disagree about the meaning of a field, the defect may be found quickly in a local test. When two independently deployed services disagree, the result may be a production incident involving retries, partial state, silent data loss, or cascading failure. The network does not merely connect abstractions. It exposes whether the abstractions were ever clear.
Microservices make abstraction concrete. Once systems communicate across boundaries, conceptual confusion becomes operational failure.
Without clear abstractions, services lack coherent responsibility. Without clear interfaces, consumers depend on implementation details. Without contracts, each consumer invents its own assumptions. Without protocols, correct local operations can compose into incorrect workflows. Without versioning discipline, independent evolution becomes coordinated fragility. Without explicit communication guarantees, retries, timeouts, and partial failures become sources of accidental behavior.
This is why microservices should not be understood simply as small applications with APIs. They are participants in a system of negotiated responsibilities. Their value depends on the clarity and stability of those negotiations. The architecture is not defined only by the services themselves, but by the promises, dependencies, and protocols that connect them.
A service boundary is meaningful only when it carries a coherent abstraction. The interface expresses that abstraction to the outside world. The contract defines what other systems may rely upon. The protocol describes how those commitments behave through time, failure, retry, and change. These are not optional refinements added after decomposition. They are what make the decomposition real.
Without them, microservices are merely network-separated code fragments.
Systems of Systems: Synchronization, Emergence, and Netflix
The discussion of service abstractions, interfaces, contracts, and protocols still leaves one major architectural question unanswered: how do these independently understandable services become a coherent system? It is possible to design each service well in isolation and still produce a platform that behaves poorly as a whole. A service may have a clear responsibility, a stable interface, sensible error semantics, and strong local tests, yet participate in workflows that are slow, inconsistent, fragile, or impossible to operate. The quality of a distributed platform therefore cannot be inferred merely by inspecting the quality of its components. The larger system exists in the organization of their interactions.
A platform such as Netflix makes this distinction easy to see. When we ask, “What is Netflix?” there is no single service that provides an adequate answer. Netflix is not merely a video playback service, a recommendation engine, an identity system, a billing platform, a content catalog, a search system, or a content-delivery network. Each of those capabilities contributes to the experience, but none of them is identical to the system as a whole. What users recognize as Netflix emerges from the coordinated behavior of many specialized systems that together allow someone to authenticate, browse a catalog, receive recommendations, select content, obtain authorization, stream media, change devices, resume playback, and maintain a coherent account experience.
The “real system” is therefore not any individual component. It is the organized relationship among components.
This is what it means to describe a distributed platform as a system of systems. Its parts are not merely modules inside a larger program. They are systems in their own right, often with separate owners, data stores, deployment schedules, operational objectives, and internal architectures. Each can be understood locally and may be capable of changing without requiring the entire platform to change at the same time. At the same time, none of these systems is fully independent in the ordinary sense. Their value depends on participation in a larger functional order.
Herbert Simon’s idea of a nearly decomposable system is useful here. In a nearly decomposable system, interactions within a subsystem are generally stronger or more frequent than interactions between subsystems, allowing the parts to be studied and changed with some degree of independence. Yet the subsystems remain coordinated enough to produce the behavior of the whole. This is an important qualification. The system is nearly decomposable, not completely decomposable. If the parts were truly independent, there would be no larger platform to discuss. If they were completely inseparable, there would be no meaningful service boundaries.
A well-designed distributed platform occupies the space between those extremes. Each service has enough autonomy to preserve a coherent local identity, but enough synchronization with the others to contribute to a stable global capability. The architecture must therefore solve two problems simultaneously. It must create differentiation so that services can specialize, evolve, and fail independently, and it must create integration so that those differentiated services still behave as one intelligible product.
This is why the hardest part of microservice architecture is not decomposition alone. Decomposition is comparatively easy. A team can separate repositories, create APIs, place components into containers, and deploy them independently. The more difficult problem is recomposition: defining how those independently operating systems cooperate to produce a result that is correct from the user’s perspective.
A microservice architecture decomposes functionality into relatively independent systems. A distributed platform recomposes those systems into a coherent operational whole.
That recomposition depends on synchronization, although synchronization should not be understood only in the narrow technical sense of locks, clocks, or simultaneous execution. At the system level, synchronization means aligning differentiated capabilities toward a common objective. It includes agreement about identity, state, timing, sequence, authority, failure, and meaning. Services must agree sufficiently about who a user is, which content is available, what a subscription permits, whether an operation completed, which version of an entity is current, and what should happen when some part of the platform is delayed or unavailable.
The word “sufficiently” matters because perfect global agreement is often impossible or prohibitively expensive. Distributed systems operate under latency, partial failure, independent deployment, and inconsistent visibility of state. The goal is not necessarily to force every component to possess an identical view of the world at every instant. The goal is to define where agreement is essential, where delay is tolerable, where approximation is acceptable, and how inconsistencies are detected and resolved.
Consider a recommendation system. Recommendations may be slightly stale without making the platform incorrect in any serious sense. A recently watched title might remain visible for a short time, or a new preference may take several minutes to influence the recommendation set. That temporary inconsistency may be acceptable because the recommendation service supports the experience rather than authoritatively defining account state. Subscription entitlement is different. If a billing service records that an account is no longer entitled to access content while a playback authorization service continues to grant access indefinitely, the inconsistency affects a core business and security rule. The architecture should therefore synchronize these classes of information differently because the consequences of disagreement differ.
This is where system-level reasoning becomes more important than local implementation quality. Each service may be correct according to its own view of the world while the platform remains globally incorrect. The billing service may correctly record the subscription state. The playback service may correctly apply the most recent entitlement state it has received. The messaging system may correctly delay delivery during an outage. Every component can satisfy its local contract, yet the user may still receive an outcome the business considers wrong.
That possibility exposes the limits of evaluating distributed systems through component-level tests alone. Local tests can show that each service preserves its invariants. Contract tests can show that messages and requests remain compatible. Integration tests can show that particular workflows succeed under controlled conditions. None of these automatically proves that the platform’s emergent behavior remains acceptable under delay, partial failure, version skew, load, and independent change. The architecture needs an explicit theory of how local correctness composes into global correctness.
The candidate’s repeated use of “service boundary” never developed into a discussion of this larger composition problem. I heard enthusiasm for separation, but little consideration of how independently deployed services would maintain coherent behavior. There was no meaningful discussion of coordination, state ownership, consistency, workflow orchestration, event ordering, partial completion, or degraded operation. This omission matters because service decomposition creates new coordination problems even when the boundaries themselves are sound.
A platform fails globally when those coordination problems are left implicit. Contracts may drift as providers and consumers evolve independently. Latencies may compound across synchronous chains until a workflow that looks fast at the component level becomes slow for the user. Data may become inconsistent because several services believe they own different representations of the same concept. Retries may repeat side effects. Events may arrive out of order. One service may optimize for throughput by batching work while another depends on prompt updates. Each local decision can be reasonable while the combined outcome undermines the platform’s objective.
This is one of the characteristic dangers of local optimization. A team responsible for one service may improve its own latency, availability, cost, or development velocity in a way that shifts risk elsewhere. A service may introduce aggressive caching to reduce database load, thereby serving stale information that another workflow assumes is current. A team may increase retry counts to improve its local success rate, thereby amplifying load on a struggling dependency. A component may batch events for efficiency, thereby violating the latency assumptions of downstream consumers. From the perspective of each team’s dashboard, the change may look beneficial. From the perspective of the platform, it may be harmful.
The system therefore needs mechanisms for preserving global coherence across local autonomy. Some of those mechanisms are technical: contracts, schemas, orchestration, event choreography, idempotency, backpressure, distributed tracing, service-level objectives, dependency budgets, and explicit ownership of authoritative data. Others are organizational: architecture review, shared operational standards, cross-team incident analysis, compatibility policies, and agreement about which outcomes matter at the platform level. A distributed platform is not maintained by code alone. It is maintained by an institutional capacity to coordinate independent change.
This is also why resilience should be understood as persistence through change rather than merely survival during failure. Netflix remains Netflix even though individual services are deployed, replaced, scaled, degraded, or temporarily unavailable. The platform’s identity persists because the architecture allows parts to change while preserving the larger pattern of capability. The user does not need every component to remain identical. The user needs the experience to remain coherent enough that the system continues fulfilling its purpose.
That persistence depends partly on graceful degradation. A recommendation subsystem may be unavailable while basic browsing and playback continue. A personalization feature may fall back to generic content. A nonessential analytics pipeline may lag without interrupting the user experience. These outcomes are possible only when the architecture distinguishes essential platform capabilities from auxiliary ones and prevents failures in less critical services from disabling the entire system.
A system of services that cannot tolerate the loss of any one participant is not meaningfully resilient merely because it is distributed. It may instead be a distributed monolith whose failure dependencies remain global. If every user request traverses a long synchronous chain and every service must be available for the workflow to succeed, then the platform has converted internal function calls into network calls without gaining useful autonomy. The services are separately deployed but not operationally separable.
Nearly decomposable architecture requires more than named boundaries. It requires that some failures remain local, some changes remain local, and some decisions remain local. At the same time, the architecture must identify the interactions where local autonomy must yield to global constraints. A payment service cannot unilaterally redefine currency semantics. An identity service cannot change token meaning without considering its consumers. A playback service cannot optimize availability by ignoring authorization. Local independence is valuable only inside the boundaries of the platform’s shared objectives.
The challenge is therefore one of controlled autonomy. Services should be free to evolve internally, but not free to violate the assumptions that allow the platform to function. They should optimize their local responsibilities, but not at the expense of system-level behavior. They should fail independently where possible, but communicate clearly when their failure affects the larger workflow. The architecture must preserve enough separation for adaptation and enough synchronization for coherence.
This is where emergence becomes more than an abstract systems concept. Emergent capability is the behavior produced by the organized interaction of parts that no single part can provide alone. No individual Netflix service creates the full streaming experience. The experience emerges from the coordination of identity, catalog, recommendation, entitlement, playback, telemetry, delivery, and many other capabilities. The platform’s quality is therefore partly a property of relationships: the timing of calls, the interpretation of state, the handling of failure, and the compatibility of change.
Emergence can produce desirable capabilities, but it can also produce undesirable failure modes. Cascading outages, retry storms, inconsistent state, and latency amplification are emergent behaviors too. No team necessarily designed them, and no single service may contain the defect. They arise from the way independently reasonable components interact. A mature systems engineer understands that architecture produces both intended and unintended emergence.
This is another reason the candidate’s local focus concerned me. His answers remained centered on individual technologies, functions, tests, and boundaries. I did not hear a model of the platform as a coordinated whole. Yet once a system is distributed, the most consequential behavior often exists between the components rather than inside them. The difficult questions concern how services combine, how failures propagate, how state converges, and how the system continues serving its purpose while individual parts change.
The positive case for microservices lies partly in their ability to support adaptation. A coherent service with a stable contract can be rewritten, rescaled, replaced, or reorganized without requiring the entire platform to be rebuilt. Teams can change local implementations while preserving global capabilities. Different services can respond to different workloads. Failures can be isolated. Technologies can vary according to need. This is the architectural promise.
But that promise is realized only when recomposition receives as much attention as decomposition. The system needs explicit contracts, synchronization strategies, observability across boundaries, and a clear account of which global outcomes matter. Otherwise, the platform becomes a collection of local systems whose interactions are governed by accident.
Netflix is useful as an example because its visible identity is stable while its internal machinery is highly differentiated and continuously changing. The persistence of the platform does not come from freezing its components. It comes from coordinating change so that the larger capability survives. That is a more sophisticated form of resilience than merely keeping individual processes alive.
A thoughtful engineer discussing service boundaries should therefore be able to move between levels of analysis. At the local level, they should explain the responsibility and contract of an individual service. At the platform level, they should explain how the service participates in broader workflows, which assumptions connect it to others, how its failures are contained, and how its changes preserve global behavior. Architecture requires both perspectives.
The candidate remained almost entirely at the first level, and even there the discussion of responsibility and contracts was thin. The missing platform-level perspective made the use of “service boundary” feel especially superficial. Boundaries matter not only because they separate systems, but because they define the terms under which systems can be recombined.
That is the deeper lesson. A distributed platform is not a pile of services. It is an organized pattern of cooperation among services. Its identity lies in that pattern, its capabilities emerge from that pattern, and its failures often emerge from that pattern as well.
The engineering challenge is not merely to create independent parts. It is to make those parts independent enough to evolve and coordinated enough to remain one system.
Observability Is Not “Just Looking at Logs”
The same pattern of shallow reasoning appeared when the discussion turned to production behavior and diagnosis. When I asked how the system would be understood after deployment, the answer was essentially that we could look at the logs. Logs are certainly useful, and I would be concerned by any production system that did not produce them. But “look at the logs” is not an observability strategy. It names one source of evidence without explaining how that evidence helps engineers infer the state of a distributed system, distinguish one failure mode from another, or detect degradation before users report it.
Observability is the ability to infer what is happening inside a system from the signals it exposes. The term originated in control theory, where a system is observable when its internal state can be reconstructed from its outputs. Software systems are not identical to mathematical control systems, but the underlying idea remains useful. A service is observable when engineers can examine its external signals and form a reliable account of what it is doing, why it is doing it, and where its behavior diverges from expectations. That requires more than the existence of textual records. It requires a deliberate relationship among instrumentation, system architecture, operational objectives, and diagnostic questions.
Logs answer certain questions well. They can record that a request was received, that validation failed, that a dependency returned an error, that a retry occurred, or that an unexpected exception was raised. Structured logs can attach fields such as request identifiers, user or tenant identifiers, service versions, dependency names, error categories, and execution duration. When those fields are consistent, logs can support detailed investigation. They can help reconstruct the sequence of events surrounding a specific failure and preserve contextual information that would be difficult to represent in a simple metric.
But logs also have serious limitations. They tend to describe individual events rather than the statistical behavior of the system. A log file may tell me that a timeout occurred, but not whether timeout frequency has doubled over the last hour. It may tell me that one database query was slow, but not whether the connection pool is approaching saturation. It may show several retries, but not whether retry traffic is amplifying load across the platform. It may contain all the necessary evidence, but at a volume and level of detail that makes the relevant pattern nearly impossible to recognize during an incident.
This is where metrics become necessary. Metrics compress repeated behavior into quantities that can be tracked over time. Request counts, latency distributions, error rates, CPU utilization, memory pressure, queue depth, connection-pool usage, cache hit rate, retry volume, and dependency availability provide different views of the system’s condition. They allow engineers to see trends, compare current behavior with historical baselines, and identify thresholds that precede failure. Logs may explain a particular event, while metrics reveal that the event is part of a larger pattern.
The distinction matters because operational failures rarely begin with a single dramatic exception. They often begin as gradual changes in system behavior. Latency increases slightly. Queue depth grows. A dependency begins returning more throttling responses. Retries increase. Worker utilization approaches its limit. The system continues to function, but with less margin. By the time users begin reporting errors, the causal chain may have been developing for hours. A mature observability strategy should make that deterioration visible before the final failure.
Latency is a good example. Reporting average response time is usually insufficient because averages conceal the distribution. A service may have an acceptable mean latency while a meaningful fraction of users experiences severe delays. Percentiles such as the fiftieth, ninety-fifth, ninety-ninth, or ninety-ninth-point-ninth percentile reveal different parts of that distribution. In a distributed system, the tail is often where degradation first becomes operationally important. If observability captures only averages, it may systematically hide the same tail-risk behavior discussed earlier in this essay.
Error rate requires similar care. A single aggregate error count may tell us that requests are failing, but not why. Are failures concentrated in one endpoint, one model, one customer, one deployment version, or one downstream dependency? Are they validation failures caused by bad input, internal exceptions caused by defects, or timeouts caused by external degradation? A useful observability system preserves enough dimensions to separate those categories without producing an unmanageable explosion of data.
Saturation is another essential concept. A service can appear healthy while running dangerously close to its limits. CPU usage, memory consumption, active connections, thread or worker utilization, queue depth, and concurrency limits help reveal how much capacity remains. This matters because nonlinear failure often begins at saturation points. A queue that is ninety percent full is not necessarily only ten percent away from trouble in any practical sense. Once consumers fall behind, latency may rise, retries may increase, and backlog may compound. The system’s response can change abruptly after a threshold is crossed.
These operational signals should be connected to explicit service objectives. Service-level indicators, or SLIs, are measurements of behavior that matters to users or dependent systems, such as successful-request rate, latency, data freshness, or job-completion time. Service-level objectives, or SLOs, define the acceptable target for those indicators. A team might decide that 99.9 percent of analysis requests should complete successfully within a particular time window, or that queued model jobs should begin execution within a defined delay. These objectives turn observability from passive data collection into an operational model of acceptable behavior.
Without an SLO, a dashboard may show that latency is 800 milliseconds, but the significance of that number remains unclear. Is 800 milliseconds excellent, tolerable, or already a violation? Without a defined objective, alerting tends to become either too noisy or too weak. Teams alert on arbitrary resource thresholds, ignore repeated false positives, and eventually discover that the system can violate user expectations while every infrastructure graph remains green. Thoughtful observability begins with the question of what successful service actually means.
Alerts should therefore correspond to meaningful conditions rather than merely to the presence of unusual data. An alert is useful when it indicates that action may be required. A temporary CPU spike may not deserve attention if the system remains within its latency and error objectives. A small but sustained increase in failed requests may deserve immediate investigation if it threatens an important workflow. Alert design requires the same kind of trade-off reasoning as testing and architecture: what signal matters, what threshold indicates risk, how quickly must someone respond, and what evidence should accompany the notification?
Distributed systems make these questions more difficult because a single user operation may traverse several services. A frontend request may pass through an API gateway, an authentication service, a FastAPI backend, a database, a queue, a model worker, and one or more external dependencies before the user sees a result. Each component may produce its own logs and metrics, yet the user experiences one operation. If engineers cannot connect the signals across those boundaries, they may see several apparently unrelated symptoms without understanding the common request that links them.
Correlation identifiers provide one mechanism for preserving that connection. A request identifier can be generated at the system boundary and propagated through downstream calls, messages, and background jobs. Each service includes that identifier in its logs, allowing an engineer to reconstruct the path of one operation across the platform. In asynchronous systems, additional identifiers may be needed to connect a submitted job with later worker execution, result storage, and notification. Without consistent propagation, debugging becomes a manual search through timestamps, guessed relationships, and partial evidence.
Distributed tracing extends this idea by representing a request as a collection of spans. Each span records an operation, its duration, its parent-child relationship, and relevant attributes. A trace can show that a request spent five milliseconds in routing, forty milliseconds in the database, two seconds waiting for model execution, and another half-second in an external API. It can reveal parallel work, repeated calls, retry behavior, and the point where latency accumulated. This is especially useful in service-oriented architectures because the performance of the whole cannot be inferred reliably from the performance of each service in isolation.
Tracing does not replace logs or metrics. Each signal serves a different purpose. Metrics are effective for detecting trends and deviations across many requests. Traces explain the path and timing of selected operations. Logs provide detailed event context. A mature observability strategy combines them so that an engineer can move from detection to localization to explanation. A metric may show that tail latency has increased. A trace may reveal that the increase occurs in calls to one dependency. Logs from that dependency interaction may then reveal throttling responses, malformed payloads, or exhausted connections.
The architecture should support that investigative flow intentionally. Instrumentation added as an afterthought often produces inconsistent names, missing identifiers, incompatible timestamps, and large volumes of low-value data. One service reports duration in milliseconds, another in seconds, and a third records only start and completion messages. Error categories differ across teams. Dependency calls are not labeled consistently. Important state transitions are silent, while routine messages flood the logs. The system technically emits telemetry, but it still cannot explain itself.
Observability must also account for semantic behavior, not only infrastructure health. A service may have low CPU usage, fast response times, and no exceptions while producing incorrect business outcomes. A model pipeline may complete successfully but use the wrong scenario configuration. A notification service may return success while delivering messages to the wrong channel. An entitlement system may respond quickly while granting access based on stale state. Operational visibility therefore needs domain-level indicators as well as technical ones. The question is not only whether the process is alive, but whether the system is fulfilling its intended purpose.
This connects observability to the earlier discussion of semantic correctness. We should measure the invariants and outcomes that matter. How many jobs enter each terminal state? How often are results marked partial? How many duplicate requests are deduplicated? How often does the system fall back to degraded behavior? How many responses rely on stale data? Which model versions produce which error classes? These measurements help reveal failures that ordinary infrastructure dashboards would miss.
Dependency health deserves similar attention. A service may appear to have an internal error problem when the real issue is a downstream system. Metrics should separate internal processing time from dependency time, identify timeout and retry rates by dependency, and show how external failures affect end-to-end service behavior. This is especially important because retries can temporarily hide dependency instability while increasing total load. A simple success-rate metric may remain acceptable even as the system consumes growing resources to preserve it.
Queue-based systems require additional signals. Queue depth, message age, consumer throughput, retry counts, dead-letter volume, and processing duration help distinguish ordinary bursts from sustained backlog. A queue containing one thousand messages may be healthy if consumers clear it in seconds, or disastrous if the oldest message has been waiting for hours. The raw count is less informative than its relationship to arrival rate, processing rate, and service objectives.
The candidate’s “look at the logs” answer did not reach any of these concerns. It named an artifact but did not explain the diagnostic model around it. There was no discussion of which events should be logged, how requests would be correlated, what metrics would indicate degradation, how distributed calls would be traced, which service objectives mattered, or how alerts would distinguish actionable failures from ordinary variation. The answer assumed that because evidence existed somewhere, understanding would follow.
That assumption is rarely justified. During an incident, engineers operate under time pressure and uncertainty. They need the system to reduce ambiguity rather than produce more of it. They need to know whether the problem is local or systemic, whether it began after a deployment, whether it affects all users or one workflow, whether retries are helping or causing harm, and whether the system is approaching a broader failure threshold. Good observability shortens the distance between symptom and explanation.
It also changes engineering before incidents occur. When teams can see latency distributions, dependency behavior, saturation, and domain outcomes, they make better architectural decisions. They can verify whether a cache helped, whether a rewrite improved the relevant path, whether horizontal scaling reduced queue delay, or whether a new service boundary introduced excessive network latency. Observability supplies the feedback required to test architectural hypotheses against actual system behavior.
This makes observability part of systems reasoning rather than an operational accessory. Architecture determines what can fail, how failure propagates, and which relationships need to be visible. Observability determines whether engineers can recognize those dynamics in the running system. A service boundary without cross-service visibility can become a boundary of ignorance. Independent deployment without version and trace information can make regressions difficult to isolate. Asynchronous processing without job correlation can make work appear to disappear.
Observability is not the existence of logs. It is the system’s capacity to explain itself under stress.
A thoughtful engineer should therefore discuss observability in terms of questions and evidence. What does healthy behavior look like? Which indicators correspond to user experience? How will we detect slow degradation? Can one request be followed across service boundaries? Can we distinguish dependency failure from internal failure? Do alerts correspond to objectives? Can we observe queues, retries, saturation, and degraded modes? Can we determine which version of the system produced a result? Can the platform reveal not only that it failed, but how the failure developed?
“Just look at the logs” does not answer those questions. It is another instance of Thoughtless Engineering: a visible technical artifact substituted for the reasoning that gives the artifact value. Logs are one instrument in an observability system. Without metrics, traces, correlation, objectives, and a deliberate diagnostic model, they may tell us many things while still failing to tell us what we most need to know.
The Positive Standard: What Thoughtful Engineering Looks Like
An essay about Thoughtless Engineering would be incomplete if it ended only with criticism. It is easy to identify shallow reasoning after a system fails, after a rewrite goes badly, or after an interview answer collapses under scrutiny. The more useful task is to define the positive standard: what does thoughtful engineering actually look like in practice?
Thoughtful engineering is not a particular methodology, architecture, language, or set of tools. It is a disciplined way of connecting decisions to the system they are intended to serve. It begins with the recognition that technical choices are not self-justifying. A tool becomes appropriate only in relation to a problem, a constraint, a risk, and an objective.
This does not mean tools are unimportant. Languages have different runtime characteristics. Databases provide different consistency and scaling properties. Deployment platforms create different operational capabilities. Testing techniques provide different forms of confidence. Architectural boundaries affect ownership, coupling, latency, and failure propagation. Thoughtful engineering does not pretend these differences disappear.
The distinction is that a thoughtful engineer derives the relevance of the tool from the system rather than deriving the design of the system from an attachment to the tool.
A thoughtful engineer may recommend Go, but only after identifying why Go’s properties matter to the workload. They may recommend higher test coverage, but only after explaining which risks the additional tests address. They may introduce a service boundary, but only after defining the responsibility, contract, and failure isolation the boundary is meant to create. They may accept a risk, but only after considering its probability, consequence, blast radius, and available mitigations.
The difference is not vocabulary. It is causal reasoning.
The Engineering Chain
Thoughtful engineering preserves the chain between the problem and the implementation:
- What are we trying to accomplish?
- What conditions constrain the solution?
- What alternatives are available?
- What does each alternative make easier or harder?
- What can fail, and how serious would the failure be?
- Why does the selected design best fit the present conditions?
- What evidence would cause us to revise the decision?
The final decision may still be simple. The team may choose to keep the Python service, add two replicas, introduce a queue, and revisit the architecture after measuring production demand. The value of the decision is not proportional to how dramatic it sounds. Its value lies in whether the intervention follows from the actual problem.
Thoughtful engineering often appears less impressive than Thoughtless Engineering because it is less interested in sweeping declarations. It is willing to say that the current architecture may be sufficient, that the bottleneck is not yet known, that a service boundary would create more cost than value, or that a metric is useful but inconclusive. This caution is not weakness. It is an acknowledgment that systems contain uncertainty and that confidence should be proportional to evidence.
What a Thoughtful Engineer Reasons Through
A thoughtful engineer does not need to produce a formal analysis for every minor decision, but they should be capable of reasoning across the following dimensions when the stakes justify it:
| Dimension | Questions it raises |
|---|---|
| Requirements | What capability must the system provide? What counts as success? |
| Constraints | What limits exist around time, cost, security, latency, staffing, regulation, and infrastructure? |
| Stakeholder impact | Who benefits, who bears the risk, and who must operate or maintain the result? |
| Option space | What broad classes of solution are available before we commit to one implementation? |
| Trade space | What does each alternative improve, and what new costs or risks does it introduce? |
| Architecture | How should responsibilities, data, and failure boundaries be organized? |
| Failure modes | What can go wrong locally and systemically? What is the blast radius? |
| Testing strategy | What evidence will give us confidence in behavior, contracts, and invariants? |
| Observability | How will the running system reveal health, degradation, and failure? |
| Lifecycle | How will the system be deployed, upgraded, operated, deprecated, and eventually replaced? |
| Evolvability | Which assumptions are likely to change, and how expensive will those changes be? |
| Runtime complexity | How does performance behave as workload, data size, or concurrency grows? |
| Operational cost | What does the design require from infrastructure and people after deployment? |
| Migration risk | What existing behavior must be preserved, and how safely can the transition occur? |
These dimensions are not independent. They constrain one another. A design that performs well may be too difficult for the team to operate. A design that is easy to deploy may create unacceptable consistency risks. A service boundary that improves ownership may increase latency and complicate debugging. Higher test coverage may improve confidence but create little value if the tests focus on low-risk implementation details.
Thoughtful engineering is the ability to see those relationships rather than optimizing one dimension in isolation.
Requirements Before Artifacts
A thoughtful engineer starts by clarifying the problem in terms that do not presuppose a solution. They distinguish a need from an implementation.
“We need Kubernetes” is not a requirement.
“We need to deploy multiple independently scalable workloads, recover failed instances automatically, and manage configuration consistently across environments” is closer to a requirement.
“We need microservices” is not a requirement.
“Two teams need to deploy independently, and one workload must scale separately from the rest of the application” identifies pressures that might justify service separation.
“We need more tests” is not yet a useful requirement.
“We need confidence that changes to the authorization logic cannot grant access incorrectly” identifies a behavioral and risk objective.
This distinction preserves the option space. Once the requirement is expressed as a particular technology, alternatives disappear before they have been evaluated. Thoughtful engineers resist that premature collapse.
Constraints and Stakeholders
Requirements describe what the system should accomplish. Constraints describe the conditions under which it must do so. These include technical limits, but they also include organizational and human realities.
The theoretically fastest solution may be a poor choice if the team cannot maintain it. The most elegant architecture may be inappropriate if the deployment deadline is fixed. A rewrite may improve long-term performance but create unacceptable short-term migration risk. A service may technically satisfy its API contract while imposing operational burdens on another team.
A thoughtful engineer asks not only whether a design can work, but who must live with it.
This includes users, operators, maintainers, downstream teams, security reviewers, infrastructure groups, and future engineers who were not present when the decision was made. Engineering quality includes how clearly the system communicates its assumptions and how responsibly it distributes cost and risk across those stakeholders.
The Option Space Before the Trade Space
One recurring feature of Thoughtless Engineering is that it compares one favored solution against a caricature of the status quo. Go is compared against “slow Python.” Microservices are compared against an “unscalable monolith.” High coverage is compared against untested code. These comparisons create the appearance of a trade study while excluding most of the actual alternatives.
A thoughtful engineer first expands the option space.
For a scalability problem, the options might include:
- measuring and profiling the current system;
- horizontally scaling stateless application instances;
- caching repeated work;
- reducing frontend request volume;
- improving database queries and indexes;
- introducing asynchronous jobs;
- separating model execution from request handling;
- applying rate limits or backpressure;
- selectively rewriting one constrained component;
- or rewriting the entire service in another language.
Only after identifying the plausible options can the engineer compare their trade-offs. The trade study should include cost, expected benefit, reversibility, uncertainty, operational burden, and migration risk. A large rewrite should not be evaluated as though it were simply a more powerful version of adding a replica. They are fundamentally different classes of intervention.
Failure Modes as a First-Class Design Concern
Thoughtful engineers do not treat failure analysis as pessimism added after the happy path has been designed. They ask how the system fails while determining how the system should work.
They consider whether a dependency can be slow rather than completely unavailable, whether a timeout leaves the final outcome unknown, whether retries can repeat side effects, whether a queue can grow without bound, whether stale data is acceptable, and whether one workload can exhaust resources needed by another. They distinguish the frequency of a disturbance from the magnitude of its consequence.
This requires moving between levels of analysis. A local function may correctly retry a request, while the system-level effect of many such retries is overload. A service may correctly cache a value, while the platform-level effect is unacceptable inconsistency. A component may satisfy its local latency target while participating in a chain whose compounded latency violates the user-facing objective.
Thoughtful engineering therefore asks both:
- Is this component behaving correctly?
- Does this local behavior compose into acceptable system behavior?
Testing as Evidence
A thoughtful engineer treats testing as part of an assurance strategy. They do not confuse test count or coverage percentage with confidence, but neither do they dismiss those measurements. They ask what evidence is needed for the kind of claim being made.
Unit tests can provide confidence in isolated behavior and invariants. Integration tests can validate actual interaction among components. Contract tests can preserve shared assumptions between independently developed services. Load tests can reveal saturation and queueing behavior. Fault-injection tests can expose failure amplification. Static analysis, schemas, type systems, code review, canary releases, and production telemetry can contribute additional forms of assurance.
The testing strategy should follow from the risk. A data transformation may require strong property and invariant testing. A dependency adapter may require controlled simulation of timeouts and malformed responses. A user-facing distributed workflow may require end-to-end validation and traceability across services.
The question is never simply, “How many tests do we have?” It is, “What claim about the system do these tests allow us to defend?”
Observability as Feedback
Thoughtful engineering assumes that some important facts will remain unknown until the system operates under real conditions. Observability is therefore part of the design, not an afterthought.
The engineer identifies the service-level indicators that correspond to user experience, the metrics that expose saturation and degradation, the traces needed to understand cross-service workflows, and the logs required to reconstruct detailed events. They define what healthy behavior looks like and decide which deviations require action.
This creates a feedback loop:
Design assumption → implementation → instrumentation → observed behavior → revised understanding
Without that loop, architecture remains based on speculation. A team cannot know whether a rewrite improved the real bottleneck, whether horizontal scaling reduced end-to-end latency, or whether a new service boundary created unacceptable coordination cost. Thoughtful engineering treats production behavior as evidence that may confirm or challenge earlier decisions.
Lifecycle and Evolvability
Thoughtless Engineering often behaves as though the system ends at deployment. Thoughtful engineering considers what happens next.
- Who owns the system?
- How will it be upgraded?
- How will contracts evolve?
- How will data be migrated?
- How will incidents be diagnosed?
- How will old behavior be deprecated?
- What happens when the original authors leave?
- What parts are likely to change, and which assumptions should be protected from change?
Evolvability does not mean predicting every future requirement. It means avoiding unnecessary commitments and making important assumptions visible. Stable interfaces, explicit contracts, modular responsibilities, observability, and meaningful tests all allow a system to change without requiring future engineers to rediscover its intent from scratch.
A thoughtful engineer understands that maintainability is not a vague aesthetic quality. It is the cost and risk of future change.
Reversibility and Proportionality
One of the clearest markers of engineering maturity is proportionality between evidence and intervention. When uncertainty is high, thoughtful engineers often prefer changes that are incremental, observable, and reversible. They measure before rewriting, isolate one component before replacing the entire system, and validate an architectural hypothesis before expanding it across the platform.
This does not mean large changes are always wrong. Sometimes the existing architecture is fundamentally incapable of meeting the requirements. Sometimes the accumulated constraints make incremental improvement more expensive than replacement. But a large, irreversible intervention should require stronger evidence than a small, reversible one.
The thoughtful engineer asks:
- How costly is this decision to reverse?
- How quickly will we learn whether it worked?
- Can we test the core assumption on a smaller scale?
- What new risks does the intervention create?
- What is the failure plan if the change does not deliver the expected benefit?
This kind of reasoning protects organizations from expensive bets justified mainly by confidence and fashion.
A Practical Checklist
Before making a consequential engineering decision, a thoughtful engineer should be able to answer most of the following questions:
Problem and objectives
- What problem are we actually solving?
- What does success look like for users and dependent systems?
- Are we describing a need, or have we already smuggled a solution into the requirement?
- Which outcomes matter most?
Constraints and context
- What are the latency, throughput, availability, security, cost, and delivery constraints?
- Which constraints are hard requirements, and which are preferences?
- What skills and operational capacity does the team possess?
- Which parts of the existing system must remain compatible?
Alternatives and trade-offs
- What options exist besides the first solution proposed?
- What does each option make easier?
- What does each option make harder?
- Which costs are immediate, and which are deferred?
- Which option preserves the most useful future choices?
Risk and failure
- What can fail?
- How likely is the failure?
- How severe is the consequence?
- What is the blast radius?
- Can the system amplify the disturbance?
- How is failure contained, detected, and recovered from?
Evidence and validation
- What measurements support the decision?
- What assumptions remain unverified?
- What tests demonstrate the relevant behavior?
- How will production telemetry confirm or challenge the design?
- What result would show that the decision was wrong?
Lifecycle and change
- Who will operate and maintain this system?
- How will interfaces and data evolve?
- How will the system be migrated, rolled back, or replaced?
- What knowledge must be preserved for future engineers?
- What happens when the current requirements change?
The checklist is not meant to turn every decision into bureaucracy. Its purpose is to expose the dimensions that slogans conceal. A minor internal refactor does not require a formal trade study. A language rewrite, major service decomposition, or critical reliability decision probably does.
The Ability to Explain “Why”
The most visible property of a thoughtful engineer is not that they always choose correctly. No engineer does. The property is that their decisions are intelligible. They can explain why a metric matters, why a language is appropriate, why a boundary belongs in one place rather than another, why a particular failure is acceptable, and why the residual risk is justified.
They can also explain the limits of their confidence.
A thoughtful engineer might say:
We are keeping the FastAPI service because profiling shows that model execution and database access dominate latency. We will scale the stateless API horizontally, move long-running model work behind a bounded queue, and monitor queue age and end-to-end latency. If routing or serialization becomes a meaningful bottleneck after those changes, we will consider selectively replacing that path.
That answer is valuable because it contains a causal model. It identifies evidence, connects intervention to bottleneck, defines observability, and establishes conditions for revisiting the decision.
Similarly, a thoughtful testing answer might say:
We stopped at 84% coverage because the remaining uncovered code consisted mainly of framework glue and defensive logging, while the critical validation, transformation, error-handling, and retry paths were tested. We reviewed the uncovered branches rather than treating the percentage itself as the stopping rule.
Again, the number is placed inside an argument rather than substituted for one.
What Thoughtful Engineering Is Not
Thoughtful engineering should not be confused with endless analysis, indecision, or fear of implementation. Systems must be built. Decisions must be made under uncertainty. Deadlines exist, and not every choice deserves equal attention.
The goal is not to eliminate heuristics but to use them consciously. It is not to avoid strong opinions but to connect strong opinions to conditions. It is not to demand perfect information but to remain honest about what is known, what is assumed, and what evidence would change the conclusion.
Thoughtfulness is compatible with speed. An experienced engineer may move quickly because they recognize the problem class, understand the relevant trade-offs, and know which details do not matter. The difference is that their speed comes from compressed reasoning rather than the absence of reasoning.
That is the positive standard I was looking for during the interview. I did not expect the candidate to produce a complete architecture, identify every failure mode, or know every testing technique. I expected him to show that his conclusions emerged from a model of the system. I wanted to hear assumptions, alternatives, consequences, and conditions. I wanted to understand why 84% mattered, why another language might be appropriate, why a service boundary belonged somewhere, and what evidence would justify accepting the remaining risk.
Thoughtful engineering does not guarantee the right answer. It produces answers that can be examined, challenged, tested, and revised.
That is what makes it engineering rather than technical reflex.
Conclusion: Engineering With the Thinking Put Back In
When I think back on the interview, what concerns me is not that the candidate failed to mention a particular technique, framework, or piece of terminology. Interviews are imperfect. People forget words, misunderstand questions, and sometimes give weaker answers than they would in ordinary work. I was not looking for a recital of testing vocabulary, a fashionable architecture diagram, or a rehearsed defense of one programming language over another. The problem was not the absence of a magic phrase. The problem was the repeated absence of reasoning beneath the phrases that were used.
The candidate could talk about coverage, integration tests, service boundaries, scalability, logs, Python, and Go. But when those concepts were examined, they rarely connected to a clear model of the system. The coverage number was not connected to a risk model. The stopping point was not connected to a theory of test sufficiency. The common input cases were not connected to an analysis of impact or tail behavior. The language recommendation was not connected to a measured bottleneck. The service boundaries were not connected to responsibility, contracts, or failure isolation. The logs were not connected to an observability strategy.
The vocabulary was present. The engineering judgment was not.
That is why I have found the phrase Thoughtless Engineering useful. It names a failure mode that is broader than bad coding and more important than unfamiliarity with any single concept. Thoughtless Engineering occurs when technical artifacts become substitutes for technical arguments. It is what happens when engineers move too quickly from a vague impression of the problem to a familiar solution, skipping the work of identifying requirements, constraints, alternatives, trade-offs, failure modes, and lifecycle consequences.
Modern software engineering makes this failure mode increasingly easy. We operate in an environment full of ready-made answers. We are told to use microservices for scale, Kubernetes for production, Go for concurrency, Rust for safety, Python for rapid development, high coverage for quality, event-driven systems for decoupling, and observability platforms for operational maturity. Every one of these recommendations may contain useful experience. Every one may also become a slogan detached from the conditions that made it useful.
“Use Go” can be the right answer.
“Increase coverage” can be the right answer.
“Split the service” can be the right answer.
“Look at the logs” can be the right next action during an incident.
“Scale horizontally” can be the right response to increased demand.
But none of these answers is self-justifying. Their value depends on the problem, the architecture, the workload, the risk, the operational context, and the cost of alternatives. A recommendation becomes engineering only when the causal chain is made explicit.
That causal chain is the real subject of this essay.
- Why does the metric matter?
- Why does the language matter?
- Why does the boundary belong there?
- Why does this failure deserve attention?
- Why is the remaining risk acceptable?
- Why is this intervention proportionate to the evidence?
- Why should the system behave better after the change?
These questions do not guarantee that the engineer will be correct. Engineering is performed under uncertainty, and even careful decisions can fail. But explicit reasoning makes decisions inspectable. It allows assumptions to be challenged, trade-offs to be debated, evidence to be collected, and conclusions to be revised. It leaves behind more than an implementation. It leaves behind an explanation of why the implementation exists.
That distinction matters over the life of a system. Requirements change, workloads grow, teams reorganize, dependencies fail, and technologies lose favor. A thoughtful design can be revisited because its assumptions are visible. A thoughtless design becomes difficult to change because future engineers inherit only the artifact. They know that the service was separated, but not what independence it was supposed to create. They know that the system was rewritten, but not which bottleneck justified the migration. They know that the team targeted a coverage percentage, but not which behaviors were considered critical. The causal chain has disappeared, leaving future maintainers to reconstruct it through archaeology.
This is also why Thoughtless Engineering should not be dismissed as merely a junior habit. Experience can improve judgment, but it can also harden preferences into dogma. An experienced engineer can be just as thoughtless when they import a successful architecture from one environment into another without examining the new constraints. Seniority does not make a heuristic universally valid. The only durable protection is the willingness to reconnect the recommendation to the actual system.
The thoughtful engineer is therefore not the person who knows the most buzzwords, speaks with the greatest certainty, or proposes the most dramatic intervention. The thoughtful engineer is the person who can explain the relationship between problem and solution. They can move from requirements to architecture, from architecture to failure modes, from failure modes to tests, from tests to evidence, and from production behavior back to revised understanding. They know which assumptions their conclusion depends on and what facts would cause them to change their mind.
That is the standard I was looking for in the interview.
I did not need the candidate to insist on 100% coverage. I needed him to explain why the existing tests were sufficient for the risks the system faced. I did not need him to reject Go. I needed him to establish the workload and bottleneck before prescribing a rewrite. I did not need him to use the phrase “contract testing.” I needed him to distinguish the external dependency from the behavior our own system was responsible for. I did not need a perfect theory of distributed systems. I needed evidence that he understood that rare local behavior can create disproportionate global consequences.
What troubled me was not that he reached conclusions I disagreed with. It was that the conclusions appeared before the analysis.
That is ultimately what Thoughtless Engineering means. It is not necessarily irrational engineering. The answer may be plausible, fashionable, or even correct by accident. It is engineering in which the reasoning has been stripped away, leaving behind implementation preferences, metrics, patterns, and slogans without the system model that gives them meaning.
The issue is not Python versus Go, monolith versus microservice, or 84% versus 100% coverage. The issue is whether an engineer can reason from requirements to consequences. Thoughtless Engineering begins when we mistake technical vocabulary for technical judgment. Thoughtful engineering begins when we slow down long enough to ask what the system is, what it must do, how it can fail, and why a particular choice follows from those facts.
The tools will change. The slogans will change. The fashionable architectures will change. The responsibility to think will not.
Comments
Post a Comment