Interactive course · about 7 hours

Software Construction

Your program works. You ran it, it did the thing, and you were right to feel good about that. Then somebody else opens it in March, changes one number, and three unrelated parts stop working. This course is about the distance between code that runs and code that can be changed by a stranger without fear.

How this works

Every idea here is something you do rather than something you read. You write assertions and watch them pass or fail against a function you cannot see inside. You fill a map of an input space with your own test cases and find the corners you missed. You break a data type's private promise on purpose, then install the tripwire that catches it. The JavaScript you write runs on an interpreter that watches every statement, so a loop that never ends is stopped and explained rather than left to spin.

What you need first

You can write a little JavaScript: variables, loops, functions, objects and lists. That is the whole prerequisite. Nothing about testing, specifications or design is assumed, and the words are all introduced where they first appear. If you have written a program and watched somebody else struggle to read it, you are exactly the right reader.

Where that JavaScript is taught

One course comes before this one: Writing an Application. These are the parts of it this course leans on, with the step that teaches each. Step 2 introduces say(), the word this site uses for printing a line. Step 3 makes a named box with let and const. Step 5 chooses between two paths with if and else, and compares with === and >=. Step 6 counts round a for loop. Step 7 holds many things in a list, takes one back out by its position, counts them with .length and adds one with push. Step 8 declares a function, calls it, and hands an answer back with return. Step 9 keeps named values together in an object and reaches inside one with a dot.

You do not have to go and read that first. Every one of those words is explained again here, in a sentence or two, at the first place this course uses it. The links above are for when the short version turns out not to be enough.

The steps

Step 1

Why working code rots

Code you have just written is the easiest code in the world to change. The whole of it is in your head: every constant, every assumption, every place a number happens to appear twice. None of that survives a month, and none of it was ever in anybody else's head at all.

So "does it work" turns out to be the small question. The larger one is what happens the first time somebody changes it, which for any code worth keeping is roughly always. Below is a shop's order module, written twice. Both versions give identical answers today.

That module is JavaScript, and a few of its words carry the whole course. function shipping(order) { ... } declares a function: a named set of steps, with order a slot for a value handed in from outside. Writing shipping(myOrder) calls it, and myOrder fills that slot for that one run. The slot is a parameter and the value put into it is an argument. return ends the function there and hands one value back to whoever called it. That is not the same as printing: a returned value can be stored, added to and tested, while printed text can only be read. let sum = 0 makes a named box holding 0 and a let box can be refilled later, while const makes one that keeps whatever it was first given. if (sum >= 30) { ... } runs the part in curly brackets only when the question is true, and else says what to do when it is not. >= is greater than or equal to, so 30 itself counts, and > would leave it out; <= is the same idea the other way round, less than or equal to. And the dot in line.price means "reach inside this thing and name one part of it".

What "a module" means here

A module is a group of functions that belong together and share a few rules. In this course a module is short enough to read on one screen. In a real project it might be a file, a class, or a folder with thirty files in it.

The size changes and the question does not: how much of this do I have to understand before I can change one line without breaking something I have not looked at.

What is a list, and what is that order?

A list holds many values one after another, in one box. Square brackets make one: let prices = [14, 3, 9]. Square brackets also take one back out by its position, and positions are counted from 0, so prices[0] is 14 and prices[1] is 3. prices.length is how many items there are, 3 here, which means the last position is always one less than the length. prices.push(7) adds 7 to the end and leaves the list one longer.

The order handed to each function in the lab below is a list, with one item in it per line of the order. Lists are what Steps 6 and 7 spend all their time on, because two names can end up holding the same list without either of them saying so.

What does line.price mean?

An object keeps several named values together as one thing. Curly brackets make one, with a name, a colon and a value for each part: { price: 14, qty: 2 }. The names are called keys and what sits after each colon is that key's value.

The dot reaches in and names one of them. If line holds that object, then line.price is 14 and line.qty is 2, and line.qty = 3 puts a new value in that one part. So every line of the order is an object, and line.price * line.qty is what that line costs.

What does for (let line of order) do?

It runs the part in curly brackets once for every item in order, and each time round line holds the next item. Four lines in the order means the body runs four times, which is how one short loop totals up an order of any length without anybody knowing in advance how long it is.

There is a second way to write a loop, and you will need it later: for (let i = 0; i < order.length; i = i + 1). Start i at 0, keep going while it is under the length, add one to it each time round. i is short for index, meaning the position and the item at that position is order[i]. Reach for this one when you need the position itself, as you will in Step 9, where a check has to compare each item with the one before it.

Lab 1 · Change one number, count the wreckage
Try this firstLeave the mark at 25 and press change the obvious place. Three of the four checks go red, and the delivery charge you actually edited is not one of them.
Pick a new free-delivery mark, then change it three ways. First the way a hurried person does it, by finding the obvious place and editing that. Then everywhere. Then in the tidy version, which has one place to change. The four behaviour checks run for real against whichever module you built.
Working is a snapshot. Changeable is a property.

Both modules pass every test the shop has today. They differ in what it costs to change them, and that cost is invisible until somebody tries. Most of what follows in this course is ways of making that cost visible early, while it is still cheap.

Lab 2 · Which functions actually care
Try this firstMark banner as depending on the mark, then press Check by running them. Each function is called twice with two different marks, and the row tells you what came back each time.
Mark the functions you think depend on the free-delivery mark, then check. The answer is not read out of a table. Each function is run twice with two different marks and compared, so what you get back is measured behaviour rather than a guess about the code.
A teammate raises the free-delivery mark from 30 to 50. They search their file for 30 in the delivery function, change it, run the app, and the delivery charge is right. A week later the banner on the basket page still says "spend 2 more for free delivery" when the basket is at 28. What happened?
It was written down twice. Every duplicated constant is a promise that two distant pieces of code will be edited together forever, by people who do not know the promise exists. Naming it once and reading it from there is not tidiness for its own sake: it removes the chance of the two copies disagreeing.
Step 2

Saying what a function promises

A function is a deal between two people who never meet: whoever calls it, and whoever wrote it. Nearly all the pain in shared code comes from that deal living in one person's head and nowhere on the page.

Written down, the deal is called a specification, or a spec in everyone's mouth, and it has two halves. The precondition is what has to be true before the call, and keeping it is the caller's job. The postcondition is what will be true when the call returns, and keeping that is the implementation's job. Splitting them apart is what makes blame answerable.

// find(sorted, target)
//   requires: sorted is a list of numbers, in increasing order
//   effects:  returns an index i where sorted[i] is target,
//             or -1 if target is not in the list

That comment is a spec. The line marked requires: is the precondition and the line marked effects: is the postcondition, and from the next lab onwards you will see a third line, modifies:. modifies: nothing is a promise that the function will not change anything you handed it: the list you passed in comes back exactly as it went in. Leaving the line out promises nothing at all. Write the line even when the function modifies nothing. Step 6 is about what goes wrong when a function quietly modifies something and never said so.

Where a spec actually lives, in a real project

In a comment above the function, in a documentation string the language pulls out into a manual, in the types of the parameters, or in the tests. Usually in several of those at once, which is fine as long as they agree.

The format matters far less than one property: somebody who wants to call the function has to be able to find out what it needs and what it promises without reading the body. A promise that only exists inside the implementation is a description of today's code, and descriptions go out of date silently.

What is that code actually doing?

