ExamplesGENERALILLUSTRATIVESIMPLIFIED

Edge Cases From Examples

Once one example exists, the edge cases come from varying it along a short, repeatable list: zero, one, many, duplicate, invalid, concurrent. The list is not a template — it is where the requirements the happy path hid tend to live, and each one either has an obvious answer or is a decision.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

You have the happy path and one worked example. How do you systematically find the cases that will break it, without guessing at random?

The situation

Checkout works. You added a product, paid in test mode, saw the confirmation. Someone on the team asks "what about edge cases?" and you say "like what?" — and realise that you have no method for finding them beyond waiting for a bug report. You could brainstorm, but brainstorming produces whatever you happened to think of.

The reflex

Brainstorm. Open a list, write "empty cart", "invalid card", "product deleted", and stop when the ideas stop. It feels thorough, because the list has things on it, and it is honest work — every item is a real case.

Why it stalls

The list is whatever came to mind, which is whatever has bitten you before. A developer who has never seen a duplicate-submit bug will not write "customer clicks Pay twice", and that is the case that costs money.

What the reflex produces — and fails to produce
  • The list is whatever came to mind, which is whatever has bitten you before. A developer who has never seen a duplicate-submit bug will not write "customer clicks Pay twice", and that is the case that costs money.
  • Nothing on the list is decided. "Empty cart" is written down but not answered: is checkout refused, or is the button hidden, or both? A case without an expected behaviour is a worry, not an edge case.
  • The list has no shape, so nobody can say whether it is finished. Ten items feel thorough; the eleventh — two customers, last unit — is the one that matters, and there was no reason it would come up.
  • The cases are found after the code, so each one is a patch. The happy-path design assumed a non-empty cart at every step, and "empty cart" now needs a guard in three places.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Take the working example and vary it along six axes that are the same for nearly every input a system accepts: zero of the thing, exactly one, many, the same thing twice (duplicate), a thing that is invalid or missing, and two of the thing at the same time (concurrent). Each axis applied to each input of the workflow produces a case; ask "what should happen?" of each.
  • Sort the answers into three piles. Obvious: write the expected behaviour down as a test description. Undecided: this is a requirement nobody stated; take it to whoever owns the product. Impossible-here: the case cannot occur because of a rule elsewhere — write down which rule, because it is now an invariant you depend on.
  • Use the piles to change the design, not just to add guards. If "product deleted while in cart" is possible, the cart holds a snapshot or a reference, and that is a data-model decision (Snapshots vs References) rather than a null check.
  • Stop when each axis has been applied to each input once. The list has a shape now, and finishing it is a fact rather than a feeling. More cases can still appear later; they get added to the same grid, not to a new brainstorm.

The grid, applied to checkout

The grid is the method made visible. Inputs down the side, six axes across, and in each cell either the expected behaviour, "decision", or "impossible because". Most cells take seconds; the ones that take longer are the lesson.

InputZeroOneManyDuplicateInvalidConcurrent
Cart itemsRefuse: "your cart is empty"Happy pathTotal correct; long order page deferredSame product twice → merge lines at add timeDeleted product → decision (remove with message)Two tabs editing one cart → last write wins, accepted for V1
Quantity per lineRefuse the lineHappy pathMore than stock → refuse with countNegative or non-integer → refuse at the boundaryTwo customers, last unit → atomic decrement, exactly one succeeds
Pay requestImpossible: the button submits oneHappy pathSecond click returns the first order (idempotency key)Card declined → order stays unpaid, message shownSame cart from two tabs → same key, same order
CustomerGuest checkout → decision (V1: account required)Happy pathSession expired mid-checkout → cart preserved, re-login

Cases that became design, not guards

A good grid changes the shape of the system in a few places. The decomposition below is the checkout after the grid, with the edge cases that earned a component of their own rather than an if-statement. Every leaf is stated as the observation that would show it works — the same sentence that started as a cell.

Checkout, after the grid
Checkout that survives its edge cases
  • Validate the cart at checkout timethe deleted-product and quantity cells
    • Refuse empty and invalid linestestable An empty cart, a zero quantity and a negative quantity are each refused with a distinct reason before any order exists.
    • Re-check products against the catalogtestable A cart holding a product deleted after it was added is shown a message naming the product, and the line is removed.
  • Make the Pay request idempotentthe duplicate and concurrent cells for the request
    • Key the request on the cart versiontestable Two Pay requests with the same key produce one order and one payment; the second response equals the first.
  • Reserve stock atomicallythe concurrent cell for quantity
    • Conditional decrementtestable Three units, two concurrent orders of two: exactly one order exists afterwards and stock reads one.

Three components came from three cells. The other cells became guards or "later", which is fine — the grid is for finding out which is which.

How the grid goes wrong

The method fails in predictable ways, and each failure has a recognisable symptom. Watching for them is cheaper than filling in every cell of every workflow.

Edge-case discovery, mis-applied
TriggerSymptomCauseResponse
The grid is filled for every field of every formA week of cells, none of which changed anythingThe method applied where the happy path had no hidden assumptionsCore workflow first; one input at a time as features arrive
Every cell becomes an early returnCheckout has eleven guards and still double-chargesCases handled as exceptions instead of examined for mechanismAsk of each case: guard, decision, or design change?
The developer decides the undecided pileA default branch sells the discontinued productA business rule hidden in code nobody asked forUndecided cases go to the owner as examples with two options
Only the six axes are ever usedPrice changed between add and pay; nobody noticedTime and state are their own axesAdd "what changed since?" as a seventh column for stateful workflows

