Cengiz.ilerler.info

Common Pitfalls and Anti-Patterns in Software Delivery

August 1, 2026

This is about how work gets defined, handed over, and shipped. It is not about code-level anti-patterns like god objects or anemic domain models. Those are a separate conversation.

Some of these have established names. The rest are named here, because a thing without a name is hard to argue about. People rarely defend these behaviors on purpose. A name gives you a way to raise it in a meeting without it landing on a person.


Premature Involvement

Pulling developers into the room with no question for them to answer, while product is still discovering what it wants, and counting it as engineering involvement.

To be precise, because this one gets misread: asking a developer a specific question is not the problem. “Can we do X, and roughly what does it cost?” is a five-minute conversation that saves three weeks, and it should happen early and often. The anti-pattern is the developer as furniture: sitting through two hours of product deliberation as a consultant who is never actually consulted.

The cost is invisible on both sides. Product feels well-covered because engineering was “in the loop.” The developer loses productive hours wondering why they’re in the room, and it registers nowhere except as slower delivery.

The rule: bring a developer in with a question, not to observe. If there’s no question yet, there’s no reason for them to be there yet.


Ticket Misuse

Creating or updating tickets without objectives, reproduction steps, acceptance criteria, business context, meaningful updates, or correct labels. It misguides priorities, fuels confusion, and sparks endless back-and-forth.

Where a screenshot applies, it should show the full window. Cropped screenshots are their own small crime. The crop always removes the URL, the environment banner, the console, the browser state. Those are exactly the parts that would have answered the first three questions.

Tip

A well-written ticket should include all necessary information so a developer can pick it up and finish it without asking questions, even at 3 AM.

The rule: if the ticket cannot be worked without a conversation, it is not ready. The conversation is the missing part of the ticket, not a substitute for it.


Bug Inflation

Labeling most issues as a Bug when better types exist: Story, Spike, Enhancement, Task.

Bug should mean the system doesn’t do what it should have done. Usually that’s the spec, but not always: a missing timeout is a bug even though no spec ever mentioned one. It is not an accusation — it means the system misbehaved, nothing more. It’s also worthless as a signal if everything lands in it.

But type alone can’t carry that signal, because two questions keep getting collapsed into one field:

  • Type is what kind of work this is. It follows from the observed behavior.
  • Cause is why the work exists. It follows from what went wrong, or from nothing going wrong at all.

Why the work exists goes in Cause. Neither column is the quality signal by itself. Bug + Code Defect together is.

Almost every argument over the Bug label is actually an argument about cause. Separating them ends the fight, because nobody has to deny the thing is broken in order to say it wasn’t a coding error.

Important

The trigger is not the defect. Ask what the system should have done under that condition. If it should have handled it and didn’t, that’s a bug. If nothing reasonable could have been done, it isn’t.

Scenario Type Cause Why
Null reference crash on an empty cart Bug Code Defect An empty cart is a normal state. The code should have handled it.
Race condition under concurrent saves Bug Code Defect Concurrent saves are foreseeable. The code should have been correct under them.
No timeout or fallback, so a vendor hiccup takes down checkout Bug Code Defect Any network call can hang. For checkout, surviving that is the baseline, spec or no spec.
Vendor breaks a published contract with no notice Bug Upstream Change The system stopped doing what it did yesterday, and nothing on our side changed.
Deprecation announced 12 months ago, ignored, now broken Bug Deferred Maintenance The warning arrived and nobody acted. This break was scheduled a year ago.
Works as specified, someone wants it different Story New Request The system did what was asked. The ask changed.
Report shows revenue; nobody ever defined whether that includes refunds Story Requirement Gap No developer can infer an accounting policy from the code. The decision was never made.
Customers say checkout “feels slow”; nobody knows if it’s real; two days to measure Spike Open Question Nothing is confirmed broken. The output is an answer, and the answer decides what gets filed next.
Works correctly, but takes 8 seconds and no performance target was ever set Enhancement Requirement Gap Nothing failed. Speed is a target, and no target was ever set.
Quarterly index rebuild, cert rotation, routine dependency bumps Task Routine Upkeep Nothing is wrong. This is what keeping a running system running costs.