Guessing the middle, over and over. lo and hi mark the stretch of the list the target could still be hiding in, and they start as the whole thing. mid is the position halfway between them; Math.floor just rounds down, because there is no position 2.5. If the item sitting at mid is the target, done. If it is too small, then everything from lo up to mid is too small as well, so lo jumps past the lot in one move. If it is too big, hi comes back the same way. Either way half of what was left disappears each time round, so a list of a thousand items is finished in ten guesses instead of a thousand.

while (something) { ... } is a loop with no counter in it: do the body again for as long as the condition is still true. Here it says "keep halving while there is any stretch left to search". And every bit of that depends on the list being in order: jumping past everything below mid is only safe if everything below mid really is smaller. That is precisely what the precondition is buying, and Step 9 shows you what happens to this same search when the order quietly stops holding.

What are those three equals signs asking?

=== asks whether two things are exactly the same, and the answer is true or false. So sorted[mid] === target asks "is the item at position mid the very thing we are looking for". !== is the opposite question, and < and > ask smaller and larger in the way they do in maths.

Keep all of those apart from = on its own, which asks nothing at all: lo = mid + 1 is an instruction to put a new value in the box called lo. Asking and putting are two different acts that look nearly identical on the page. Step 10 is about how much harder "exactly the same" becomes once the two things being compared are lists.

Lab 3 · Who broke the deal
Try this firstSwitch the implementation to the sloppy one and change nothing else. The list is still sorted and the answer is still wrong, so the verdict moves from the caller to the implementation.
Break each half of the deal in turn. Hand the careful implementation an unsorted list and it returns nonsense, and that is not its fault. Hand the sloppy implementation a properly sorted list and it also returns nonsense, and that is entirely its fault. The verdict is worked out by running the search and then checking the answer against a slow, obviously correct scan.
Say the precondition another way

A precondition is something the function is given, rather than something it checks. "Requires: the list is sorted" means: if you hand me an unsorted list, I owe you nothing, and anything at all may happen, including a plausible wrong answer that you do not notice for a year.

That sounds harsh, and it buys something real. A function that has to cope with every possible input is bigger, slower and harder to reason about than one that is allowed to assume. The cost is that somebody has to make the precondition true. That somebody has to be told.

What is say(), and is it real JavaScript?

say(something) prints one line into the output panel under the editor, and that is the whole of what it does. It is not part of JavaScript. It is a word this site adds to the interpreter, the program that runs your code on this page, because it is shorter to read than the real one. In a browser you would write console.log(something) instead and nothing else would change.

Printing is not returning, and the difference matters from here on. say(x) puts x somewhere you can read it and the program cannot use it afterwards, while return x hands x back to whoever called the function, to store or add or test. A function that says its answer instead of returning it looks as though it works and cannot be built on, and none of the checks in Part 2 can see it. The other page word in the next two labs is check, which records one comparison and carries on; Step 3 is where it is explained.

Lab 4 · Write to a spec, not to a guess
Try this firstPress Check against hidden lists before you write anything. It gets through the first list and then names the one it fails on.
The spec is the whole brief. Your function is run against lists you cannot see: single items, negatives, repeats, a long one. It is never handed an empty list, because the precondition says it will not be. You may add a guard for the empty case if you like, and you will not be marked on it either way.
Lab 5 · Scratch pad
Try this firstPress Run before changing anything. One of the two assertions fails, because the body returns 0 and the spec above it promises something else.
Nothing here is marked. A spec, a body, and check. Write the body so the assertions agree with the spec, then change the spec instead and watch which assertions stop making sense. Three other specs are behind the buttons, including one where the answer is right and the deal is broken anyway.
A function's spec says requires: n is at least 1. Someone calls it with 0 and gets back a nonsense answer instead of an error. Who is at fault, and what is the honest fix?
The caller broke the deal. There are two honest fixes and one dishonest one. Fix the call so it never passes 0. Or widen the spec, promising something specific for 0, and then implement that promise. The dishonest fix is to add a check inside that returns something plausible while the spec still says nothing about 0, because now the real behaviour and the written deal disagree, and the next person reads the deal.
Step 3

From “I think it works” to evidence

"I think it works" is a feeling about code. Evidence is a list: inputs you chose, the answer you expected for each one, and what the machine actually gave you. A single line of that list is called an assertion and a pile of them is a test suite.

You write one like this: check(what you got, what it should be,"a name for the case"). The first part is a value your program has just computed, almost always a call to the function you are testing. The second is what you say it ought to be, worked out by you, by hand, from the spec. Never by running the code and copying down what came out: that writes down a fact about today rather than a promise. The third is a name, so that when it fails you know which case broke. If the first two disagree the case is marked failed and shown to you, and the program carries on to the next line.

Here is the function the next three steps work on. You get its spec and not its body, which is the situation you are usually in, and the better situation to test from: you are checking the promise, not the code that happens to implement it today.

// fee(weight, distance)
//   requires: weight and distance are numbers, both at least 0
//   effects:  returns what the courier charges, in pounds
//
// The shop's rules, in the shop's words:
//   a parcel up to 1 kg going up to 5 km travels free
//   everything else starts at 3
//   over 20 kg adds 5
//   over 100 km adds 4
//   over 20 kg and over 100 km adds a further 2 on top
Why am I not allowed to see the code

Because tests written while looking at the body tend to test the body. You see a loop and write a case that walks the loop; you see three branches and write three cases. What that misses is the behaviour the code was supposed to have and does not, because there is nothing on screen to remind you of it.

Testing from the spec alone is called black box testing, and it is the kind that can catch a missing rule. The other kind, where you read the code and make sure every line runs at least once, is called glass box testing. It is a useful second pass rather than a replacement.

What check() is, and what it is called in real projects

check(what you got, what it should be,"a name") records one assertion and moves on. If the two disagree, the case is marked failed and shown to you. That is the whole idea, and every testing tool in every language is a dressed-up version of it.

The spellings differ. You will meet assertEquals(want, got) in Java, assert got == want in Python, and expect(got).toBe(want) in JavaScript. Different words, same three parts: a value you computed, a value you expected, and a name so you know which one broke.

Lab 6 · Predict every verdict, then run them
Try this firstMark the third line as will fail, then press Run all six. Two of the six do fail, and the reason is given underneath.
Commit to pass or fail on each line before you run anything. Two of these six do fail. When one does, exactly one of two things is wrong: the code, or the expectation. Deciding which is the real work of testing, and the assertion cannot do it for you.
A test that cannot fail is not a test

An assertion earns its place only if there is a version of the code that it would catch. Otherwise it is a comment that costs time to run. The next lab makes that concrete: your suite has to pass against the real function and fail against a sabotaged one, or it does not count.

Lab 7 · Write a suite that can catch something
Try this firstPress Check my suite with the single case that is already there. It asks for at least four assertions, which is the first of the two things it wants.
Four assertions or more, all passing against the real fee. Then the same suite is run a second time against a version that has quietly dropped one of the shop's rules, and at least one of your cases has to notice. Read the rules above again before you start: one of them only shows up when two conditions are true at once.
Your suite of 30 assertions is green. A colleague asks whether the function is correct. What is the most honest answer?
Thirty inputs, chosen by you. Tests are evidence, not proof, and the evidence is only as good as the choosing. That is why the next step is about which cases to pick: thirty cases from the same corner of the input space tell you almost nothing, and five cases spread across the regions and their boundaries tell you a great deal.
Step 4

