Being a Good Developer and Being a Good Software Engineer Are Not the Same Thing
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Confusion Nobody Names Out Loud
You have a developer on your team who ships consistently. PRs are clean, tests pass, tickets close on time. By every visible metric, they're performing. But then you ask them to own a service end-to-end — to make architectural decisions, handle cross-team dependencies, reason about failure modes — and something breaks down. Not the code. The thinking.
That gap has a name, and it's not seniority. It's the difference between being a good developer and being a good software engineer. These are related skills, but they are not the same skill, and conflating them causes real damage: to promotions that don't work out, to systems that get complicated when they should have stayed simple, and to developers who don't know why they're hitting a ceiling.
What a Good Developer Is Good At
A good developer translates requirements into working code efficiently. They know their language and framework deeply. They write readable code, handle edge cases, and don't leave messes for others to clean up. They're fast. Their output is predictable. In most sprint-based environments, this is exactly what gets rewarded.
This is genuinely valuable. Don't underestimate it. A team of strong developers who execute well is a powerful thing.
But the job description stops there. A developer is optimizing for the task in front of them. The unit of work is the ticket.
What a Software Engineer Has to Think About
A software engineer optimizes for the system, not the ticket. The unit of work is the product over time — including the parts that don't exist yet, and the conditions that haven't happened yet.
This means thinking about:
- Change: How does this design hold up when requirements shift in six months? Where are the seams that will need to open?
- Failure: What happens when the database is slow? When the third-party API goes down? When two services get deployed out of order?
- Operability: Can someone debug this at 2am without reading your notes? Does the logging tell the story of what went wrong?
- Cost of understanding: Every abstraction you add is paid for by every engineer who reads the codebase after you. Is this abstraction earning its keep?
None of these concerns show up in a ticket. They require a different mode of attention — one that is zoomed out and forward-looking, not heads-down and present-focused.
Where the Gap Shows Up
Here's a concrete example. A developer is asked to add retry logic to a service that calls an external payment API.
A good developer writes a retry loop with exponential backoff, catches the relevant exceptions, and ships it.
A software engineer asks: Are these retries idempotent? What happens if the payment actually went through but the response was lost — will we charge the customer twice? Should retries happen at the caller level or be pushed into a queue so they survive process restarts? What's the alerting story if retries are consistently exhausting?
The developer's solution works in the happy path and in most failure scenarios. The engineer's solution works in the failure scenarios that will actually cost you money.
// Developer version: handles transient failures
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
return paymentClient.charge(request);
} catch (TransientException e) {
Thread.sleep(backoff(attempt));
}
}
// Engineer version: same retries, but asks the upstream question first
// Does paymentClient.charge() accept an idempotency key?
// If not, this retry loop is a liability, not a safety net.
String idempotencyKey = request.getIdempotencyKey(); // must exist
return paymentClient.charge(request, idempotencyKey);
The difference isn't code complexity. It's the question that was asked before writing a line.
The Skill You Have to Deliberately Build
Most developers become good developers naturally, through repetition. You write enough code, you get fast, you get clean. The feedback loop is tight.
Software engineering skills have a much slower feedback loop. You don't know if your data model was wrong until eighteen months later when you're trying to add a feature and the schema is fighting you. You don't know if your service boundaries were off until the team has been blocked by cross-service deployments for a year.
This means you have to build it deliberately:
- Read post-mortems — your own company's if available, public ones from other engineering teams. They are the compressed wisdom of expensive mistakes.
- Own things across their lifecycle — not just until they ship, but through the first incident, the first scaling problem, the first requirement change.
- Study systems that are old — not to copy them, but to understand what assumptions they were built on and which ones turned out to be wrong.
- Ask "what breaks this" before asking "how do I build this" — make it a reflex, not an afterthought.
The Practical Takeaway
If you're managing someone who ships well but struggles when given ownership: don't just give them harder tickets. Give them a system to steward — from design through incident response. Let them feel the gap between building something and being responsible for it.
If you're the developer in this story: the fastest way to close the gap is to stop treating your tickets as self-contained units. Before you close the next one, write down three things that could go wrong with your solution in production — not during development, in production. Then figure out whether your implementation handles them.
That habit, repeated, is how you start thinking like an engineer.