A pricing routine applies three discounts, a cap, shipping and tax. Written once with labels and jumps, once with structured blocks and once as named functions, it prints the same totals to the cent. What differs is how much of the routine you must hold in your head to change one rate safely.

Each advance in programming between 1843 and the early 1970s removed a difficulty that came from the tools and left the difficulty that comes from the problem, and design became a subject of its own because that remainder does not go away.

This is the first of eight parts of The Foundations of Software Engineering, a series that follows one small pricing problem through eleven designs and the history behind each of them. The question it asks is why we design software the way we do today.

The limit stated before the machine existed

In 1843 Ada Lovelace published an English translation of L. F. Menabrea's memoir on Charles Babbage's Analytical Engine, with her own Notes, lettered A to G and signed with her initials. Note G sets out, with a table and a diagram, how the engine could compute the Numbers of Bernoulli. It also states a limit that the rest of this article keeps meeting: "The Analytical Engine has no pretensions whatever to originate anything. It can do whatever we know how to order it to perform."

Lovelace is often called the first programmer, and the claim is contested. Allan Bromley's study of the engine records, of Babbage's own work, "several dozen sample programs prepared between 1837 and 1840 (all, incidentally, substantially predating the preparation of Ada Lovelace's notes, which incorporate several of them)". The Computer History Museum limits the claim to this: "The Notes included the first published description of a stepwise sequence of operations for solving certain mathematical problems." First published is defensible. First written is not.

Alan Turing's "On Computable Numbers, with an Application to the Entscheidungsproblem" was received by the London Mathematical Society on 28 May 1936 and read on 12 November 1936; the publisher's record dates the volume 1937, which is why both years are cited.

Turing does not write "Turing machine". His terms are "a-machine" and "universal computing machine", and section 6 says: "It is possible to invent a single machine which can be used to compute any computable sequence." Our reading: if one machine can do the work of any other, the difficulty that remains is in the description given to it.

Stored programs and the first documented reuse

In Manchester the Small Scale Experimental Machine, nicknamed the Baby, ran its first stored program on 21 June 1948. In Cambridge, by the Computer Laboratory's own history, "The first logged program on EDSAC I (computing the squares of integers from 1 to 99) ran on 6th May 1949."

EDSAC is where two burdens are first documented as leaving the programmer. Its "initial orders", written by David Wheeler, are described by Cambridge as instructions that "assembled programs in symbolic form from paper tape into the main memory and set them running". The second release, in August 1949, added relocation "to facilitate the use of subroutines (an important invention by D.J. Wheeler)". A program could now be written in symbols and translated by a program, and a routine written once could be placed wherever the next program needed it.

That is a documented early case of what an assembler does. Kathleen Booth is widely credited with an earlier assembly notation, at Birkbeck in 1947. That credit is not established here against a primary or institutional source, and nothing below depends on it.

Languages that took the machine out of the program text

Grace Hopper wrote A-0 for the UNIVAC between October 1951 and May 1952 and called it a compiler, meaning a program that had the computer copy and assemble library subroutines. In her keynote of 1978 she said of it: "it wasn't what you'd call one today". Calling A-0 the first compiler in the modern sense overstates it.

John Backus gave the reason for FORTRAN as money. His history of the language says: "The cost of programmers associated with a computer center was usually at least as great as the cost of the computer itself." He proposed the project at IBM in late 1953, and the compiler for the IBM 704 was completed in April 1957. It removed the translation of a formula into machine instructions by hand.

COBOL removed a different one. Its report of April 1960, published by the US Department of Defense, traces the language to a meeting in the Pentagon on 28 and 29 May 1959 and to a Short Range Group that "was composed of six manufacturers and three Government representatives". The name, it says, "suggests a COmmon Business Oriented Language". The first word is the aim: one language for business programs across the machines of competing makers. The report names FLOW-MATIC, the language of Hopper's group, as one of three sources, but COBOL was designed by the committee, not by Hopper.

ALGOL 60 came from thirteen authors who, in the words of the Revised Report, "conferred in Paris from January 11 to 16, 1960". Peter Naur edited the Report, which appeared in Communications of the ACM in May 1960. It defines its syntax in a metalinguistic notation, and its language has blocks and recursive procedures. A block gives a stretch of program text its own names and lets it stand where a single statement could: the unit structured programming later reasons about.