Choosing the cases that matter

Two numbers, each of them free to be anything. You could test for a hundred years and cover none of that space. Testing everything is available only for functions so small that you would not bother.

What you can do instead is carve the space into regions, where the function is supposed to behave the same way everywhere inside a region, then take one case from each region. Then take the boundaries between the regions as well, because a boundary is where somebody typed > and meant >=. How much of that carving your cases have actually visited is called your coverage. Nine regions with at least one case in each is full coverage of the regions. Be precise about what that buys you. It is a fact about where you looked, and it says nothing whatever about whether what you saw was right.

How you find the regions when nobody hands them to you

Read the spec and mark every phrase that changes the answer. In the delivery rules those are "up to 1 kg","up to 5 km","over 20 kg" and "over 100 km". Each phrase cuts one axis into bands, and the regions are every combination of bands.

Two axes with three bands each gives nine regions. It grows quickly, which is the point: nine is already more cases than most people write by instinct, and every one of the nine is somewhere the function might behave differently from all the others.

"Input space" sounds grand. What is it?

Every possible call you could make to the function, laid out as a shape. A function of one number has a line of inputs. A function of two numbers has a flat sheet, one axis each, and a particular call is a dot somewhere on it. A function taking three things has a solid, and after that the picture gives up while the idea keeps working.

The map in the lab below is that sheet: weight up the side, distance along the bottom, one dot per test case. The lines drawn across it are the places where the shop's rules change what happens.

Lab 8 · Fill the map
Try this firstType 25 and 150 into the boxes and press Add this case. A second region lights up, and the narration names it and counts what is still empty.
Add cases and watch the map fill. Each band is drawn the same width so the small ones are visible, and every case is placed by working out which band it falls in. The fee shown beside a case is what the current implementation answers, which is not evidence that the answer is right: coverage tells you where you have looked, never that what you saw was correct.
Lab 9 · Cover all nine from code
Try this firstPress Run before changing the code. The map fills in from the two calls the starting suite makes, and reports two regions out of nine.
Nine regions, and land exactly on at least one boundary value. The coverage is measured, not claimed: every call your suite makes to fee is recorded, and the region is worked out from the arguments that actually arrived. Assertions that fail do not count as coverage, because a suite you have to ignore is not covering anything.
The rule is "over 20 kg adds 5". You have tested 10 kg and 30 kg and both were right. Which case is most likely to catch a mistake?
Exactly 20. A mistake between > and >= is invisible at 10 and at 30 and visible only at 20, on that single value. Fifteen tells you what 10 already told you. A thousand is worth a case for a different reason, overflow and silly-value handling, but it will not find this one.
Step 5

The bug your tests do not catch

A green suite means every case you thought of behaved the way you expected. It says nothing about the cases you did not think of, and that is where the bug is, because if you had thought of it you would have written the code differently in the first place.

Below are three functions with passing suites and hidden bugs. Your job each time is one assertion: it has to pass against the correct version and fail against the broken one. That is the exact definition of a test that pulls its weight.

Why "make it fail on the broken one" is the right standard

Deliberately breaking a function and checking that your suite notices has a name: mutation testing. Tools do it automatically, changing a > to a >= or deleting a line, then reporting which mutations nobody caught. Every uncaught mutation is a behaviour with no test behind it.

You can do the cheap version by hand any time. Break the code on purpose, run the suite, and if it stays green you have just learned something about your tests rather than about your code.

Lab 10 · Three rounds, one assertion each
Try this firstPress Does it catch the bug before adding a case. The three cases already there pass against both versions, which is exactly the problem.
Keep the cases that are already there and add your own. Round one hides at a boundary. Round two hides in an input nobody types. Round three is the mean one: every number in the existing suite has a single digit, and that is not an accident.
Round three: what was actually wrong with it?

Putting a list in order is something the language will do for you, and by default it does it as though everything in the list were text. Text is compared character by character, the way a dictionary orders words, so "10" comes before "2" for exactly the reason "apple" comes before "b": the first character settles it. On single digits that gives the same answer as ordering by size, every time, which is precisely why the bug can hide. The fix is to hand the sort a small function of your own that says which of two numbers should go first.

The testing lesson is the one worth keeping, though. Three cases, all passing, all drawn without thinking from the same narrow shape, and a bug sitting just outside it. Ask it of any suite you write: what do all my cases have in common that they did not have to have? Single digits. All positive. All the same length. Never empty. Every one of those is a corner you have not looked in.

Lab 11 · Let the machine hunt instead
Try this firstPress Hunt bug A: the missing pair rule. It finds a disagreement in the first few random cases, then shrinks it to something you could have written yourself.
Throw random inputs at both versions until they disagree, then shrink. The first failing case is usually a mess of decimals. Shrinking retries smaller and rounder values while the disagreement survives, and what falls out is a case a person could have written, sitting exactly on the boundary that was missed.
Lab 12 · Scratch pad
Try this firstPress Run. The assertion fails, which is the point: at exactly 20 kg the real fee and bugB give different answers, and one line of the output shows both.
Nothing here is marked. The real fee, both broken copies and check, with no goal attached. The line underneath counts how many of the nine regions and how many of the four boundary values your calls actually reached, so you can aim at the gaps and watch the count move.
Words that come from other people having jobs

Production is the copy of the software that real people are actually using, as against the copy on your own machine or the one the tests run against."It works on my machine and breaks in production" is the oldest complaint in the trade. The gap it describes is the reason a bug can be real and invisible to you at the same time. A codebase is just all of the code for one piece of software, taken together.

Three more you will meet on this page. A code review is somebody else reading your change before it is allowed in. A README is the file at the top of a codebase saying what the thing is and how to run it. A screenshot test takes a picture of the screen and complains when the next picture differs. None of those is enforced by the machine: the first two are people agreeing, which is exactly why Step 8 argues that hiding something in the code beats agreeing not to touch it.

A bug reaches production. You can reproduce it. What goes into the codebase first?
The failing test first. It proves you have actually reproduced the thing rather than something that looks like it, it tells you the moment the fix works, and it stays behind forever to stop the bug coming back. A fix with no test is a fix that gets undone by somebody who does not know why the line was there.
Step 6

Two names for one list

A number in a variable is the number itself. A list works differently: the variable holds a reference, which says where the list lives, and two variables can hold the same reference. Change the list through one name and the other name sees it, because there was only ever one list.

Two names for one object has a name of its own: it is called aliasing, and the two names are aliases for each other. It is the single most common source of bugs that make no sense. The code you are staring at is correct. Something on the other side of the program is holding the same list.

Lab 13 · One box or two
Try this firstLeave both choices where they are and look at the picture. Two names, two arrows, one box: the third line never mentions a, and a changed anyway.
Choose how b is made, then change b and look at a. The picture is drawn from the run, not from the source: after the program finishes, the two values are compared for identity, and the arrows follow whatever the answer was.
So how do you actually make a copy?

a.slice() builds a brand new list holding the same items as a and hands it back. Two boxes now instead of one, so b.push(99) lands in b's box and a never hears about it. The name is odd because slice can also be given a start and an end, to copy part of a list. Called with nothing in the brackets it copies the lot, and that is the everyday way of saying "give me my own list".