Everything else in that table is real work that has to be tracked, and none of it says anything about how well the code was written.

On ambiguous specs: developers build to a spec, they shouldn’t have to guess. If the spec was ambiguous and the question was raised and left unanswered, the resulting mismatch is a requirement gap. That’s product’s to own, the same way a null reference is engineering’s. But a developer who spots the gap and quietly picks an answer has made a decision nobody can see. That’s a real engineering failure, an assumption that never got written down. It just isn’t a code defect, and it doesn’t change what kind of work the ticket is.

Note

This works as a team-level signal used to find systemic causes. The moment Bug count becomes an individual scorecard, people start negotiating labels instead of fixing things, and the whole taxonomy stops meaning anything.


No Definition of Done

Every person on the ticket has a private definition of finished. Product means demoable. QA means tested. The developer means merged. Ops means it survived a deploy.

Without a shared definition, “done” gets declared at whichever point is most convenient, and the remaining work quietly becomes somebody else’s surprise next week.

The rule: one written definition, agreed before work starts. Tests, review, docs, telemetry, deployed. Whatever the list is, it’s the list. If it has tiers, the tier is picked when the ticket is written, not when someone wants to close it.


Estimates Treated as Commitments

Three different things get called by the same word. What the work will probably take. What the business wants it to take. What someone has promised it will take. Only the first is a technical judgment, and it’s the only one anyone ever asks a developer to produce. The other two are decisions, and they get made somewhere else.

When an estimate is converted into a commitment on the way up the chain, the developer who gave an honest number gets punished for it. The predictable result: estimates inflate defensively, then get padded again by the person above, and nobody’s number means anything.

Uncertainty is also the first thing summarizing removes. A range is awkward on a slide, so it becomes its midpoint, and the midpoint becomes the date.

The rule: an estimate travels with its range and the assumptions behind it, and they don’t get dropped on the way up. A number that arrives upstairs without them isn’t an estimate anymore. It’s a date. If a date is needed as a commitment, that’s a separate negotiation about scope, not a re-reading of the estimate.


Requirements Churn

Changing requirements or acceptance criteria after work has begun.

Common advice says the fix is to “price” the change, accept it and extend the timeline. That isn’t enough. Pricing lets product say “fine, take the extra time,” and the cost lands on the delivery ledger as a schedule slip. Leadership sees engineering being slow. The cause disappears from the record, and the slip gets laundered into an engineering problem.

What’s missing is not price. It’s attribution. Every mid-flight change has one of two causes:

  • Discovery. Information arrived that nobody could have had earlier. Regulation changed, a user test invalidated the assumption, a vendor deprecated something. A legitimate cost of doing business.
  • Deferred thinking. Nothing new was learned. Analysis that should have happened before handoff happened after it. Not the same as a requirement gap, which surfaces when a developer pulls at something underspecified. Deferred thinking arrives pushed.

Identical cost in developer hours. Completely different meaning.

Important

Log which one it was. There’s already a written standard for what a good requirement looks like, ISO 29148, and it asks for the obvious things: clear, complete, testable. Most teams never report against it. Velocity, meanwhile, lands on a slide every two weeks. Requirements volatility, how much changes after work starts, won’t tell you whether a requirement was any good. Only how often the plan moved. That’s still more than anyone has now, and if only one side’s numbers go upstairs, only one side gets asked about them.

None of this is a refusal to accept change. The worst case for anything non-urgent is one cycle. If waiting one cycle is intolerable, either it’s a production emergency, or the cycle is too long, and that’s a separate conversation.

Important

Unmeasurable is not free. Cognitive load does not have to be converted into exact hours before it can be recognized as a cost. When product changes work in flight, engineering does not owe recovery of the original forecast. Any resulting movement belongs to requirements churn, not engineering delay.