How to do it

Most important first.

  • List the inputs of the workflow first: for checkout, the cart, its items, the quantities, the customer, the payment, the request itself. Edge cases are variations of inputs, so you need the inputs to vary.
  • Apply zero / one / many / duplicate / invalid / concurrent to each input. Most cells are boring; the method is cheap precisely because most cells take five seconds.
  • Write the expected behaviour for each non-boring cell as a sentence a test could be written from, and mark the ones you had to guess (A Slice Is Testable).
  • Pay particular attention to duplicate and concurrent, because the happy path is single-threaded and single-click by construction and those two axes are where it silently assumed the world would cooperate (Duplicate Requests).
  • Take the undecided pile to the product owner as examples, not as abstractions: "a customer presses Pay twice within a second — one order or two?" gets an answer; "how should we handle idempotency?" gets a shrug.
  • Add the decided cases to the invariants list where they belong (What Must Never Break): "stock never below zero" and "one payment per order" were edge cases before they were invariants.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • Checkout, input "cart items". Zero: an empty cart — refuse checkout with a reason, and hide the button; both, because the request can arrive without the button. One: the happy path. Many: a cart of forty lines — fine, but the total must not overflow anything and the order page must paginate or scroll; noted, not V1.
  • Input "the Pay request". Duplicate: the customer clicks twice — the second click must find the first order and return it, not create another one; this is the case that makes an idempotency key a requirement rather than a nicety. Concurrent: two tabs, both paying the same cart — same answer, same mechanism, and now the mechanism has two tests.
  • Input "product in cart". Invalid: the product was deleted by the admin after it was added — undecided, taken to the founder as "Alice has a discontinued lamp in her cart; does checkout refuse, remove it with a message, or sell it anyway at the old price?" She chooses remove-with-message, and the cart now needs to notice at checkout, not only at add time.
  • Input "quantity". Zero: a line with quantity zero — invalid, refuse. Many: quantity larger than stock — refuse with the count. Concurrent: two customers, last unit — the race from Example-Driven Thinking, now sitting in a grid next to its siblings rather than found by luck.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • The edge-case list has a shape — inputs down one side, six axes across — and you can say it is complete for this workflow rather than that it feels complete.
  • Every case has an expected behaviour or a name next to it, and the decided ones read as test descriptions.
  • At least one case changed the design rather than adding a guard: a snapshot instead of a reference, an idempotency key, an atomic decrement.
  • The concurrent and duplicate rows have entries, which means the happy path's single-threaded assumptions have been named.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?What are the inputs of this workflow, and what happens with zero, one and many of each?
  • ?What happens if this request arrives twice, and what happens if it arrives twice at the same moment?
  • ?Which of these cases has an obvious answer, which is a decision nobody has made, and which is impossible because of a rule I am now depending on?
  • ?Which case changes the data model or the mechanism, rather than needing a guard?

What can go wrong

How the move itself fails
  • The grid is filled in for every input of every workflow before anything is built. Six axes times every field is hundreds of cells, most of them boring; the method is for the core workflow first, and for one input at a time when a new feature arrives.
  • The axes are treated as the only edge cases. Zero / one / many / duplicate / invalid / concurrent finds most of them; it does not find "the price changed between add-to-cart and pay" or "the customer's currency differs from the product's". Those come from the state and time axes (Overwrite or Append?), and the grid should say so.
  • Every case gets a guard and none gets a decision. The result is a checkout with eleven early returns and a design that still assumes a cooperative world underneath them.
  • The undecided pile is decided by the developer. "Sell the discontinued lamp anyway" is a business call, and a developer who makes it silently has hidden a requirement inside a default branch.
What the move costs
  • A grid is more work than a brainstorm and most of its cells are empty by inspection. The cost is paid in exchange for being able to say "done".
  • Cases found before the code delay the code. The happy path could have shipped an afternoon earlier; the afternoon bought the duplicate-click case, which would have cost far more found in production.
  • Taking undecided cases to a product owner produces decisions you may not like, and some of them — "sell it anyway" — are harder to build than the guard you would have written.
Misreads
  • "Six axes is the complete list." It is the list that finds the cases the happy path structurally hides — because the happy path always has one of everything, once, valid, alone. Time, state, money and permissions have their own axes, and each domain adds its own.
  • "Edge cases are a testing concern." They are a requirements concern that testing later confirms. The case where a deleted product sits in a cart changed the cart's data model; a test could not have done that after the fact.
  • "Handle every case in V1." The grid tells you the cases exist; the MVP decision says which ones V1 handles and which are written down as known and deferred. "Forty-line carts render badly" is deferred; "double-click charges twice" is not (What Cannot Be Simplified).

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALZero / one / many / duplicate / invalid / concurrent applies to any input of any system — a function argument, a request body, a message on a queue, a file in a bucket; the axes name what a happy path structurally assumes.
  • ILLUSTRATIVEThe empty cart, the forty-line cart, the discontinued lamp and the double click are invented; the numbers are for the shape of the grid, not a real store's limits.
  • SIMPLIFIEDSix axes is a teaching list. Real edge-case discovery adds time (stale state), money (rounding, currency), permissions (who may do this) and locale, and for a parser or protocol the input-shape axes dominate; the six are the ones a happy path hides in every domain.

Where the depth lives

This domain asks the question and hands the answer off by name.