You could write it yourself: start an empty list, loop over the original, push each item onto the new one. That is exactly what slice is doing for you. Worth knowing, because the copy is never free: copying a million-item list really does do a million pieces of work. That is the whole reason the language hands out arrows by default and makes you ask for the copy when you want one.

Why the language does it this way at all

Because a list can be enormous. Copying one every time it is passed to a function would make passing a million-item list a million-step operation, and most of the time you did not want a copy. So the language passes the reference, which is one small number, and copying becomes something you ask for.

The cost of that speed is that sharing is the default and it is invisible in the code. Nothing at the call site says whether the function you are handing your list to intends to change it. Only the spec can tell you, which is why specs say things like "modifies: nothing".

Lab 14 · A function that leaves its argument alone
Try this firstPress Check it leaves the original alone before changing anything. The returned list is correct and the check still fails, and the message says exactly what it caught.
Return a new list; leave the one you were given untouched. The checker holds on to the original list, calls your function, and looks at the original afterwards. It also calls twice with the same list, which catches the version that quietly grows it each time.
A function is documented as effects: returns the list sorted. It is implemented with list.sort(), which sorts in place and returns the same list. Callers pass their own lists in. What breaks, and when?
It works until it does not, somewhere else. This is the shape of the whole family: the code with the mistake runs perfectly, and the damage appears in a different function, written by a different person, that reasonably believed its list had not moved. Either copy before sorting, or say "modifies: list" in the spec so callers know what they are agreeing to.
Step 7

Drawing what is really there

When aliasing bites, the way out is to stop reading and start drawing. A snapshot diagram is a picture of one moment in a running program: a small box for every name, an arrow from a name to the thing it refers to, and a box for every object that exists.

Two names with arrows into one box is a fact you can see. The same fact written as four lines of code is something you have to work out, and people work it out wrongly.

Lab 15 · Draw it before you run it
Try this firstAnswer the three pairs for Program 1, then press Check the diagram. The verdicts and the picture both come from running the program.
Decide same box or different boxes for each pair, then check. The truth is settled by running the program and comparing the two things for identity, so the diagram you get back is the one the machine actually built. The third program is the one that catches nearly everybody.
Lab 16 · How deep does a copy go
Try this firstPick one of the three answers, then press Run it. Two of the three are things the program looks like it does, and the printed line settles it.
Predict what gets printed, then run it. A copy of a list of lists copies the outer list only. The inner lists are shared by both, so a change through one of them lands in both, and a change that replaces a whole slot does not.

Two words for that, and you need both later. A shallow copy duplicates one level: a new outer list holding the very same arrows the original held. A deep copy walks the whole structure and duplicates everything it finds, all the way down to the bottom. slice() is shallow, which is why the inner lists in the lab above turned out to be shared, and why the deep version needed a loop that slices each inner list too. The note below is about why any language would hand you the shallow one by default.

Shallow, deep, and why anybody chooses shallow

A shallow copy duplicates one level: a new outer list holding the same references. A deep copy walks the whole structure and duplicates everything it finds. Deep is what people usually mean when they say "copy", and it is also the one that can be slow, can loop forever on a structure that points back at itself, and can duplicate things that were meant to be shared.

Most languages give you shallow by default and make you write the deep one, because shallow is cheap and predictable. Knowing which one you have is enough to avoid nearly all of the trouble.

Lab 17 · Scratch pad
Try this firstPress Run and count the boxes before you read the line underneath. Three names, and fewer boxes than names.
Nothing here is marked. Write any program and get the snapshot of the moment it finished, drawn from the values themselves rather than from the source. Draw what you expect on paper first, then run it: the useful runs are the ones where your drawing and the machine's disagree.
You have a snapshot showing two arrows from different names into one box. Which statement is safe?
A change to the box is seen by both. The last option is the one people get wrong. Reassigning a name (b = somethingElse) only repoints that one arrow; the box is untouched and a still refers to it. Changing the contents (b.push(1)) reaches through the arrow into the shared box, and that is what both names see.
Step 8

Hiding how it works

A stack is not a list. A stack is three operations, push, pop and size, plus a promise about the order things come back in: whatever went on last is the first thing to come off. Think of a pile of plates. The fourth plate goes on top of the third, and when you take one you get the fourth back, not the first. Push "a", then "b", then "c", and three pops hand you "c", then "b", then "a". Popping when there is nothing there hands back undefined. A list is one way to keep that promise. Whoever uses the stack has no business knowing which way you chose. There is a word for whoever uses it, the client, and it does not mean a customer. The client of a piece of code is simply whatever code calls it: another part of the same program, or a program somebody else wrote next year.

That is what an abstract data type means: the type is defined by what you can do with it, not by what is inside it. The inside is called the representation, and the whole design question is whether anyone outside can reach it.

Here is the shape every type in this course uses. makeStack() is an ordinary function, and what it hands back is an ordinary object, except that the things stored under its keys are functions rather than numbers. So let s = makeStack() gives you an object, and s.push(4) reaches into it for the function stored under push and calls it with 4. The list that actually holds the items is declared with let inside makeStack, above the return, where all three functions can see it and nothing outside can name it.

Wait, a function stored inside an object?

A function is a value, the same way 7 and "hat" are values, so it can be kept under a key like anything else. { push: function (x) { items.push(x) } } is an object with one key, called push, whose value happens to be a function. Reading it back with s.push hands you the function itself; writing s.push(4) hands you the function and then calls it with 4.

The other half is that makeStack runs its body afresh every time it is called. Each call builds its own items list and its own three functions, which is why two stacks made from the same makeStack know nothing about each other. If you moved let items = [] up above the word function, there would be one list in the whole program and every stack would share it. The lab checks for exactly that.

How do I take the last thing off a list?

list.push(x) puts x on the end, which you have met. list.pop() is its exact opposite: it takes the last item off and hands it back, so let last = list.pop() both shortens the list by one and tells you what was removed. Called on an empty list it removes nothing and hands back undefined.

Those two operations are all a stack needs, and that is not a coincidence. A list with things pushed on and popped off at the same end already behaves the way a stack promises to behave. Which is the point of the step: a stack is not the list, it is the promise and a list is one convenient way of keeping it.

Lab 18 · Build the stack from its operations
Try this firstPress Check the operations on the empty ones you were given. Two steps pass by luck before it stops and names the third.
Three operations, and nothing said about how. The checker only ever calls push, pop and size, in sequences you cannot see, including popping an empty stack and running two stacks side by side. Any inside you like is fine as long as the operations behave.
How the inside gets hidden when there is no private keyword

Some languages have a keyword: private in Java, an underscore convention in Python, module-level visibility in Go. JavaScript's oldest and strongest answer is the closure. Variables declared inside the factory function are reachable by the operations defined inside it and by nothing else, because there is no name for them outside.

It is not a rule that somebody chooses to respect. There is no expression a client can write that reaches those variables, which is a different and much stronger kind of hidden.

Lab 19 · Watch a client cheat, then make it impossible
Try this firstPress Run the cheat on the open stack and read the last two lines of output. The stack says its size is 0 while there is still something inside it.
The same client code, against two stacks. The first one hands out its inner list along with the operations. The client reaches past push and pop, and the stack starts lying about its own size. The second one hands out the operations only, and the same line of client code cannot run at all.
A team agrees in a code review that nobody will touch stack.items directly, and writes it in the README. Two years later, is the representation hidden?
Reachable means reached. Not because people are careless, but because at three in the morning during an outage, stack.items.length is right there and it works. Once one caller depends on the inside, changing the inside breaks that caller, which is exactly the freedom the abstract data type was supposed to buy you. Hiding it in the code costs one line and settles the question permanently.
Step 9