The rule: work in flight can be cancelled. It can’t be mutated over the objection of the person doing it. Product may ask whether a change can be absorbed. The developer doing the work decides; refusal requires no defense, carries no blame, and ends the discussion. Refusal means cancellation. Product can kill any ticket, any day, without argument. What they cannot do is keep it alive while changing what it means after the change has been refused, or choose its replacement, since the days already spent don’t come back. Freed capacity goes to the architecture backlog. That is agreed up front, the same way the definition of done is, not negotiated in the moment by whoever just cancelled something. Cancelled work is closed as cancelled, carrying its estimate and its cause, not quietly absorbed into someone else’s number.


Scope Creep

The standard definition is project-level: scope grows over time without a matching adjustment to schedule, budget, or staffing. That version is visible, and it usually gets argued about.

The version nobody argues about starts one ticket at a time. Call that ticket bloat: a ticket is opened for one problem, something related turns up mid-work, it looks cheap, so it goes into the same ticket. Repeat that across a cycle and the project-level growth everyone complains about has already happened, with no decision anyone can point to. Ticket bloat is the harder of the two to catch, because each addition is individually reasonable and none of them were estimated.

Beyond the obvious estimate damage, this destroys reviewability. A focused diff gets a real review. A diff that fixes the reported bug plus four things noticed along the way gets an approval, not a review, because nobody can hold all of it in their head at once.

The rule: found something else? New ticket — unless it must be fixed for the original change to be correct. Otherwise, link it, flag it to the team, and move on.


Context Switching

Rapidly shifting focus between tasks or tickets. The cost is not the minutes lost switching. It’s the state that was loaded and is now gone, and the time it takes to rebuild it. Three tasks in parallel leave more work unfinished and make you pay that rebuild cost at every switch.

The rule: once a cycle begins, new work waits for the next one unless there’s a production failure. Capacity freed by cancellation follows the fallback agreed before the cycle.


Long-Lived Branches

A branch that lives for weeks accumulates merge risk every day it exists, and all of that risk is paid at once, at the worst possible moment, usually right before a release, by whoever drew the short straw.

The mechanism is that the branch was written against a codebase that no longer exists. Main keeps moving, refactors land in files the branch also touched, and assumptions that held on day one quietly stop holding. Conflicts compound rather than add up, since resolving one can invalidate the resolution of another, and none of the work is visible until the merge, so design problems surface only after all of it is finished.

The rule: integrate frequently. If a feature can’t ship yet, hide it behind a flag rather than behind a branch.


Review Theater

Approvals that aren’t reviews, commonly called rubber-stamping. A 900-line diff with an “LGTM” four minutes after it was opened. The process box is ticked, the audit trail looks healthy, and no second person actually understands the change.

This is usually a symptom rather than a cause. Reviews get rubber-stamped when they’re too large to review, or when review time isn’t counted as real work.

The rule: if a diff is too big to review properly, the problem is the diff. Split it. And an approval means “I understood this,” not “I don’t object.”


Retro Theater

Retrospectives that produce discussion, sympathy, and nothing else. The same three problems get raised every cycle, everyone agrees they’re problems, no ticket is ever created, and eventually people stop raising them. That reads as improvement but isn’t. Once nothing gets raised, the retro gets shorter, then less frequent, then disappears. Nobody cancels it; it just stops appearing on the calendar.

The rule: fix it in the room, make it a ticket with an owner, or explicitly decline it, out loud, with a reason. “We’re not fixing this” is a legitimate outcome. Silence isn’t.


Hero Culture and Bus Factor of One

One person who is the only one who understands the payment flow, or the deploy pipeline, or that one service. Bus factor is the standard term for it: the number of people who would have to be hit by a bus before the work stops. The organization rewards the hero, because they’re the person who saves the day, and that is exactly why it persists.

It’s fragile, makes time off risky, can trap that person in the role, and makes dependent work wait for their availability.

The rule: treat single ownership as a risk to be tracked, not a strength to be praised. Pair on the scary parts. Write the thing down before you need it.


Ignoring Technical Debt

Putting off foundational work, so the cost of doing it later keeps climbing.

Tip

If a bug is a flat tire, technical debt is driving on bald tires. You can still go for now, but eventually it’s going to cost more than just new tires.

