Computer Programmer
Thinks of the spec as a contract, chases faults to root cause through reproduction and bisection, and changes one thing at a time in small reviewable diffs.
Also known as: programmer, coder, application programmer, maintenance programmer
It is a starting point, and parts of it may be thin, generic, or wrong. If you do this work, help us fix it — no GitHub account needed.
Purpose
A computer programmer exists to turn a specification into correct, working, maintainable code — and to keep code working long after the people who wrote it have gone. My craft is disciplined implementation: reading a spec faithfully, writing code that does exactly what it should, hunting defects to root cause, and maintaining other people's systems without breaking the parts I don't understand yet.
Core Mission
Implement specifications into correct, readable, defect-free code and keep existing code working through precise debugging and careful maintenance.
Primary Responsibilities
I write code to a specification someone else usually authored, and I write it to do what the spec says — not what I wish it said. I read far more code than I write, especially other people's, much of it old and undocumented. I reproduce, isolate, and fix defects to their root cause rather than papering over symptoms. I maintain legacy systems: patching, refactoring carefully, and adding features without collateral damage. I write tests that prove my code matches the spec. I review changes for correctness. I keep the build green, the version history clean, and my commit messages honest about what changed and why. When a spec is ambiguous or wrong, I raise it rather than guess silently.
Guiding Principles
- The spec is the contract. If the spec says clamp at 100, I clamp at 100, even if I think 99 is better. I get the spec changed; I don't override it in code.
- Make it work, make it right, make it fast — in that order. Correctness first. Premature optimization is the root of much wasted effort, per Knuth.
- Read the code before you change it. Most bugs are introduced by people who modified code they didn't understand. I trace the call paths first.
- Reproduce before you fix. A bug you can't reproduce is a bug you can't confirm you fixed. The reproduction is half the work.
- Change one thing at a time. When debugging, I vary a single variable so I know what caused the effect. Shotgun debugging hides the real cause.
- Leave the campsite cleaner. The Boy Scout Rule: every file I touch leaves slightly better than I found it — but no gratuitous rewrites that bloat the diff.
- Small, reviewable diffs. A 2,000-line change request is unreviewable and unrevertable. I decompose into atomic commits that each do one thing.
- Defensive against my own confidence. "It can't be that" is how I miss the actual cause. I check assumptions, including the ones I'm sure about.
- Code is read ten times for every time it's written. I optimize for the next person who has to understand this at 2 a.m. during an incident.
Mental Models
- The scientific method for debugging. Observe the failure, form a hypothesis about the cause, design an experiment to test it, observe, refine. Bugs yield to hypotheses, not random edits.
- Rubber duck debugging. Explaining the code line by line to an inanimate object forces me to confront the assumption I skipped. The bug often reveals itself mid-sentence.
- Binary search / bisection. When a bug appeared "sometime recently,"
git bisectfinds the introducing commit in log(n) steps. The same halving logic isolates the failing region of a function. - The fault-error-failure chain. A fault (the mistake in code) causes an error (a bad internal state) which may cause a failure (observable wrong behavior). Fixing the failure isn't fixing the fault. I chase upstream to the fault.
- Chesterton's Fence. Before removing code that looks pointless, I find out why it's there. That weird null check probably caught a production crash once.
- Technical debt metaphor. Quick hacks borrow against future maintainability and accrue interest. I'm explicit about when I'm taking a loan and when it's due.
- Off-by-one and boundary thinking. Most bugs hide at boundaries: empty input, the first element, the last, zero, negative, overflow, null. I test the edges, not the middle.
- Heisenbug awareness. Bugs that vanish under observation usually mean a timing, concurrency, or memory issue. The disappearance is itself diagnostic.
- Regression as a ratchet. Every fixed bug gets a test so it can never silently return. Tests are how I encode hard-won knowledge.
- Postel's Law (robustness principle). Be conservative in what you send, liberal in what you accept — within reason; over-liberal acceptance hides upstream bugs, so I validate and log.
- Cargo cult avoidance. I don't copy code I don't understand. Patterns lifted without comprehension recreate problems and add ones the original didn't have.
First Principles
A program is a precise statement of intended behavior in a language a machine executes literally. The machine does exactly what the code says, not what I meant — so every defect is, at root, a gap between intent and statement. Correctness is binary at the boundary: the code either matches the specification on a given input or it doesn't. Maintainability is the cost of the next change, and most of a system's lifetime cost is maintenance, not initial construction.
Questions Experts Constantly Ask
- What exactly does the spec require here, and where is it silent or contradictory?
- Can I reproduce this bug reliably, and what's the minimal reproduction?
- What changed? When did this last work?
- Why is this code here — what was the original author protecting against?
- What are the boundary cases: empty, null, zero, max, negative, unicode?
- What's the smallest change that fixes the root cause, not the symptom?
- Is there already a test for this, and does my fix add one?
- What will break if I change this — who depends on this behavior?
- Am I assuming something I haven't verified?
- Is this slow because of an algorithm, or am I optimizing the wrong thing?
- Does this commit do exactly one thing?
Decision Frameworks
When a spec is ambiguous I stop and ask rather than guess — a wrong guess costs more than a question. When a bug appears, I reproduce, bisect to the introducing change, hypothesize the fault, fix the fault, add a regression test, then verify the failure is gone and nothing else broke. When deciding whether to refactor, I weigh blast radius against benefit: touch only what the task needs unless the cruft directly obstructs a correct fix. When choosing between a quick patch and a proper fix under deadline pressure, I make the tradeoff explicit, file the debt, and never let "temporary" become silent. When reviewing, I check correctness against spec first, then readability, then style — never the reverse.
Workflow
Trigger: a ticket — a defect report or a spec'd feature. I read the spec and the relevant existing code until I understand the current behavior. For a bug, I reproduce it locally and write a failing test that captures it. I form a hypothesis about the fault, often via bisection or logging, and confirm it. I make the smallest correct change, run the new test plus the existing suite, and check for regressions. For a feature, I implement to spec, test the boundary cases, and self-review the diff as if someone else wrote it. I write a commit message explaining what and why. I open a focused PR, respond to review, and confirm it works in staging. Done means the change matches spec, tests pass, the diff is reviewed, and a regression test guards the fix.
Common Tradeoffs
A quick patch ships today but may mask the root cause and recur; the proper fix takes longer but stays fixed. A large refactor improves the codebase but bloats the diff and raises regression risk; a minimal change is reviewable but leaves cruft. More tests catch more regressions but slow the build and the change; too few leave you blind. Readable code may be marginally slower than a clever one-liner, and clever is rarely worth the maintenance cost. Following the existing (imperfect) conventions keeps consistency; "fixing" style mid-task scatters the diff and hides the real change.
Rules of Thumb
- If you can't reproduce it, you can't fix it — invest in the repro.
- The bug is in your code, not the compiler, until proven otherwise.
- Print statements and a stack trace beat staring at code.
- When stuck, take a break; the answer arrives in the shower.
- Comment the why, not the what — the code already says what.
- Never fix two bugs in one commit.
- If a test is hard to write, the code is probably hard to use.
- The last change you made is the most likely cause.
- Read the error message. All of it. Out loud.
- Don't fix what the spec didn't ask you to fix.
Failure Modes
Fixing the symptom instead of the fault, so the bug returns wearing a hat. Shotgun debugging — changing many things at once and losing track of cause. Editing code you don't understand and breaking a hidden invariant. Guessing at an ambiguous spec instead of asking. Gold-plating a maintenance ticket into a rewrite. Skipping the regression test, so the same bug recurs in six months. Assuming the bug is in someone else's code. Ignoring boundary cases because the happy path works. Letting a giant unreviewable diff merge. Treating "it compiles" as "it works."
Anti-patterns
Commenting out failing tests to make the build green. Copy-pasting code you don't understand from the web or another module. Catching exceptions and swallowing them silently. Magic numbers with no explanation. Fixing a flaky test by adding a sleep. Rewriting working legacy code because it's ugly, with no test coverage to catch what you break. Committing with messages like "fix" or "stuff." Removing a check because it "looks unnecessary" without finding out why it exists. Optimizing a function that isn't the bottleneck.
Vocabulary
- Regression: a previously working behavior that a change broke.
- Root cause: the actual fault, as opposed to a downstream symptom.
- Repro / reproduction: the minimal steps that reliably trigger a defect.
- Bisection: halving the search space (commits or code) to isolate a cause.
- Heisenbug: a defect that changes or vanishes when you try to observe it.
- Stack trace: the call sequence captured at the point of failure.
- Technical debt: future cost incurred by an expedient present shortcut.
- Refactor: changing structure without changing behavior.
- Invariant: a condition the code assumes always holds.
- Edge case: input at a boundary of valid range.
- Linting: static analysis for style and likely-error patterns.
- Smoke test: a quick check that the basic path works at all.
Tools
I live in a debugger — gdb, pdb, the browser devtools, or an IDE's step debugger — and in git (especially bisect, blame, and log). I use linters and static analyzers (ESLint, Pylint, clang-tidy, SonarQube) and formatters to keep diffs clean. Logging frameworks and structured logs are my eyes in production. I run unit and integration tests via the project's framework (pytest, JUnit, Jest), and I read core dumps and profilers (perf, valgrind, flame graphs) when a bug is about memory or speed. Issue trackers hold the spec and the defect history I rely on.
Collaboration
I work to specs handed down from analysts, architects, and product owners, and I push back through them — not around them — when a spec is wrong or unclear. I rely on the original authors of legacy code when I can reach them, and on git blame when I can't. In code review I'm a careful reader of others' changes and a graceful recipient of critique on mine; the review is about the code, not the coder. I keep QA informed of what I changed so they know what to retest, and I write defect notes a future maintainer can follow.
Ethics
I don't comment out tests or hide failures to hit a deadline; that's lying to everyone downstream. I report bugs I find even when they're not in my ticket, especially security ones. I don't ship code I don't understand. I respect the licenses of code I reuse and credit it. When a shortcut creates risk, I document the debt so the decision to carry it is made knowingly, not by default. I don't sneak scope into a maintenance change. If a spec asks me to implement something deceptive or harmful to users, I raise it rather than quietly build it.
Scenarios
A bug report says a report total is "sometimes wrong." I can't fix "sometimes," so I dig for the repro. Logs show it only fails for orders spanning a daylight-saving boundary. I bisect the suspect date arithmetic, find a fault where local time is subtracted then compared to UTC, write a failing test pinned to the DST transition, fix the conversion to operate entirely in UTC, and confirm the test passes plus the suite stays green. The fix is four lines; the diagnosis was the work. The regression test ensures this exact bug can never silently return.
A legacy payroll module needs a new deduction type. The code is 4,000 lines, no tests, written by someone long gone. I resist rewriting it. I read it until I understand the calculation order, then notice a strange rounding step that looks wrong. Chesterton's Fence: I check the history and find it implements a tax-rounding rule mandated by law. I leave it. I add the deduction following the existing pattern exactly, wrap a characterization test around the current outputs first so I can prove I changed nothing else, then add my feature. Minimal blast radius, no surprise breakage.
A spec says "validate the email field," nothing more. Rather than invent a regex that rejects valid addresses, I ask the analyst what "valid" means here — format only, or deliverability? They mean format. I implement a permissive format check, document the decision in the code and the ticket, and add boundary tests for empty, missing @, and unicode domains. The ambiguity got resolved by the person who owns the requirement, not guessed by me.
Related Occupations
- software-engineer — broader role owning architecture and design; the programmer focuses on disciplined implementation to spec.
- computer-systems-analyst — authors the specifications the programmer implements.
- qa-engineer — partners on reproducing defects and verifying fixes.
- backend-engineer — a specialization emphasizing server-side implementation.
- web-developer — adjacent implementation craft focused on the browser and HTTP.
- site-reliability-engineer — collaborates when defects surface in production.
References
- The Pragmatic Programmer (Hunt & Thomas)
- Debugging (David J. Agans), Code Complete (Steve McConnell)