The tripwire inside the type

Every data type keeps a quiet promise about its own insides. A sorted list is sorted. A fraction is in lowest terms. A stack's count matches how many items it holds. That promise is called the representation invariant, and nothing on earth checks it unless you write the check.

When an invariant breaks, the program does not stop. It carries on with a structure that no longer means what every other function assumes it means, and the wrong answer surfaces later, somewhere unrelated. So you write the check yourself: a small function that looks at the inside and answers true or false, called after every operation that could have damaged anything. That function has a customary name, checkRep, short for "check the representation". You will meet it under that name in real code and for the rest of this course. This page also calls it the tripwire, because that is what it is for. It repairs nothing. It goes off at the exact moment something steps on it, which is the only moment when the cause is still standing next to the symptom.

Representation, invariant: two words at once

The representation is the actual stuff a data type keeps inside to do its job: the list behind the stack, the two whole numbers behind a fraction, the table behind a set. Step 8 called it the inside. This is the proper name for it.

An invariant is something that stays true the whole time, no matter which operations get called or in what order. Put together, a representation invariant is a sentence about the inside that every operation promises to leave true: the list is sorted, the count matches the length, the bottom of the fraction is never zero.

Lab 20 · Off, then on
Try this firstRead the output as it stands, with the tripwire off, then press checkRep on. Same four inserts, same bug, and the program stops in a completely different place.
Run the same four inserts twice. With the tripwire off, everything succeeds and then a search says an item is missing when you can see it in the list. With the tripwire on, the program stops inside the insert that broke the order, and names what broke. Same bug, and a very different distance between cause and symptom.
Does this stay in the shipped code

Often yes. A check that costs a few comparisons next to work that costs far more is not worth removing, and in a running system a loud failure at the right place beats a quiet wrong answer everywhere else. Some languages let you switch assertions off at build time so you can keep them in the source and pay nothing in production.

The one to think about is a check that walks the whole structure inside an operation that is supposed to be quick. Checking that a thousand-item list is sorted, on every insert, turns cheap inserts into expensive ones. The usual answer is to keep it on in testing and in development, and to sample it rather than delete it in production.

How do I ask whether something is a number?

typeof x hands you back a word describing what kind of value x is. typeof 7 is "number", typeof"7" is "string" (string is the usual word for text), and typeof true is "boolean". So typeof xs[i] !=="number" is how you ask "is this item something other than a number".

Why the invariant bothers to say it: a list will hold anything you put in it, and the text "2" sits quietly between 1 and 3 when you compare it with <. A check that only compares neighbours waves it straight through. Then some later operation does arithmetic on it and gets "21" where it wanted 3. An invariant is the place to say what the inside is allowed to contain, not only what order it has to be in.

Lab 21 · Write the check itself
Try this firstPress Check against hidden reps before writing anything. Returning true always gets four representations right, and then meets one that is out of order.
One function, true or false. It is run against valid and broken representations you cannot see, including the empty one and the single-item one, which are the two most commonly forgotten. It must also leave what it is given exactly as it found it: a check that sorts the list in order to see whether it is sorted has destroyed the evidence.
Where does a checkRep call belong?
At the end of anything that mutates. That is where the damage is done, so that is where the shortest distance between cause and message lives. Checking on the way in as well is useful for catching damage done by something outside, and if the check is expensive that is the one to drop first. Putting it only in the tests misses the case that matters most: a real input, in production, that no test ever tried.
Step 10

When is one thing equal to another

Equality looks like the dullest idea in programming until you write it down. Three different questions are hiding behind one symbol: are these the same box, do these hold the same contents, and can any operation tell them apart.

The third one is the definition professionals reach for. Two values are observationally equal if no sequence of operations in the type's interface can distinguish them. The interface is the list of operations the type offers, and nothing at all about how they are done. Everything else is a way of approximating that cheaply.

"Observationally equal", said with two vending machines

Two machines stand side by side. You can put coins in and press buttons, and that is all you can do, because the panels are welded shut. If every sequence of coins and buttons gets the same thing out of both, then as far as anybody using them is concerned they are the same machine. One might hold its cans in a spiral and the other in a stack, and no customer can ever find out.

A data type is the same arrangement. The operations are the buttons. Two values are observationally equal when no sequence of operations tells them apart. Take a set, which Step 11 builds twice. It is a type that holds a collection of different things. You can put something in, ask whether something is in it, and ask how many different things it holds. Those three are the whole interface. Notice what is not on the list: nothing asks which order they went in. Which is why a set holding 1 and 2 equals a set holding 2 and 1: there is no button you can press that tells them apart.

Lab 22 · The gallery of things that are not what they look like
Try this firstPredict the first line, then press Evaluate all six. Every answer is worked out by evaluating that expression on the spot.
Predict each one, then run it. Every answer here is computed by actually evaluating the expression, not quoted from a table. Three of the six have nothing to do with JavaScript being odd. They are how arithmetic works on real hardware, and every language you ever use does the same.
Why 0.1 + 0.2 misses, in one paragraph

Numbers are stored in binary, as a sum of halves, quarters, eighths and so on. A tenth is not a sum of those, in the same way that a third is not a finite decimal: 0.3333 never quite gets there. So 0.1 is stored as the nearest number that binary can hold, 0.2 likewise, and the sum of those two nearest numbers is not the nearest number to 0.3.

No better computer fixes this. The way out is to stop comparing decimals for exact equality: ask whether the difference is smaller than some tolerance, or hold money as a whole number of pennies and never divide it.

Writing an equality that goes all the way down needs one idea you have not used yet: the function has to call itself.

A function that calls itself, is that allowed?

It is, and it is the only sensible way to compare two things when you do not know how deep they go. Asking whether [1, [2, [3]]] equals [1, [2, [3]]] means comparing the outer lists position by position, which means asking whether [2, [3]] equals [2, [3]], which means asking whether [3] equals [3]. It is the same question three times over, on smaller and smaller pieces. So write the question once, and let it ask itself.

What stops it going on forever is that each call is handed something strictly smaller than what it was given. Eventually you reach a piece with no parts inside it: a number, some text, true, null. There the answer is just ===, and the function answers without asking again. That case, the one that does not call itself, is the one to write first. A recursive function with no such case runs until the interpreter stops it. This page's interpreter will stop it and say so.

Three helpers you will want. typeof x hands back a word saying what kind of thing x is: "number", "string", "boolean", "object". Lists and objects both answer "object", which is why Array.isArray(x) exists to tell those two apart. And Object.keys(obj) hands back a list of the object's key names, so Object.keys({ x: 1, y: 2 }) is ["x","y"], which you can loop over with for ... of and count with .length.

Lab 23 · Write deep equality
Try this firstPress Check against hidden pairs before changing the body. The very first pair, two lists both holding 1, 2 and 3, already fails.
Same information, whatever the boxes. Tested against nested lists, objects whose keys are written in a different order, values that differ one level down, and the empty cases. The recursive shape is the point: two things are deep-equal when their parts are.
A test does check(makeSet([1, 2]), makeSet([1, 2])) with an equality that compares the inside lists position by position, and it fails, because one set stored them in a different order. What is wrong?
It is comparing the wrong thing. Nothing in a set's interface can tell you which order the items went in, so two sets holding the same items are observationally equal and a correct equality has to say so. Sorting inside would make this particular comparison work, and it fixes the symptom by constraining the representation, which is backwards: the interface decides what equal means, and the representation then has to keep up.
Step 11