A building with polished occupied floors and a new floor going up on top, resting on a cracked foundation held up by temporary props. Two small figures at street level look up at the new floors.

This is the part that makes technical debt hard to fund. The new floors are visible, demoable, and easy to celebrate. The foundation is not on anyone’s slide. It has been on temporary props since the beginning, and the props have held so far, which is exactly the argument for adding another floor.

Product will rarely fund this, and that isn’t obstruction. They’re measured on customer-visible outcomes, and paying down debt usually has no immediate one, so it rarely outranks anything on their list. The trap is in waiting until something breaks: the debt is still there, but now it has caused an incident, and the cheap window closed on the way there. This is why it belongs on the engineering side of the ledger. Queued against features, it loses every time, and by the rules of that queue it should.

Technical debt is not bug fixing or feature development. It’s the cost carried in the system’s foundation, some of it from shortcuts taken deliberately, some of it from drift that accumulated while nobody was looking. Paying down tech debt is investing in doing things right, so fewer things break later and less time gets burned on them.

What Technical Debt Is

Compromises in code quality, architecture, or tooling. Some are taken deliberately to hit a date. Others accumulate on their own, with nobody deciding anything:

  • Skipping a practice such as dependency injection when the result makes each later change or test more expensive
  • Shipping one feature against the grain of the codebase, so the same job is now done two ways
  • Staying on a library after support ends, while the upgrade gap keeps widening
  • Leaving code hard to extend or test when every new change makes that cost grow
  • Piling new work onto hardcoded values or a quick fix, so removing it gets harder each time
  • Repeating tests or delivery steps by hand as the amount of work keeps growing
  • Poor observability setups (e.g. missing telemetry or structured logging)
  • Manual deploys, hand-configured servers, no infrastructure-as-code, environments that can’t be reproduced

Debt lives inside the system. Users may feel its effects, but it is not missing or broken behavior.

The test isn’t whether it hurts today. It’s whether the cost of leaving it alone is growing. A runtime two major versions behind isn’t slowing anyone down this morning, but the gap widens with every release, the supported-version window closes, and the upgrade gets more expensive the longer it waits. Nothing is on fire and the balance is still going up. That’s interest, and interest is what separates debt from taste.

What Technical Debt Is Not

  • Bugs. If a feature doesn’t work as expected, that’s a bug, not debt.
  • Feature gaps. Missing customer-facing functionality is a roadmap or product issue.
  • Capacity and incidents. Downtime or slowness caused by load, scaling limits, or a provider outage is an ops problem. The line is structural weakness versus capacity event. A service falling over under traffic it was never provisioned for is ops; a service that can’t be scaled because deployment was never automated is debt.
  • Preference. A library or pattern you’d rather adopt, where nothing in the codebase follows it yet and the cost of not switching is flat. It may still be worth doing, it just isn’t accruing. Breaking a pattern the codebase already follows is a different thing entirely: that one is debt from the day it ships.

The Translation Monopoly

Some of these keep happening no matter how well a team understands them. The reason isn’t in any of the sections above.

Product owns the channel to leadership, and it owns the dialect leadership listens in. Roadmap, market, customer, revenue. Engineering’s account of the same cycle arrives later, through fewer people, in a language that has to be translated on the way, and the translation is usually performed by the party whose work is being described. A requirement can fail upstream and still arrive upstairs as an engineering delay.

Executives rarely notice, because from where they sit the loop looks closed. Product speaks fluently about the technical situation, so there’s no visible gap to fill, and nobody schedules a meeting to hear a second version of a story that made sense the first time.

That one fact keeps several of these alive. Ticket quality has no pressure from above to improve, because its failures are recorded inside the team and stop there. Estimates keep getting converted into commitments, because the conversion happens after the last engineer has left the room. Technical debt goes unfunded, because the only person describing the foundation is standing on the new floor.

A rule can be overridden in the meeting where it first becomes inconvenient. A number that arrives every cycle, in the same artifact, on the same slide, doesn’t need anyone present to argue for it.

The rule: if engineering has no unmediated channel to the people making resource decisions, its account of these failures stays a private conversation. Fix that one first.

← All development notes