Picture two programs of exactly the same size. One is a script you wrote on a Saturday to rename a folder of holiday photos. It ran once, it worked, and you will never look at it again. The other is the payments module at a mid-sized company: written seven years ago by people who have since left, modified by forty others, running on its third cloud provider and its fourth major version of the language. Line for line, both are just code. Yet nobody who has worked on the second would describe the two jobs as the same activity. Something happens to code when it has to last, and when more than one mind has to hold it. That something is the difference between programming and software engineering.
Programming integrated over time
The clearest statement of the idea comes from Google. In the book Software Engineering at Google (2020), authors Titus Winters, Tom Manshreck and Hyrum Wright build their whole argument on a phrase Winters favours:
“Software engineering is programming integrated over time.”
The calculus metaphor is deliberate. Programming is the act at a single instant: understanding a problem and expressing a solution a computer can run. Engineering is the sum of all those instants across the life of a system, including every future change you cannot yet foresee. The book adds two more dimensions that follow naturally: scale, meaning how many people and how much code are involved, and trade-offs, meaning the decisions you make knowing their costs will arrive later.
A useful question falls out of this framing: what is the expected lifespan of this code? If the answer is “an afternoon”, almost every practice we are about to discuss is waste. If the answer is “years, maybe decades”, then the cost of writing the code is a rounding error next to the cost of living with it. Most of the famous engineering practices exist because the second situation is surprisingly common, and because people chronically underestimate how long their code will survive.
Change is the real workload
Here is the uncomfortable truth that experienced engineers absorb: the hard part is rarely writing the first version. The hard part is changing it safely. Requirements shift, dependencies publish security fixes, operating systems deprecate APIs, and the business discovers it actually meant something slightly different all along.
Google’s authors name a phenomenon that makes this harder than it looks, now widely known as Hyrum’s Law. Put simply: once enough people use a system, every observable behaviour of it will be relied upon by someone, whether or not you promised it. The order in which a function happens to return results, the exact wording of an error message, a quirk in timing — somebody, somewhere, built on it. That means any change, even a “harmless” internal improvement, can break things you have never seen.
Once you accept that change is the normal state of affairs, the practices of software engineering stop looking like bureaucracy. They look like tools for making change survivable.
The practices, and what each actually buys you
Version control: a memory for the team
A version control system such as Git records every change, who made it and, ideally, why. For a solo weekend project it is a convenient undo button. For a team it becomes something closer to institutional memory. When a bug appears, you can find exactly which change introduced it. When two people edit the same file, the system helps reconcile their work. When a colleague asks “why on earth is this written like that?”, the commit history often holds the answer, left by someone who is long gone.
Tests: confidence as a renewable resource
Automated tests are often sold as a way to catch bugs, and they do. But their deeper value is temporal. A good test suite lets someone who did not write the code change it without fear. It converts the question “did I break anything?” from an anxious guess into a command you can run. That confidence is what allows a codebase to keep evolving instead of calcifying into something everyone is afraid to touch.
The flip side matters too. Tests that are brittle, slow or tied to implementation details can make change harder, failing every time someone improves the internals without altering behaviour. Tests are an investment, and bad investments lose money.
Refactoring: paying down the interest
Every shortcut taken to ship on time leaves behind what Ward Cunningham famously called technical debt. Debt is not inherently bad; borrowing to move fast is often the right call. The trouble is interest. Refactoring — restructuring code without changing what it does — is how you pay the debt down before the interest payments consume the team’s week. It only works safely when the tests above exist, which is a nice example of how these practices lean on each other.
Continuous integration and delivery: small steps, often
CI/CD pipelines automatically build, test and often deploy every change. The quiet insight behind them is that risk grows with the size of a change. Merging three months of divergent work is a nightmare; merging an hour of it is barely an event. By making integration continuous, teams trade rare, terrifying releases for frequent, boring ones. Boring, in production systems, is the highest compliment available.
Observability: seeing what the code actually does
A program on your laptop can be poked with a debugger. A system serving millions of requests across hundreds of machines cannot. Logs, metrics and traces are how engineers ask questions of a running system they cannot pause. Observability accepts a humbling fact: you will not predict every failure, so you had better be able to understand the ones that happen.
Deliberate architecture: drawing the seams
Finally, there is structure. Architecture is largely the art of deciding where the boundaries go, so that a change in one place does not ripple unpredictably through everything else. Clear modules, stable interfaces and explicit dependencies let many people work in parallel without constantly stepping on one another. Conway’s observation that systems tend to mirror the communication structures of the organisations that build them is a reminder that architecture is as much about people as about code.
The cargo-cult trap
Now for the honest part. Every one of these practices can be performed without being understood, and when that happens it becomes ritual.
The term cargo cult, popularised in this sense by Richard Feynman, describes imitating the visible form of something successful while missing the substance that made it work. Software is full of it:
- Coverage theatre. Teams chase a percentage of lines covered by tests, producing tests that execute code without meaningfully checking anything.
- Microservices for a team of four. Architectures designed for organisations with thousands of engineers get adopted by startups, who then spend their time operating distributed systems instead of building a product.
- Process for its own sake. Mandatory approvals, elaborate ticket workflows and ceremonial meetings that once solved a real coordination problem persist long after the problem has gone.
- Copying the giants. “Google does it” is a poor argument when you are not operating at Google’s scale, with Google’s constraints and Google’s budget.
The time-and-people framing is the antidote. Each practice should be able to answer a simple question: what future cost does this reduce, and is that cost real for us? A monorepo, a code review policy or a deployment pipeline is justified by the problem it solves, not by its presence on a list of best practices. Good engineers are not those who apply the most process; they are those who apply the right amount, and notice when the right amount changes.
Scaling the dial
It helps to think of engineering rigour as a dial rather than a switch. A prototype to test an idea deserves speed and very little ceremony. A tool used by your own team deserves version control and a handful of tests. A system handling other people’s money or health data deserves the full apparatus, and then some.
The common mistake is not setting the dial wrong at the start. It is failing to turn it as circumstances change. The throwaway prototype becomes the production system because it worked and nobody had time to rewrite it. The practices that would have made it maintainable arrive late, expensively, or never. Recognising that moment of transition — “this is no longer a weekend project” — is one of the most valuable instincts an engineer can develop.
The long view
There is something quietly humane about software engineering at its best. Its practices are, in the end, acts of consideration for people you may never meet: the colleague who joins next year, the on-call engineer woken at three in the morning, the future version of yourself who has forgotten everything. A clear commit message, a well-named test, a clean boundary between modules — each is a note left for a stranger.
Programming is a conversation with a computer. Software engineering is a conversation with time, carried on through code, among everyone who will ever have to change it.