One interface, two implementations

An interface is the list of operations and what each one promises, with nothing said about how. Write your tests against the interface and something useful falls out: the same suite runs against any implementation of it, and swapping one for another becomes a decision about speed rather than a rewrite.

Here is a set, with the usual three operations, built twice. One keeps a list and searches it. One keeps a table and looks straight up. Both keep the same promises.

// makeSet()
//   effects: a new empty set, with three operations
//     add(x)   puts x in; adding the same thing twice changes nothing
//     has(x)   true when x is in the set
//     size()   how many different things are in it
Interface, in languages that have the word

Java writes interface Set and the compiler refuses to build anything that claims to be a Set without every operation. Go infers it from the operations a type has. Python leans on the operations existing at the moment you call them. Here it is an object holding three functions, which is the same idea with no ceremony.

What none of those languages check is the promises. Nothing in a type system says "adding the same thing twice changes nothing". That part is the spec, and the only thing that enforces it is the test suite you run against every implementation.

How can it find something without looking through everything?

The list version starts at the front and compares its way along, so asking whether something is in a set of two hundred can cost two hundred comparisons. The table version does something else entirely: it turns the item into a place. It runs the item through a fixed piece of arithmetic that always gives the same answer for the same item, and puts it in the slot that arithmetic names. Later, to find out whether the item is there, it runs the same arithmetic, goes to that one slot and looks. One look, and it does not get slower as the set fills up.

You have used a table already without calling it one. An object with keys is exactly this. counts["apple"] does not walk the object's keys one at a time looking for "apple"; it works out where "apple" is kept and goes straight there. The table-backed set in this step is an object being used that way. That is the whole of why its cost per operation barely moves, while the list version's cost grows with every item you add.

Lab 24 · One suite, both implementations, and a race
Try this firstPress Run the suite on the table version. The same eight assertions, an implementation with nothing in common with the first, and the same result.
Run the suite against each, then run the workload. The suite is identical, so passing it twice is evidence that the two are interchangeable. The operations are tallied as the work happens rather than looked up, and the gap the race opens between the two totals is the whole reason anybody builds the second implementation. Then run the hasty one and watch the same suite catch it.
Lab 25 · Add a third implementation
Try this firstPress Run the shared suite on mine before changing anything. One line fails, and it is the one about adding the same thing twice.
Your own set, judged by somebody else's tests. The suite that runs against yours is the same one that ran against the other two. That is the promise of an interface: you did not have to write tests, and nobody had to read your code to trust it.
Lab 26 · Scratch pad
Try this firstPress Run. All eight pass. Now break one operation on purpose and find out how many of the eight notice.
Nothing here is marked. The implementation is at the top and the shared suite sits underneath it, both editable. Swap in a sorted list, put a tripwire inside, move the storage outside makeSet and see which promise breaks first. Anything the suite stays quiet about is a decision the interface left to you.
You swap the list-backed set for the table-backed one. The shared suite passes. A page that displays the set's contents starts showing them in a different order, and a screenshot test fails. Whose mistake is this?
The page depended on something unpromised. This is so common it has a name: Hyrum's law, which says that with enough users, every observable behaviour of your system will be depended on by somebody, whatever the spec says. Two honest ways out. Fix the page to sort what it displays, or widen the interface to promise an order, and then every implementation has to keep that promise forever.
Step 12

Harden a messy module

Everything so far, applied to a piece of code somebody else wrote in a hurry. It works for the orders it has seen. It is about to be changed by you, and then by three other people. Changing the shape of code without changing what it does is called refactoring: splitting a long function into named pieces, giving a bare number a name, moving a decision to one place. It is most of what you are about to do here.

Three properties, each measured by a probe rather than by opinion. Safe from bugs: it holds up on inputs nobody tried. Easy to understand: the parts have names and can be looked at one at a time. Ready for change: the rule that changes most often can be changed in one move.

What refactoring is, and what it is not

Refactoring means changing the shape of code without changing what it does. Splitting a long function into named pieces, giving a number a name, moving a decision to one place. The behaviour before and after is identical, which is what makes it safe to do in small steps.

Here you are doing two jobs at once, because the module also has a bug in it, and separating those is the professional habit: refactor with the tests green, then fix the bug as its own change, so that when something breaks you know which of the two did it.

How does one function change something all the others can see?

Declare it once, above all of them: let freeAt = 30. Every function written underneath can read freeAt, because a function can see the names that surround it as well as its own parameters. And one of them can change it, like this: function setFreeDeliveryAt(mark) { freeAt = mark }. After that, every function that reads freeAt sees the new value, without being told and without being edited. That is what Step 1's tidy module was doing, and it is what "the rule that changes most often can be changed in one move" looks like written down.

It is also the thing to be careful with, and you already know why. A value that anybody can reach and anybody can change is the trouble from Step 6 one floor up. It is tolerable here because there is exactly one of them, it is changed through one named function rather than from wherever and the module owns it. A module with nine of them, changed from everywhere, is the mess somebody calls you in to fix.

Lab 27 · Three properties, one module
Try this firstPress Run before you change a line. All three lights are red, and each one names the first thing its probe found.
Get all three green. The scoreboard probes the module by calling it: hidden orders including the empty one and a line with a quantity of zero, each named piece called on its own, and the free-delivery mark moved to see whether every answer follows. Read the rules at the top of the code before you start, especially the line about an order whose subtotal is 0.
Lab 28 · Scratch pad
Try this firstPress Run, then change the two numbers in the fee call and run it again. Nothing here is marked, and nothing you do is recorded.
Nothing here is marked. The delivery function, the assertion runner and a blank page. Try the thing you were not sure about while reading: what your deep equality does on a list inside an object, whether your stack survives being popped twice, what happens to a suite when you break the code it tests.
You are about to restructure a module that has no tests. What is the first move?
Pin the current behaviour down first. Tests written afterwards can only tell you that the new code does what the new code does. Tests written first turn "I think this refactor is safe" into a green bar, including the parts of the old behaviour that were accidental and that somebody now depends on. A rewrite from scratch throws away every one of those accidents at once, which is why rewrites so often ship worse software than the mess they replaced.

What you can do now

  • Write a spec with a precondition and a postcondition, and say who is at fault when each one breaks.
  • Turn a belief about code into a suite of assertions that can fail.
  • Carve an input space into regions, cover them, and pick the boundary cases where the mistakes live.
  • Write the test that catches a bug a green suite missed, and check your suite by breaking the code.
  • Tell aliasing from copying, draw a snapshot diagram, and write functions that leave their arguments alone.
  • Build a data type that hides its representation, and install a checkRep that stops the program at the moment the invariant breaks.
  • Say what equality means for a type you designed, and implement it.
  • Test against an interface so that two implementations become interchangeable.

Where this goes

  • Concurrency. Everything in Step 6 gets sharper when two things run at once: shared mutable state is manageable when one thread touches it and nearly impossible when two do. Immutability stops being a style preference and becomes the main defence.
  • Language engineering. Specifications, invariants and abstract data types are the tools you build a compiler out of, because a parse tree is an abstract data type with a rep invariant and a lot of operations.
  • How programs break. The same argument one layer down. A buffer overflow is a rep invariant broken by something outside the type, in a language where nothing is hidden and there is no tripwire unless you wrote one.