These were not successive stages. The COBOL specifications and the ALGOL 60 Report appeared within a month of each other, three years after the FORTRAN compiler, for different kinds of work. None of them said how a large program should be arranged.

YearWhoWhat, in the original termsSource
1843Ada LovelaceNotes A to G; Note G computes the Numbers of BernoulliNotes
1949D. J. Wheeler"initial orders" for EDSACCambridge
1957J. W. Backus and groupFORTRAN compiler for the IBM 704Backus
1960Short Range Committee"COmmon Business Oriented Language"Report
1960Thirteen authors, edited by P. Naur"Report on the Algorithmic Language ALGOL 60"Revised Report
1966C. Böhm and G. Jacopini"languages with only two formation rules"Paper
1968E. W. Dijkstra"A case against the GO TO statement"EWD215
1968NATO conference, Garmisch"the 'software crisis' or the 'software gap'"Report
1971N. Wirth"Program development by stepwise refinement"CACM 14(4), DOI 10.1145/362575.362577
1974D. E. Knuth"Structured Programming with go to Statements"Computing Surveys 6(4), DOI 10.1145/356635.356640
1986F. P. Brooks, Jr."essence" and "accidents"Essay

1968 and 1969: what the NATO reports record

The first NATO Software Engineering Conference met in Garmisch from 7 to 11 October 1968. Its report, edited by Peter Naur and Brian Randell and published in 1969, says of its own title: "The phrase 'software engineering' was deliberately chosen as being provocative". Randell's later account attributes the choice to Professor Fritz Bauer and calls the term "then practically unknown". The conference popularised the term; who first used it is not established by these sources.

The report did not announce a software crisis. Section 7.1.2 records an argument: "There was a considerable amount of debate on what some members chose to call the 'software crisis' or the 'software gap'." It names no author for either phrase and prints an objection from Kolence: "I do not like the use of the word 'crisis'. It's a very emotional word." The unqualified form is Edsger Dijkstra's. His Turing lecture of 1972, "The Humble Programmer", calls Garmisch "a conference that created a sensation as there occurred the first open admission of the software crisis", and that reading stuck.

