All essays

Paul on Programming

Software Engineering Begins Before the First Line of Code

A controller that compiles, runs, and passes its unit tests can still be the wrong controller.

It can implement the wrong behavior beautifully. It can meet a requirement that was never valid. It can make a perfectly reasonable local decision that becomes unreasonable when connected to the rest of the machine. Those failures belong to software engineering even when the code itself is tidy.

That is why I resist the habit of treating software development as something that begins when a programmer opens an editor. Engineering begins with understanding the problem, the intended use, the constraints, and the evidence that will tell us whether we have succeeded. Implementation gives those decisions an executable form. It cannot make the missing decisions cease to matter.

Consider a hypothetical requirement for a controller to detect a failed sensor and enter a safe state. It sounds sensible. It might even pass through a review without attracting much attention.

But it leaves most of the difficult work unresolved. A sensor can fail silently, drift, become intermittent, report an implausible value, or continue reporting a plausible value that is no longer changing with the physical process. A communication failure can look like a sensor failure. A delayed measurement can be accurate for the instant at which it was taken and inappropriate for the instant at which it is used.

Then there is that phrase “safe state.” The right response depends on the machine, its operating condition, and the hazards involved. Removing an output immediately may be appropriate in one application and create another hazard in a different one. A requirement that leaves all of that to the person implementing the fault handler has delegated a system decision without acknowledging it.

The compiler will accept whichever answer that person chooses.

Before production implementation, I want enough analysis to establish the behavior we intend, the assumptions under which it is valid, and the limits of what we know. That includes ordinary details with extraordinary consequences: units, timing, initialization, resource limits, invalid inputs, loss of communication, and recovery after a fault.

Some of those questions require experiments. I am entirely comfortable writing exploratory code to investigate a peripheral, measure an execution path, or test whether a proposed approach is feasible. The experiment needs a purpose, a way to interpret the result, and a clear account of what remains unproven. Early code can be part of the engineering analysis.

What concerns me is when exploratory code quietly becomes the product baseline because it already runs. The original experiment may have excluded error handling, assumed ideal timing, used a narrow operating range, or depended on a person manually correcting the setup. Promoting it to production requires examining those assumptions and doing the omitted work. A successful demonstration does not perform that examination for us.

This is also why a software development lifecycle matters. Analysis, requirements, architecture, design, implementation, integration, verification, validation, and sustainment describe responsibilities that have to be addressed. They describe how we connect a need to a dependable result and preserve that connection as the system changes.

The responsibilities are related, but the work is not obliged to proceed through them once in a perfectly straight line. A prototype can expose an impossible requirement. Integration can reveal a timing assumption that needs revision. A field observation can send us back to the model of the physical system. A disciplined process provides a way to absorb that information and identify the consequences.

Winston Royce’s 1970 paper is useful here because its actual argument is more interesting than a staircase diagram. He warned about an implementation in which important timing, storage, and input/output behavior would first be experienced during late testing. His proposed additions included preliminary design, a pilot version for critical areas, deliberate test planning, and customer involvement. [1] Whatever lifecycle we choose today, the underlying concern is recognizable: discovering an architectural mistake late can require revisiting much more than a few lines of code.

Calling a team Agile does not settle the issue. The principles behind the Agile Manifesto explicitly value technical excellence, good design, sustainable development, and regular adaptation. [2] Those are compatible with serious engineering. They do not tell an embedded team that system analysis is optional or that every uncertainty will be resolved by the next demonstration.

Likewise, calling a process rigorous does not settle the issue. A substantial document can contain vague requirements. A review can approve an architecture nobody has tested against its hardest constraints. A completed form can show that an activity occurred without establishing that the activity answered the question it was supposed to answer.

I want evidence of understanding, and I want enough recorded reasoning for another competent person to examine it.

NASA’s investigation of the Mars Climate Orbiter loss illustrates why that connection matters. The Phase I report identified a mismatch between the units required for ground software output and the units actually supplied. The required metric units were already specified in interface documentation. The board also identified contributing problems involving communication, the transition from development to operations, and verification and validation of ground software. [3]

The lesson I draw is more demanding than “write down the units.” The requirement must influence implementation, the interface must be checked, unexpected behavior must be investigated, and the people operating the system must understand the relevant assumptions. A specification sitting in a repository is only one part of that chain.

Verification and validation should therefore influence the design early. When we write a requirement, we should think about what observation or analysis would establish that it has been met. When we choose an architecture, we should think about how its significant behavior can be observed. When we make an assumption about the physical environment, we should decide how that assumption will be checked.

Return to the hypothetical sensor fault. A useful verification approach might need controlled stale data, out-of-range values, interruptions, and recovery sequences. It may require observing both internal fault decisions and the resulting physical outputs. We also need to examine whether the specified response meets the actual operational need. Passing a test of the implementation cannot establish that the chosen response was the right requirement in the first place.

Host tests, simulation, and target measurements can each contribute useful evidence. Their conclusions have limits. A host test of a calculation does not measure the target’s interrupt latency. A simulation exercises the model we supplied; a result that depends on an inaccurate model can be reassuring and wrong. A successful target test under one load does not establish behavior under every relevant load. We should be precise about what each result supports.

This does not mean every project needs the same collection of documents and reviews. A disposable laboratory utility and a controller with consequential failure modes justify different levels of effort. Scale the process to the risk, complexity, intended life, and people who will depend on the result. Keep the underlying questions visible even when the records are short.

A concise requirement with a clear rationale and a reproducible check can be more useful than pages of ceremonial prose. A small architecture note that explains a constraint and the alternatives considered can save the next engineer from reopening a settled issue—or help that engineer recognize when it is time to reopen it.

The same care has to continue after release. A field fix changes something. We need to understand what behavior is being changed, what else depends on it, and which evidence must be revisited. The development lifecycle has not ended merely because the work is now called maintenance.

For a team under schedule pressure, doing this work can feel like postponing progress. I think the practical test is whether we are reducing uncertainty about the system we have agreed to deliver. Sometimes that means writing code. Sometimes it means measuring a signal, challenging a requirement, or discovering that the proposed architecture cannot meet the timing budget.

That discovery is valuable even when it produces nothing impressive for a demonstration. It gives us a chance to change direction while we still have choices.

Before writing the next production function, I want to know what behavior it owes the rest of the system, which assumptions it relies on, and how we intend to check it. I do not need perfect foresight. I do need a reasoned answer that the next engineer can inspect.

Analysis, design, process, and standards. Then we can get on with the code.


Sources

1. Winston W. Royce, “Managing the Development of Large Software Systems,” Proceedings of IEEE WESCON (August 1970): 1–9; subsequently reprinted in the proceedings of ICSE 1987. ACM reprint record; accessible scan of the paper. See the discussion accompanying Figures 2–4 and Steps 1, 3, 4, and 5. The cited material is Royce’s original paper, not commentary from the site hosting the scan. 2. Authors of the Agile Manifesto, “Principles behind the Agile Manifesto” (2001). Original text. See the principles concerning technical excellence and design, sustainable development, and adaptation. 3. NASA, Mars Climate Orbiter Mishap Investigation Board Phase I Report (November 10, 1999). Official report. See the executive summary, printed pp. 6–8, and §§4–5. The report identifies both the units mismatch and broader contributing causes; the example should not be reduced to a claim that documentation was simply absent.