Step 13

Treat generated code as an untrusted patch

A language model or coding agent can search a repository, explain unfamiliar code, propose a test and edit several files. That changes the speed of software work, not the definition of a correct change. The output is a patch produced from incomplete context and statistical prediction. It must pass the same specification, tests and review as a patch written by a person.

Begin with a bounded task: the required behaviour, files that may change, commands that may run, data that may be read and actions that require separate permission. Give the tool the relevant source and project instructions, then inspect the diff rather than accepting its final explanation. A concise explanation can be wrong; a test can be written to confirm the model's own mistaken interpretation.

Run formatters, type checks, unit and integration tests in a controlled environment. Add a regression test that fails before the fix and passes after it. Review changed interfaces, dependencies, error paths and security boundaries. If an agent can write to external services, deploy or delete data, trusted code and user permissions must authorise each class of action. Generated text never grants that authority.

When is an AI-generated test useful?

It is useful when it states an independent requirement and would catch a plausible wrong implementation. Run it against the old code, the proposed change and a deliberately broken variant. A test that passes every version supplies no evidence.

Keep hidden or independently written evaluation cases for work where the generator also writes the implementation. Otherwise the same misunderstanding can appear in both code and test, producing a green but incorrect pair.

Lab 29 · Review four proposed changes
Try this firstCompare the small tested patch with the wide agent run. Both have green tests. Identify why only one passes the mechanical scope and authority gates.
These are entry gates, not a full review score. Passing them means the patch is ready for requirement, diff and test-quality review. It does not prove the tests cover everything that matters.
A coding agent fixes the requested bug and all tests pass, but it also updates five unrelated files and publishes a package. How should the run be judged?
The process is part of correctness. Tests do not authorise scope changes or external side effects. Keep the wanted patch, remove or split unrelated edits, and examine why the publish action was available.
Step 14

Use types and still check the boundary

A static type checker examines code before it runs. If a function expects a number and another part passes a list, the checker can reject that path before a person finds it in use. Types make many impossible states harder to express and document what each module expects.

Network responses, saved files and user input arrive at runtime. A type annotation does not inspect those bytes. Parse untrusted data at the boundary, check its shape and turn it into a trusted internal value.

type Quest = { id: string; title: string; done: boolean }
function parseQuest(value: unknown): Quest {
  // Return a Quest only after every required field passes.
}
Does a typed program have no type errors?

It has no errors covered by the type rules and declarations that passed. An unsafe cast, incorrect library declaration or unvalidated runtime value can still make a declaration disagree with reality.

Types also do not prove business rules such as “the end date follows the start date.” Enforce those with constructors, runtime checks and tests.

What are union and generic types?

A union says a value is one of several named shapes. Checking a field such as state tells the checker which other fields are available.

A generic describes one relationship for many types. Result<T, E> holds either a value of type T or an error of type E without losing which one is present.

Lab 30 · Refine unknown data into a Quest
Try this firstTry the four incoming values. Add checks for the object, required fields and field types. Watch which operations become safe after each check.
A boundary parser returns one complete trusted value or a specific error. Avoid spreading half-checked fields through the codebase.
An API response is annotated as Quest but never inspected. What is still missing?
The response starts as unknown data. A checker cannot examine future bytes from a server.
Step 15

Design how operations fail

Some failures are expected outcomes: a title is empty, a record is missing, or a version conflicts. Return a value that makes the caller handle those cases. Exceptions suit faults that cannot be handled at the current layer. Catch them at a boundary that can add context, clean up and choose a response.

Error text should name the operation and useful identifiers without exposing secrets. Preserve the original cause when adding context. “Save failed for quest 42 because the database deadline expired” is actionable.

What must cleanup handle?

Files, locks, transactions and temporary resources need release on success, failure and cancellation. A finally block or resource scope runs cleanup whichever path leaves the operation.

Cleanup can fail too. Record that without hiding the original fault, and make repeated cleanup safe where possible.

Are timeout and cancellation the same?

A timeout is a policy that cancels work after a deadline. Cancellation is the signal sent through the operation. Code must stop new work, release resources and decide what partial effects remain.

A write may have committed before cancellation arrived, so the outcome can still be uncertain.

Lab 31 · Carry an error through three layers
Try this firstTrigger invalid input, a missing record, a deadline and an unexpected database fault. Choose where each is handled, then cancel during the transaction.
Check the response, retained cause and cleanup state. Handling means the layer can make a correct decision, not merely that it can catch an exception.
Why preserve an error's cause when adding context?
Keep both levels. Context explains the operation; the cause explains the dependency.
Step 16

Point dependencies toward stable rules

A dependency exists when one module must know another module's name, data shape or behavior. When business rules import a database driver, changing storage reaches into the rules. Define a small port owned by the rules, then let a database adapter implement it.

High cohesion keeps related decisions together. Low coupling limits what one part must know. Splitting every function into its own service creates more network and compatibility coupling rather than less.

What is a dependency cycle?

If A imports B, B imports C and C imports A, none can be understood or replaced independently. Draw the arrows and move a shared rule behind an interface owned by the more stable layer.

Cycles at package or service level affect builds, ownership and deployment more than local recursion does.

What do ports and adapters buy?

The core describes operations it needs, such as QuestStore.save. A database, in-memory test store or remote API can sit behind that port.

Architecture tests can inspect imports and fail when a framework detail points into the wrong layer.

Lab 32 · Break a dependency cycle
Try this firstAdd the five module arrows and locate the cycle. Move one contract to the stable core, then replace the UI and database adapters separately.
The repaired graph has no cycle and no inward dependency on a framework or database. Adapters still depend on the core contract.
Who should own the interface used by business rules to save a quest?
The stable policy owns the port. Database code adapts to that contract.
Step 17

Generate cases and test the tests

An example test checks one selected case. A property states a relationship for many generated values. Reversing a list twice should return the original list. Sorting should preserve every item and place each neighboring pair in order.

When a generated case fails, a shrinker searches for a smaller case that still fails. Fuzzing feeds malformed bytes into parsers. Mutation testing makes deliberate code changes and checks whether the suite notices.

Where does the expected answer come from?

A property can compare two routes, check an invariant or use a slow reference. If the test copies the production algorithm, both may share one mistake. Choose an independent oracle.

Generators should deliberately include empty, maximum, duplicate, Unicode and deeply nested cases.

What does a surviving mutant mean?

The changed behavior may be uncovered, the assertion weak, or the code equivalent or unreachable. Inspect the mutant before adding a test. A mutation score is evidence, not a reason to add meaningless assertions.

Use mutation selectively on important logic because running many altered versions costs time.

Lab 33 · Shrink a failure and kill a mutant
Try this firstGenerate lists for the sort property. Shrink the failure, then change <= to < and see whether the suite detects it.
Keep the smallest counterexample as a named regression test. It explains the rule better than the original large random input.
A mutation survives. What has been proved?
The suite supplied no distinguishing observation. Inspect whether the change matters.
Step 18

Make concurrent changes deterministic

Two tasks can each be correct alone and wrong when their steps interleave. If both read counter 7, add one and write 8, one increment is lost. Protect the complete operation with an atomic update, lock, transaction or one owner that receives messages.