The second conference, in Rome from 27 to 31 October 1969, produced "Software Engineering Techniques" (1970), edited by J. N. Buxton and B. Randell, not by Naur and Randell as is often written (Randell's archive lists both).

The go to letter and what the theorem does not say

In March 1968 Communications of the ACM printed a letter to the editor titled "go to statement considered harmful". The title is not Dijkstra's. His manuscript, EWD215, is headed "A case against the GO TO statement", and in a note of 2001 he recalls that the editor turned the paper into a letter to speed publication and that "he had given it a new title of his own invention! The editor was Niklaus Wirth." The argument concerns the programmer, not the machine: "The go to statement as it stands is just too primitive, it is too much an invitation to make a mess of one's program."

The result usually cited beside the letter is two years older. In May 1966, in the same journal, Corrado Böhm and Giuseppe Jacopini published "Flow diagrams, Turing machines and languages with only two formation rules".

The paper shows that flow diagrams can be expressed in a language with two formation rules, composition and iteration, given additional variables and predicates. It is a result about what can be expressed. It does not argue against the go to statement or say that a program so transformed is clearer, and the name "structured program theorem" is not in it. Dijkstra himself warned in EWD215: "The exercise to translate an arbitrary flow diagram more or less mechanically into a jumpless one, however, is not to be recommended."

The counterpoint has a date and a title: Donald Knuth, "Structured Programming with go to Statements", ACM Computing Surveys 6(4), December 1974 (DOI 10.1145/356635.356640). Its argument should be read in the paper and is not summarised here. The statement was never simply abolished. C# still has goto, and Microsoft's history of the language lists labeled break and continue among the features of C# 15, released in November 2026.

The same rules, unstructured and then structured

The series' example prices an order by ten fixed rules, listed in its README. Three of them are discounts on the subtotal: 5% from 1,000.00 and 10% from 5,000.00 for volume, 3% for Silver and 7% for the Gold tier, and 10% for the coupon WELCOME10 on a first order.

Stage 00 is a modern reconstruction of unstructured code: today's C#, written with the habits of a language that had only labels, jumps and flags. It is not period code and it reproduces no historical listing. This is how it computes the three discounts:

C#
    if (subtotal < 1000m) goto Loyalty;
    if (subtotal < 5000m) goto SmallVolume;
    volume = Math.Round(subtotal * 0.10m, 2, MidpointRounding.AwayFromZero);
    goto Loyalty;
SmallVolume:
    volume = Math.Round(subtotal * 0.05m, 2, MidpointRounding.AwayFromZero);
Loyalty:
    if (order.Tier == CustomerTier.Standard) goto Coupon;
    if (order.Tier == CustomerTier.Silver) goto Silver;
    if (order.Tier == CustomerTier.Gold) goto Gold;
    goto Reject;
Silver:
    loyalty = Math.Round(subtotal * 0.03m, 2, MidpointRounding.AwayFromZero);
    goto Coupon;
Gold:
    loyalty = Math.Round(subtotal * 0.07m, 2, MidpointRounding.AwayFromZero);
Coupon:
    if (order.CouponCode == null) goto Cap;
    if (order.CouponCode == "") goto Cap;
    if (order.CouponCode != "WELCOME10") goto Reject;
    if (!order.IsFirstOrder) goto Cap;
    coupon = Math.Round(subtotal * 0.10m, 2, MidpointRounding.AwayFromZero);

The whole routine has 20 goto statements and 10 labels. Each test states when to skip a rule, the reverse of how the rule is written. To know when the last line runs you need the four conditions above it and every way of arriving at Coupon: two jumps and a fall from Gold. Nor does the excerpt end where it appears to: three of its jumps go to Cap, a label further down the file.

Stage 01 computes the same discounts with selection and iteration only:

C#
decimal volume = 0m;
if (subtotal >= 1000m)
{
    if (subtotal >= 5000m)
    {
        volume = Math.Round(subtotal * 0.10m, 2, MidpointRounding.AwayFromZero);
    }
    else
    {
        volume = Math.Round(subtotal * 0.05m, 2, MidpointRounding.AwayFromZero);
    }
}

decimal loyalty = 0m;
if (order.Tier == CustomerTier.Silver)
{
    loyalty = Math.Round(subtotal * 0.03m, 2, MidpointRounding.AwayFromZero);
}
else if (order.Tier == CustomerTier.Gold)
{
    loyalty = Math.Round(subtotal * 0.07m, 2, MidpointRounding.AwayFromZero);
}
else if (order.Tier != CustomerTier.Standard)
{
    throw new ArgumentException("Unknown customer tier.", nameof(order));
}

decimal coupon = 0m;
if (!string.IsNullOrEmpty(order.CouponCode))
{
    if (order.CouponCode == "WELCOME10")
    {
        if (order.IsFirstOrder)
        {
            coupon = Math.Round(subtotal * 0.10m, 2, MidpointRounding.AwayFromZero);
        }
    }
    else
    {
        throw new ArgumentException("Unknown coupon.", nameof(order));
    }
}

Only the control flow changed. Each rule is now a block that can be read without the others, and nothing outside the coupon block can enter it halfway. The routine still makes eight separate rounding calls, each repeating the rounding mode, and the literal 0.10m still appears twice with two meanings, the upper volume rate and the coupon rate. The discounts take 42 lines where Stage 00 took 22. Structure bought reasoning about one part at a time. It did not buy brevity, and it put no rule in one place.

Stage 01 is not a mechanical translation, the distinction Dijkstra drew: it was rewritten from the rules. Nor does it keep a strict single exit. It rejects invalid input by throwing from inside its loop, which in our judgement is the clearer form.

The proof that behaviour is the same is Every_stage_prices_every_order_in_the_table_identically in tests/Pricing.Tests/StageEquivalenceTests.cs, which runs a table of 22 hand-computed orders against every stage. Every_stage_rejects_invalid_input in InvalidInputTests.cs checks rejection by exception type only: Stage 00 gives every fault one message, where Stage 01 says which rule was broken.

Stepwise refinement and named functions

Niklaus Wirth, the editor who had retitled Dijkstra's letter, published "Program development by stepwise refinement" in Communications of the ACM 14(4) in April 1971 (DOI 10.1145/362575.362577). The title names the technique. What follows is our use of it, not a summary of the paper: state the task as a few steps in the vocabulary of the problem, then refine each step into a function. Stage 02 is the result:

C#
public static PriceBreakdown Price(OrderData order, decimal taxRate = 0.23m)
{
    Validate(order, taxRate);

    decimal subtotal = Subtotal(order);
    decimal volume = VolumeDiscount(subtotal);
    decimal loyalty = LoyaltyDiscount(order.Tier, subtotal);
    decimal coupon = CouponDiscount(order.CouponCode, order.IsFirstOrder, subtotal);
    decimal discount = CapDiscount(volume + loyalty + coupon, subtotal);
    decimal shipping = Shipping(subtotal - discount);
    decimal tax = Tax(subtotal - discount + shipping, taxRate);
    decimal total = RoundMoney(subtotal - discount + shipping + tax);

    return new PriceBreakdown(
        subtotal,
        [new("volume", volume), new("loyalty", loyalty), new("coupon", coupon)],
        discount, shipping, tax, total);
}

It reads as the list of rules, in order. The functions it calls follow it in src/Pricing/Stage02NamedFunctions/OrderPricing.cs, one for each rule and most of them a single expression.

Against Stage 01, three things changed. Each rule has a name and can be read alone. The rounding mode is written once, in RoundMoney in the same file, where Stage 01 wrote it eight times. And the top routine can be checked against the README by eye.

Three things did not change. The rates are still literals, and 0.10m still means two things in two functions. Every function is a private member of one class, so nothing but a name separates the decision about rounding from the decision about the Gold rate. And it is still one file that knows every rule: a change to shipping and a change to the coupon are edits to the same file. There is a small cost as well: validation and the subtotal now walk the order lines three times where Stage 01 walked them once.

The host program prices three sample orders with every stage. For the order named mixed, which earns all three discounts:

Text
Stage 00 Unstructured          | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 01 Procedural            | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20
Stage 02 Named functions       | mixed  | subtotal  2000.00 | discount   360.00 | shipping 0.00 | tax   377.20 | total  2017.20

The other two orders agree in the same way. The numbers are equal because the rules are the same. Only their arrangement differs.

When the long procedure is the right answer

Stage 01 is not a defect waiting for a refactoring. For a script with three rules and one author, a single procedure that reads from top to bottom is, in our judgement, entirely adequate: there is no indirection to follow, every number is on the screen, and the person who changes a rule is the person who wrote it. Naming functions pays when rules change separately or when a second person arrives. Choosing Stage 01 while neither holds is an engineering decision, to be revisited when one does. Stage 00 is the only one of the three we would not choose, and the objection is to the cost of reading it, not to its result, which is correct.

What every one of these steps left behind

Frederick Brooks gave the remainder a name. "No Silver Bullet" was first published in the proceedings of the IFIP Tenth World Computing Conference in 1986 and reprinted in IEEE Computer in April 1987, so both years are cited.

Brooks divides the difficulties of software into essence, "the difficulties inherent in the nature of the software", and accidents, "those difficulties that today attend its production but that are not inherent". The familiar pair "essential versus accidental complexity" is a later shorthand: his nouns are "essence" and "accidents".

Read through that distinction, which is our reading and not a claim Brooks makes about each case, the history above is a list of accidents removed. Symbolic form and relocation removed writing in the machine's own form and rewriting a routine for each program. FORTRAN removed the hand translation of formulas, COBOL the tie between a business program and one maker's machine, ALGOL 60 the absence of a defined syntax and of local names. Structured control removed the need to trace every jump. None of them removed a pricing rule: the ten rules are present in full in Stage 00, Stage 01 and Stage 02, and would be in any language.

That is why design became necessary. What remains is deciding how the parts of the problem are arranged, so that a change to one rule is a change in one place. Stage 02 does not achieve that, and no further improvement in control flow will. Part 2 takes Stage 02 and divides it into modules that each hide one decision likely to change.

What to take from this

  • Before rearranging code, name the difficulty you are removing. If it belongs to the rules themselves, no rearrangement removes it.
  • If you are tracing jumps, flags or inverted conditions to learn when a line runs, rewrite that part from the rule and not from the existing code.
  • Extract a function when it gives a rule a name you can check against the specification. Do not extract to shorten a routine that already reads from top to bottom.
  • Count the places where one fact is written. Eight copies of a rounding mode are a defect of arrangement even when all eight are correct.
  • Keep the long procedure while there are few rules and one author, and record that as a decision.
  • Put a table of expected results in place first. Without it you cannot tell a redesign from a change in behaviour.

Run the code

The example is one download, software-foundations-example.zip: unpack it and run the commands in its foundations folder. It was built and its tests were run with the .NET 10 SDK on 13 November 2026, and it needs that SDK. .NET 10 is a long-term support release; .NET 11 was released on 10 November 2026 and the example does not need it.

  • dotnet test runs the tests.
  • dotnet run --project src/Pricing.Host prints the output, of which three lines are shown above.
  • ./counterproof.sh copies the solution, changes one rule at a time in the copy and requires the tests to fail each time. Three of its changes touch this article's code: the Gold rate in Stage 01, the first-order condition of the coupon in Stage 02 and the free-shipping threshold in Stage 00.

The tests that cover what this article showed are Every_stage_prices_every_order_in_the_table_identically in tests/Pricing.Tests/StageEquivalenceTests.cs, Every_stage_rejects_invalid_input in tests/Pricing.Tests/InvalidInputTests.cs, and The_host_prints_exactly_the_committed_expected_output in tests/Pricing.Tests/HostOutputTests.cs, which compares the host's lines with src/Pricing.Host/expected-output.txt.

References

Tags#software-foundations#software-design#engineering#dotnet#development#havecode

The Foundations of Software Engineering

From the origins of programming to modern architecture. 8 parts, read in order.

  1. 01
    Before Design Patterns: How Software Engineering Learned to Manage Complexity

    You are here.

  2. 02
    Structured Programming, Modularity and Information Hiding

    Structured control flow, procedures, modules, information hiding, coupling, cohesion and separation of concerns are separate ideas from separate authors. The same pricing rules, cut as functions and then as modules, show what Parnas's criterion localises and what it does not.

  3. 03
    Object-Oriented Programming: Origins, Promises and Trade-offs

    Object orientation is several mechanisms with separate origins: Simula's subclasses, abstract data types, Smalltalk's messaging. One pricing example shows what encapsulation and an interface buy, what they cost, and when functions over plain data stay the better design.

  4. 04
    The Gang of Four: 23 Design Patterns and the Problems They Solve

    The 23 patterns of the 1994 book, each as a problem, a shape, a cost and its modern C# form, in one pricing example. Most are a single move, an interface placed where something varies, and the example needed only one of them.

  5. 05
    SOLID, Coupling, Cohesion and Dependency Inversion

    Coupling and cohesion are the criteria; the five SOLID principles are later heuristics with separate origins in Meyer, Liskov and Martin. One pricing calculator shows what each buys and costs, and why dependency inversion, dependency injection and a container are three different things.

  6. 06
    Refactoring: Improving Design Without Changing Behaviour

    Refactoring changes structure against a recorded definition of behaviour, in steps small enough to check. One legacy pricing method is pinned by characterisation tests, reshaped in seven steps, and the tests themselves are proved able to fail.

  7. 07
    From Design Patterns to Domain-Driven Design and Modern Architecture

    Domain-driven design, ports and adapters, the onion and the clean architecture share one rule: the business rules refer to nothing else. The pricing example gains a domain model and a tested boundary, and needs 22 types where one class did.

  8. 08
    Do We Still Need Gang of Four Patterns in Modern C# and .NET?

    All 23 patterns set against C# 14 and .NET 10: eleven have a shorter form in the example, built from a language or platform feature, and twelve do not. The problems remain in every case, and each modern form has a failure of its own.

Support independent engineering knowledge

Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.

Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com

Comments