A data race is unsynchronized access where at least one access writes. A higher-level race can exist even with atomic pieces, such as checking a seat and booking it in two separate operations.

Do locks solve every race?

A lock protects code that uses the same lock. Several locks can deadlock when tasks acquire them in different orders. Keep critical sections small and use a fixed order.

Immutability and message passing reduce shared writable state, but ownership and message order still need rules.

How can a race test be repeatable?

Pause each task at named yield points, choose a known interleaving and assert the final state. Save the schedule that exposes the failure.

Continue with Concurrent Data Structures for atomics, memory order and safe reclamation.

Lab 34 · Schedule a lost update
Try this firstInterleave two increments one step at a time. Reproduce 8 instead of 9, enable the atomic operation and replay the schedule.
The schedule is part of the regression case. A test that merely repeats the tasks may never select the failing interleaving again.
Two atomic operations check a seat and then book it. Can the pair still race?
Atomic pieces do not make an atomic sequence. Protect the complete invariant.
Step 19

Change contracts while old versions still run

A public contract includes data fields, errors, timing expectations and side effects, not just a function name. A compatible change preserves what existing callers rely on. Version numbers communicate intent; tests and migration plans establish whether compatibility is real.

Production versions overlap. Expand first while old code still works. Migrate data in a restartable job. Switch readers and writers. Contract last, after no live version or stored record needs the old shape.

Why not change every service at once?

An instant coordinated change is difficult to retry or roll back. Mobile clients may remain old for months. Prefer tolerant readers, additive fields and explicit deprecation windows.

When a semantic break is necessary, version the contract and give callers a tested migration path.

What makes a data migration restartable?

Process bounded batches, record progress and make repeating a batch safe. Verify counts and invariants before and after. Avoid one transaction across the entire dataset.

Rolling code back does not automatically reverse data already changed by the new version.

Lab 35 · Order an expand-migrate-contract release
Try this firstDeploy old and new readers against four schema states. Arrange the six actions so every intermediate combination remains readable.
Test the overlap matrix, not only the final state. Old code reading data written by new code is a common failure point.
When should the old database field be removed?
Contract last. Removal follows the overlap window and verified migration.
Step 20

Know what enters the build

A dependency saves work but adds code, maintainers, update policy and transitive packages to the system. A lockfile records exact resolved versions. A software bill of materials lists what entered a build so a team can answer which released products contain a vulnerable component.

Prefer authenticated registries, verified checksums or signatures, reviewed update automation and reproducible build steps. A scanner reports known findings; it does not prove an unreported package is safe.

Where should configuration and secrets live?

Configuration selects behavior for an environment. Secrets are credentials and keys. Keep secrets out of source, logs, generated artifacts and browser code. Supply them at runtime through a controlled secret store.

Grant each workload only the secret and permission it needs. Rotation must replace both stored credentials and running consumers without an unsafe gap.

Is a pinned old dependency always safer?

Pinning makes a build repeatable, but old code can contain known flaws. Update deliberately: inspect the change, run compatibility and security tests, stage the release and keep a recovery route.

A package with no maintenance plan becomes risk even if it has no current advisory.

Lab 36 · Admit or reject a dependency update
Try this firstCompare four update records. Check provenance, lockfile changes, permissions, tests and vulnerability reachability before choosing a release action.
A version number is not the decision. Record the evidence, affected behavior and reason to accept, hold, replace or remove the package.
What does a lockfile prove?
It records resolution. Review, provenance and vulnerability response remain separate work.
Step 21

Make ownership and decisions visible

A reviewable change has one reason, a small diff, passing checks and enough context to evaluate risk. Code ownership identifies who understands a boundary and must review sensitive changes. It should route knowledge, not create one person who can never leave.

An architecture decision record states the problem, constraints, options, decision and consequences. It preserves why a choice was made. Update or supersede it when the decision changes instead of rewriting history.

What belongs in code review?

Check the requirement, diff, tests, data and security effects, compatibility, operations and deletion plan. Style automation should handle mechanical details so people can examine behavior.

Large mixed changes hide risk. Separate formatting, generated files, dependency updates and behavior when that makes each part independently verifiable.

How should a feature flag end?

Name an owner, intended lifetime, default and removal condition. Test both states while both exist. After rollout is complete, remove the old branch and flag rather than adding permanent combinations.

Flags help separate deployment from release, but they also create runtime states that need monitoring.

Lab 37 · Route a change through review
Try this firstSelect a payment, schema, UI or dependency change. Build the reviewer set, required evidence, decision record and flag-removal condition.
The route should follow affected boundaries, not file count. A two-line permission change can need more review than a large generated snapshot.
What is the purpose of an architecture decision record?
It records reasoning. Later teams can evaluate whether the original constraints still hold.
Step 22

Use production evidence without exposing people

Logs record events, metrics summarize values over time, and traces connect the spans of one request. Include request IDs, operation names and safe dimensions. Exclude passwords, tokens and unnecessary personal data.

A service-level objective states the reliability people need, such as a proportion of successful requests within a latency limit. An error budget measures how much failure remains before reliability work takes priority over risky release work.

What should an incident review produce?

Build a timeline from evidence, describe impact and contributing technical or organizational conditions, then assign specific prevention, detection and recovery improvements.

Stopping at the person who made the last edit hides the system conditions that allowed one action to cause broad harm. Track actions to completion and verify that they change the failure mode.

Can a rollback always restore service?

It can restore old code. It may not reverse database writes, messages, emails or external effects already produced. Test rollback and forward-repair plans against stateful changes.

Gradual rollout, health checks and automatic stopping reduce the amount affected before recovery begins.

Lab 38 · Diagnose one slow request
Try this firstInspect the metric spike, three log lines and trace spans. Find the slow dependency, remove a leaked token field and decide whether the SLO budget permits rollout.
Use the trace to locate time, logs to explain the event and metrics to measure scope. No single signal answers all three questions.
Why is a request ID useful?
Correlation is the goal. Keep sensitive contents out of ordinary telemetry.
Step 23

Measure where change is expensive

Maintainability is observed through changes. Count how many modules a typical feature touches, how often a change is followed by a defect, how long tests and review take, and where dependency cycles or repeated logic concentrate work. A single line-count or complexity score cannot judge design by itself.

Refactor in verified slices. Characterize current behavior, create a seam, move one responsibility, compare results and remove the old path. Do not mix a large behavior change with a large structural rewrite unless no smaller safe route exists.

Should every duplicate line be abstracted?

No. Similar code can change for different reasons. Wait until the shared rule is clear, then give that rule one name. A premature abstraction couples cases that only happen to look alike today.

Duplication of a safety rule or protocol may deserve earlier attention than repeated presentation markup.

Where do the deeper topics continue?

Performance Engineering measures time and resource costs. Reliable Systems develops failure boundaries, retries and operations. Fault Tolerance tests recovery.

Build a Language shows how types, interfaces and invariants support parsers, interpreters and compilers.

Lab 39 · Plan a measured refactor
Try this firstInspect the dependency graph, change history and test times. Choose one seam, order the slices and compare risk before and after each move.
The plan must preserve a runnable system after every slice and name the evidence that permits the next one.
Which is the strongest evidence that a boundary is expensive to change?
Use change history and outcomes. Static signals help locate questions, not settle them alone.