Learn Problem Solving & Engineering Thinking
How to move from a sentence you do not know how to build to the next question you need to answer. Thirty-three modules, from the first hour with a vague goal to using AI without losing understanding.
How to Start
7 lessons"I have no idea where to begin." The loop that turns that sentence into a next question, and the technology-first reflex it replaces.
"Build an e-commerce store" is not a task, it is a sentence. Before any technology, ask what it means: who uses it, what they do, what data exists, what must persist, what can wait — until the smallest useful version is visible and the next question is obvious.
Before "how", answer "what for": what will be different when this is done, for whom, and how would we tell? A goal you can check is the difference between building the store and building a store.
Problem → Understand → Requirements → Constraints → Unknowns → Decomposition → Smallest Useful Step → Model → Experiment / Implement → Observe → Debug → Learn → Iterate. Not a template — a map that says where you are and what comes next.
"I want to build e-commerce" → React, Next, Postgres, Redis, Kafka, Docker, Kubernetes — a reflex, not a plan. Recognise it, understand why it feels like progress, and replace it with Problem → Requirements → Data → Workflows → Scale → Architecture → Technology.
A technology is an answer. Before accepting one, find the question: what requirement it serves, what simpler thing meets it, what it costs to run, and what would have to be true for it to be the right call. The Why Ladder, applied to "we need Redis".
The domain's promise in one move: from "I have no idea" to "I know the next question I need to answer". Progress on an unfamiliar problem is measured in questions sharpened, not features shipped.
Documentation, search, AI, books, examples, libraries and frameworks all accelerate understanding, and the line is that none of them can replace it. The good tool-use loop, the Problem → Ask AI → Copy → Hope loop, and how to tell which one you are in.
Problem Framing
6 lessonsTurning a vague goal into an explicit problem: what is actually being asked, what must be true, what can be ignored for now, and goal against implementation.
"Build an e-commerce platform" is a bad frame; "customers browse, add to cart, pay, and admins manage products" is better; adding what must never happen, what is outside the system and what V1 leaves out is better still. The frame is the first artefact.
"I want to build something like Spotify" contains a problem, and a fixed sequence of questions extracts it: who, what they do, the one workflow that matters, the data, the boundaries, what can wait, what you do not know. The output is a first slice, not a design.
"The user can pay" is a goal. "Stripe Checkout" is an implementation. Requirements that arrive as implementations lock in decisions nobody made; the move is to separate the two so the goal can be met by the best implementation, not the first one named.
Say the problem back in your own words — to the requester, to a colleague, to the page — before solving it. The restatement is where misunderstandings surface, and it is the cheapest experiment in the domain.
For "checkout works" to be true, a set of smaller things must each be true: the cart total is right, stock exists, payment succeeded exactly once, an order was recorded. Decompose the frame into conditions, and each condition is a test and a place a design decision lives.
A frame lists everything the system will need; V1 needs a fraction of it. Deciding what to ignore — and writing down why, and what would bring it back — is how a project gets small enough to start without forgetting what it left out.
Requirement Discovery
7 lessonsFunctional and non-functional requirements, the ones that only appear during implementation, and the order: happy path first, failure path second.
"Need checkout" is one requirement written down and a dozen hidden inside it. Discovery is asking "what if" of the sentence — payment fails, the user refreshes, the backend crashes after the charge, the product sells out — until the hidden ones have names and each can be tested.
A functional requirement says what the system does, for whom, with what observable result. "Manage orders" is not one; "an admin can change an order from paid to shipped and the customer is notified" is. The test is whether you could watch it happen.
Latency, security, availability, accessibility, cost, auditability, scale — the qualities of a system, each of which is a requirement only when it has a measurement and someone who cares about it. The skill is finding the ones this system actually has, and refusing the ones it does not.
Some requirements cannot be found by asking; they appear when the first slice runs and the code asks a question nobody had. That is not a failure of discovery — it is the other half of it — and the move is to notice the question, write it down as a requirement, and decide rather than improvise.
Build the sequence where everything goes right — cart, pay, order, confirmation — before any failure handling, because the happy path is the thing the failures are failures *of*. The order is contested: the failure-first camp has a real argument, and this lesson says where each wins.
With the happy path working, each discovered failure becomes an injection: break the provider, click twice, kill the process, sell the last unit — against real code — and watch what happens before deciding what should. Failure handling designed from observation, not from imagination.
A brief is defined as much by what it leaves out as by what it says. "Build a payment service" that never mentions currencies, refunds, idempotency, a provider or a ledger is not a short brief — it is a brief with five decisions hidden in it, and the strong learner asks before building.
Constraints
5 lessonsTime, users, data, the technologies you know and the ones you must use, budget and legal — the questions whose answers shape the architecture before any diagram.
Seven questions decide the design more than most requirements do: how much time, how many users, how much data, which technologies I know, which I must use, what budget, what law. Ask them before the diagram, because a design made without them is made for a project that does not exist.
The three quantities that shape a design most — how long you have, how many people will use it, how much data there is and how it grows — and the habit of answering them with a number and a source instead of with "a lot" or "not much".
Two technology constraints that look alike and are opposites: what the team already knows, and what the situation imposes. The first is a soft constraint that decides most choices by default; the second is a hard one that has to be understood rather than resented — and the "we must use X" that is neither is the one to challenge.
The two constraints engineers most often leave to someone else and most often get surprised by. Budget decides how many pieces the system may run and how; law decides what may be stored, where, for how long and who may see it. Both are design inputs, and both are cheapest when found before the schema exists.
Given the same requirements, two different sets of constraints produce two different correct architectures. The store with six weeks, two developers and tens of users is a monolith on one server; the same store with a partner sending a launch spike and a platform team is something else. Neither is the "right" architecture — each is the right one for its constraints.
Invariants
5 lessonsProperties that must always remain true — an order total is never negative, a payment never happens twice — found from examples and turned into tests.
A feature list says what the store does. An invariant says what it must never do — an order total is never negative, a payment never happens twice — whatever feature, bug or concurrent user is involved. Finding them is a different question from finding requirements, and it is asked before the data model.
Abstract state is hard to reason about; a concrete case is not. Construct a small example — three units, two buyers, one price change — walk it step by step, and the property that must hold falls out of the moment the example goes wrong. Counterexamples find invariants faster than definitions do.
The running example, worked fully: an order total is never negative, every order item references a product, a payment never happens twice, inventory is never negative. For each — who can violate it, where it is held, and what happens when it is not. The invariants decide the schema and the transaction boundaries before either exists.
Product A has three units. Alice buys two; Bob buys two at the same moment. Every line of both checkouts is correct and the store has promised four units of three. The example reveals the concurrency problem that no single-user test can see, and the thinking move is to name it as a question before choosing a mechanism.
An invariant on a page protects nothing after the person who wrote it leaves. Turned into a test that asserts the property after every sequence of actions — including the interleaving that found it — it becomes the only form of the rule that survives refactors, new endpoints and new engineers.
Assumptions
5 lessonsMaking assumptions explicit — one warehouse in V1 — so that when the requirement changes you know why the architecture has to.
"Only one warehouse in V1" is a fine simplification and a terrible secret. Written down with what depends on it, it becomes the reason you can later say exactly why "multiple warehouses" changes the inventory model, the checkout and the shipping estimate — instead of discovering it one bug at a time.
A requirement is something somebody asked for. An assumption is something the design takes as true that nobody asked for. They look identical in a spec — "orders are in one currency" could be either — and confusing them means either building for a future that was never promised or treating a promise as if it could change.
"Multiple warehouses" arrives. If "one warehouse" was written down with its dependents, you know why the inventory model, the checkout and the shipping estimate all have to change, in what order, and which slice proves the new shape before the rest is rebuilt. If it was not, you find out one bug at a time.
A running list of what the system takes as true, when it was decided, why, what depends on it and what would make it false — kept beside the unknowns and the decisions, reviewed when a requirement arrives, and retired rather than deleted. The unknowns board is where it lives.
Some assumptions are silently load-bearing: one currency, one timezone, one warehouse, users have accounts, the provider is up, a request arrives once. Each is reasonable for V1, each is invisible in the code, and each turns a requirement change into a redesign. Knowing the list is how you notice yours.
Unknowns
7 lessonsAn unknown is not a blocker. "I don't understand payments" becomes a specific question, a tiny experiment, and a return to the main problem.
Before the unknowns can be worked on they have to be separated from what you know and from what you are merely assuming. The board with four columns — known, unknown, assumed, need to verify — is the first hour of any problem you have never seen.
"I don't understand payments" feels like a wall. It is a sentence with no question in it. The move is to ask "what exactly don't I understand?" until the wall becomes "how does a backend initiate and confirm a payment?" — a thing you can go and find out this afternoon.
The rewriting step itself: how a topic becomes a question your system could ask. Subject, verb, object, the step of the workflow it serves, and the observation that would count as an answer.
The full detour from §14: Unknown → Specific Question → Research → Tiny Experiment → Understanding → Return to the main problem. The experiment is the step people skip, and it is the only step that produces knowledge rather than familiarity.
The same need asked three ways: "how do payments work?" cannot be answered; "how does a backend confirm a payment?" is answered by a diagram; "which system should be authoritative for whether an order has been paid?" is answered by a decision. The ladder is how you tell where a question stands.
The board lists what you know you do not know. The unknowns that hurt are the ones not on it — and there are moves that surface them: asking what the experts in this area worry about, walking the failure paths, reading a real system's incident history, and listening for the word "just".
The board as a working document: four columns — Known, Unknown, Assumed, Need to verify — kept for the life of the feature, with the rules for what moves between columns and when. The lab at /thinking/unknowns is this board with a question assessor attached.
Problem Decomposition
8 lessonsThe deepest module. Breaking a store into catalog, cart, checkout, orders and payments — then checkout into its steps — and telling a good decomposition from a technical one.
"Build an e-commerce platform" cannot be built; "list products", "add to cart" and "create an order" can. Decomposition is the move from the one to the other — and the split is judged by whether each piece could be built and checked on its own.
Browse a product, add it to a cart, check out: a capability is something an actor can do, end to end, that you could demonstrate. Splitting a problem along capabilities gives every piece a test in the user's words and a natural order of work.
Frontend, backend, database is a true statement about the solution and an empty one about the problem: it fits every application, so it distinguishes none. It is still the right split in a few specific situations — when the product is already known and one layer is the problem.
A useful subproblem is understandable, testable, meaningful and small enough to build without further splitting. Most bad decompositions fail exactly one of the four, and naming which one tells you how to fix it.
Layer-only, too wide, too deep, a single child, vague names: the five shapes a decomposition takes when it has stopped being about the problem. Each is visible in the tree's outline before any code is written.
A child that is still too big is decomposed the same way its parent was. Checkout becomes Load Cart, Validate Items, Calculate Total, Create Payment, Create Order, Confirmation — and the recursion stops where a leaf could be built on Monday and checked on Friday.
Checkout depends on Cart, Inventory, Payment and Orders — but not all in the same way. Some must answer before the customer can be told anything; some can be told later. Drawing the edges, and marking which kind each is, turns a tree into an order of work and a design for what happens when a dependency is slow.
The whole module applied to one capability: checkout from a sentence to testable leaves, with the layer it crosses, the decisions the split surfaces, the dependency it cannot control, and the tool it did not need.
First Principles
5 lessonsRemoving implementation assumptions to find the fundamental requirement. "We need Redis" → why? → repeated reads are slow. Now evaluate options.
"We need Redis" names a solution. First-principles thinking asks what problem it solves — repeated reads are slow — and only then evaluates options, of which Redis is one. The move is not "avoid tools"; it is "find the requirement before you choose".
"We need Kafka" → why? → async events → why? → checkout should not wait for the email. The real requirement is "send the email asynchronously", and a job table meets it. Kafka may be overkill — and the ladder also says when it is not.
Requirements arrive with implementation baked in: "the cart is stored in the session", "orders are a table", "search hits the database". Strip each implementation word and see what the sentence still requires — often less, sometimes something different.
Payments can fail: that is essential, and every checkout must handle it. Five frameworks for one checkout: that is accidental, and it was chosen. The distinction tells you which complexity to design for and which to remove — and it is not always as clear as the slogan suggests.
"Could work" is the half of the slogan people drop. The simplest thing that could work meets the real requirement — failure handling included — with the fewest parts; the simplest thing that does not is a demo. The move is finding the first, and knowing what evidence would make it insufficient.
Systems Thinking
6 lessonsChanges propagate through connected components. Change a price and it reaches cart, checkout, order history and refunds — so where should price be captured, and who owns it?
Change a product's price and the change reaches the cart, checkout, order history and refunds. Following one fact through every component that reads it reveals where the fact should be captured — and it is rarely where the reflex puts it.
Every component that reads a fact is a component the change must reach. Counting the readers, and the hops between writer and reader, tells you what a change costs and where it will be wrong — before it is made.
The current price of a product and the price a customer paid are two different facts with two different owners. Naming the source of truth for each fact — and noticing when one fact is actually two — is how the same number stops appearing in two versions.
For each piece of information in the system, one component should be allowed to change it and everyone else should ask. Assigning an owner to every fact — cart contents, stock, payment status, read state — is how a system stays consistent without a coordinator.
The payment provider, the email sender, object storage and the shipping carrier are not part of the store. Drawing the boundary — what we control, what we merely call — is the first step of every design, because everything outside the line can fail without asking us.
Everything outside the line can fail, can be slow, can change and can rate-limit — and will, on a schedule you do not control. For each crossing, deciding what the store does in each of those four cases is design work, not error handling.
Abstraction Levels
5 lessonsFrom "user clicks Buy" to the SSD in seven levels. Solve at the highest useful level first; zoom in when necessary; know that the framework is syntax over the mechanism.
"User clicks Buy" is also a POST, a service method, a SQL transaction, a set of database pages, a filesystem write and an SSD operation. Naming the levels a problem lives on — and which one you are standing on — is how you stop mixing questions from different levels into one confused one.
Solve the problem at the highest level where it can be stated and checked, and go deeper only when that level cannot explain what you see. Most problems are solved at the top; the ones that are not announce themselves by a promise that holds and a symptom that persists.
At the architecture level the store is Frontend → Backend → Database. Zoom into Backend and it is Router → Service → Repository; zoom into Database and it is Planner → Index → Pages. Every box is a system at the next zoom, and choosing the zoom is choosing which questions are visible.
"Checkout is slow" → Backend → the database query → the B-tree → pages → the OS cache → the SSD. Each layer is a domain of its own, and the move is knowing when a layer's explanation is complete and when the next one down is required — never skipping a layer, and never going one further than the evidence sends you.
Not "how do I create an Express route" but "an HTTP request arrives, a router matches it, a handler runs, a response is written". Learn the mechanism at the level below the framework and the framework becomes syntax — learnable in an afternoon, swappable in a week.
Vertical Slices
5 lessonsProduct page through API through database to a visible product, instead of a week of database then a week of backend. A slice is independently testable; a skeleton proves the pieces connect.
Not the database in week one, the backend in week two and the frontend in week three — but one product, from page to API to database and back, visible on day one. A slice is one narrow feature through every layer, and it is the unit in which a system actually becomes real.
Building layer by layer feels safe because each layer is a real thing that gets finished. The trap is that a finished layer proves nothing about the product, and every question about the product is deferred to the last week, when they all arrive together.
Browser → API → Database → Browser, doing almost nothing, working end to end, deployed. A walking skeleton is the thinnest possible slice built to prove the pieces connect — and it deliberately proves nothing else.
Create Product → Store Product → Display Product: three steps that together are the smallest thing a store can do that is worth watching. Find it, build it, watch it run, then grow it — because every grown step is tested against something that already works.
Create Order through Frontend → API → Business Logic → Database → Response can be tested on its own, before any other slice exists. That is what makes a slice a unit of progress rather than a unit of work — and the test must say what it does not prove as clearly as what it does.
MVP Thinking
5 lessonsThe smallest version that proves the core idea — products, cart, order, payment — and the line between what can be simplified and what cannot be ignored.
The smallest version that proves the core idea — for the store, products, cart, order, payment — and nothing that does not change whether the idea is proven. Not the product with features removed; the product with the proof left in.
Deciding what stays out is the same decision as deciding what goes in, made honestly: each excluded feature gets a reason, a trigger for revisiting it, and the assumption it leaves behind in the code.
An MVP simplifies scope and keeps correctness where the idea lives; a bad prototype simplifies correctness and keeps scope. The first tests an idea; the second tests whether customers notice.
Most of a system can be simplified in V1; a few things can only be done or not done — money, identity, and data that cannot be recreated. Knowing which is which is the whole skill of scoping an MVP.
V1 shipped and something was learned. The next version is not "V1 plus the phase-2 list"; it is the smallest change that acts on what was learned, with each simplification undone only when its trigger fires.
Data Discovery
5 lessonsFrom "customer places an order containing products" to Customer, Order, OrderItem, Product. Entities from plain English, what must persist, and snapshots against references.
"Customer places an order containing products" is already a data model: Customer, Order, OrderItem, Product. Entities are the nouns, relationships are the verbs, and the hidden entity is the one the sentence needs but does not name.
A requirement is a source of entities only if it is read for them. The rule is a noun the system must remember; the traps are nouns that are really attributes, attributes that are really entities, and the entity you only find by asking "one or many?".
"Users can create projects and invite members" hides User, Project, Membership and Invitation — two of which are verbs. The move is to read requirements as data until a first schema falls out, then hand the schema to Database Engineering to make it correct.
Not everything the system knows must survive a restart. Orders must; a cart may; a product's "in stock" badge is recomputed. Sorting data into must-persist, may-persist and recompute is what keeps the schema small and the losses acceptable.
The price the customer paid lives on OrderItem, not on Product. Whenever a fact was true at a moment and the thing it refers to can change, the fact is a snapshot; a reference is right only when the current value is the one wanted.
Interface Discovery
5 lessonsWhich components must communicate, where the boundary of our system is, and which external dependency has to answer before the user can be told anything.
An interface exists wherever two parts must agree on something. Find them by asking, for every workflow step, who has the information and who needs it — the browser, the backend, the database, the payment provider — and you have the interfaces before any of them has a shape.
The boundary of the system is the line between what you can change and what you can only call. Finding it — payment provider, email, image storage, the customer's browser — tells you which interfaces are contracts with a stranger and which are conversations with yourself.
Once the boundary is drawn, each edge crossing it is a contract to write — in your terms, not the stranger's. The interface says what your system needs from the other side and what it promises back; the provider's SDK is an implementation of that, not a definition of it.
Checkout depends on cart, inventory, payment and orders. Not all of them must answer before the customer sees a result; asking which ones must — and what you would tell the customer if the others are still working — is the decision that separates a synchronous call from an asynchronous one.
An external system can fail, be slow, change, rate-limit you, and repeat itself. None of those is a bug in it; they are properties of being outside. The interface you design around it either accounts for all five or discovers them one incident at a time.
State Modeling
5 lessonsWhat changes over time — order status, cart contents, inventory, payment status — and the state machines that fall out of asking.
Entities say what exists; state says what about them changes. Order status, cart contents, stock levels and payment status all change, each on a different trigger, and each one is a place where the system must know "the current value" — which is where most bugs live.
For every piece of state there is a choice: keep the current value, keep the history, or both. Stock is a number that is overwritten and a history that is appended; the choice depends on who will ask "why?", and getting it wrong is cheap to fix early and expensive later.
When a piece of state has a small set of named values, the values are half the model; the other half is which transitions are allowed and who causes them. Order CREATED → PAID → SHIPPED → DELIVERED, with CANCELLED and REFUNDED as exits, is discovered by asking "from here, what can happen?" until nothing new appears.
PENDING → PAID → FULFILLED, with CANCELLED and REFUNDED added later. Which transitions are legal, who triggers each, and in what order to build them so that the store works before the machine is complete — the state module applied end to end on the capstone's order.
Paid twice, shipped before paid, refunded with no payment: some combinations of state must not merely be rejected at runtime but be impossible to write down. Finding them is a discovery move — ask which combinations of fields would be a lie — and where to enforce each is a decision, not a doctrine.
Failure Modeling
6 lessonsAfter the happy path works: what if payment fails, the request repeats, the database is down, the browser closes. Failure injected on purpose.
The basic path works. Now ask, step by step, what happens if the payment fails, the database is down, the request repeats, or the user closes the browser — and turn each answer into a state the system can be in and a test that puts it there.
"Payment failed" is not one event. The card can be declined, the provider can time out, the browser can close after the charge, the confirmation can arrive twice. The question that sorts them is: who says the payment succeeded, and how does my system find out?
A payment must not happen twice, and the double click, the browser retry, the webhook redelivery and your own retry loop all make the same request twice. Idempotency is the name for the answer; the lesson is discovering the question.
The payment succeeded and the backend crashed before the order was written. Two things that were supposed to be one thing happened separately, and the system is now in a state nobody designed. The move is to find every such gap and decide what fills it.
Once the store works, make it fail on purpose: a payment that times out, a product that runs out during checkout, a checkout submitted twice, a database that is not there. Each injection turns a failure you reasoned about into one you observed.
Five questions to ask of any design, at any boundary: what if it fails, what if it is slow, what if it repeats, what if it arrives out of order, what if it is unavailable. They are dull, they are the same every time, and that is why they work.
Pseudocode
4 lessonsInputs, outputs, state, branches and failures written down before any framework syntax — pseudocode as a thinking tool, not a coding step.
"function checkout(cart): validate cart, calculate total, create payment, create order, return confirmation" — five lines that say what checkout does, written before any framework decides how. The lines are where the missing decisions become visible.
Pseudocode is not a notation for code you have not typed yet. It is a device for finding inputs you forgot, state you did not know you needed, branches you had not considered and failures you had not decided — and it works because it is too small to hide any of them.
The four things every operation has and pseudocode must show — what comes in, what goes out, what is remembered, where the path splits — plus failures, which are branches with consequences. Checkout under all five, and the chat app for contrast.
The pseudocode had six endings on one screen. The code has a route, a validator, an ORM, an SDK and a catch block — and, if you are not careful, one ending. What survives the translation, what the framework adds, and how to keep the branches visible.
Example-Driven Thinking
5 lessonsWhen the abstract problem is hard, construct a concrete case: three units, Alice buys two, Bob buys two at the same time. Edge cases, counterexamples and thought experiments.
"Design the inventory system" cannot be reasoned about; "three units, Alice buys two, Bob buys two at the same moment — what should happen?" can. A concrete example turns a design task into a question with an answer, and the answer usually reveals the requirement the abstract sentence was hiding.
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.
When a claim sounds right — "this always works", "that can never happen" — try to construct one case where it does not. A counterexample found in five minutes on paper is worth more than a week of confidence, and failing to find one after honestly trying is evidence, not proof.
Run the system in your head at one user, a hundred, a million — not to build for a million, but to see which assumptions hold at each size and where the design would first bend. A thought experiment is the cheapest experiment there is; it finds the assumption, and a real experiment then checks it.
Take the store from one server and one database to a size where something breaks, and watch which component is actually the bottleneck at each level — then see that a cache fixes reads and not writes, a queue fixes checkout wait and nothing else, and at low traffic every added component is unjustified. The lab runs the numbers; the lesson is how to read them.
Experiments & Prototypes
6 lessonsA prototype answers a question. Spikes, experiment design, predicting before running, and the feedback loop that beats three weeks of design.
"Can the browser upload a five-gigabyte file straight to object storage without going through our server?" is a question. A prototype that does only that — and nothing around it — answers it in an afternoon. A prototype built to "see how the upload feature would work" answers nothing and takes a week.
A prototype is allowed to ignore polish, scaling, error handling and extensibility — that is what makes it fast. The condition is that it says, in writing, what it ignored and therefore what it does not prove. A prototype that does not say so becomes production by accident, and the omissions become bugs nobody chose.
A spike is a time-boxed investigation whose only output is reduced uncertainty: a question, a box of hours, an answer, and then a decision to discard or integrate. The time-box is what distinguishes it from research that never ends, and the written answer is what distinguishes it from a branch someone will find later and wonder about.
Question, hypothesis, test, observation, conclusion — the same structure whether the experiment is a load test, a spike or a debugging probe. The hypothesis is written before the test so the result can disagree with you; the observation is recorded before the conclusion so the conclusion cannot rewrite it.
Before you run the code, say what you expect to see. If the output matches, you understood the system; if it does not, you have just found the exact place your model of it is wrong — and that is information you can only get by having committed to a prediction first. It is the cheapest experiment in engineering and the most skipped.
Change, run, observe, fix — minutes per cycle — beats design for three weeks, build everything, and discover on the last day that one assumption was wrong. The reason is not that thinking is bad; it is that the cost of a wrong assumption grows with everything built on top of it, and a short loop finds it before anything is.
Debugging
8 lessonsSymptom, observation, hypothesis, experiment, evidence — and the anti-pattern of changing random things. Reproduction, minimal reproduction, binary search, logs as evidence.
A bug is an unknown with a symptom attached. Symptom → observation → hypothesis → experiment → evidence → updated hypothesis is the same loop the domain uses for everything else, and it turns "Payment failed" into a sequence of questions each of which can be answered.
Error → change some code → restart → change something else is the most common debugging method and the worst. Instead ask what changed, what evidence exists, and which layer owns the symptom — three questions that cost less than one restart.
A bug you cannot reproduce cannot be shown fixed. Before hypotheses, get the failure to happen on demand — the exact input, state and environment — and write down the recipe, because the recipe is the test the fix has to pass.
Once the failure reproduces, remove everything that is not needed for it to keep failing — the UI, the real provider, the other tables, the framework — until what remains is small enough that the cause has nowhere to hide.
Frontend or backend? Backend or database? Before commit X or after? Every debugging question that splits the candidate space in half is worth asking before any question that removes one candidate — and git bisect is the same move applied to history.
A log line is an observation that was cheap to make in advance. It supports or kills a hypothesis; it does not generate one. "Add more logging" is only a plan when you can say which hypothesis the new line would distinguish.
An error message answers four questions if you let it: what failed, where, with what input, and which assumption broke. Searching the exact string first skips all four and hands your hypothesis to whoever wrote the top result.
Before changing code or making an observation, say what you expect to see if your hypothesis is right and what you expect if it is wrong. The prediction is what turns a change into an experiment and stops "it looks fine" from meaning anything you like.
Reading & Reverse Engineering Code
5 lessonsAn unfamiliar codebase: entry point, follow the data, find the state, find the side effects, understand the boundary. Not "read every file".
README, entry point, one main feature end to end, the dependencies it pulls in, the data model it touches, and the tests that describe it — in that order, and stopping when the question you came with is answered. Not every file.
Find the entry point, follow the data, find the state, find the side effects, understand the boundary. Five questions that turn an unfamiliar failing system into a path you can put observations on — without first understanding all of it.
Where does the program start? What receives input? Where is data stored? Which modules change state? What external systems exist? Five questions recover the design of a system whose design was never written down — from the running thing, not the folder names.
Pick one value — an order, a price, a cart — and follow it from where it enters to where it rests, through every transformation and hand-off. Code is organised by module; behaviour is organised by data, and following the data reads the code in the order it actually runs.
A snippet from documentation, a search result or an assistant is a proposal, not a solution. Before it goes in: what does it do, why does it work, what does it assume, and how does it fail? Four questions that take minutes and are the difference between using a tool and being used by it.
Reading Documentation
5 lessonsGoal, relevant concept, minimal section, try the example, modify it, apply. Documentation before tutorials, tutorial dependency, and search as a skill.
Documentation is consulted, not read. Start from the goal, find the concept that owns it, read the smallest section that answers it, run the example, change the example until it breaks, then apply it to your problem — and stop reading the moment the next step is obvious.
A tutorial shows one path through a tool, chosen to be watchable. Primary documentation states the contract. Read the contract for anything your system will depend on, and use tutorials for what they are good at: orientation, vocabulary, and seeing the pieces connect once.
Given a library's documentation and no prior knowledge, find five things and stop: how to install it, the one abstraction everything else is built on, one API call you can run, what it does when something goes wrong, and one limitation that would matter in production.
If you can only build what a tutorial builds, you have not learned the tool; you have learned the tutorial. The way out is a ladder: follow it, modify it, build something similar without it, then build from requirements alone — and the last rung is where the understanding is tested.
"My code no work react" finds nothing; "React stale closure useEffect interval" finds the answer in the first result. The query is a compressed statement of what you understand about the problem, and evaluating what comes back — authoritative, current, your version, explains why — is the second half of the skill.
Researching Unknown Technology
6 lessons"What is Redis?" Do not read everything. What problem does it solve, what does it guarantee, what is the simplest example, how does it fail, what are the alternatives.
"What is Redis?" Do not read everything. Ask why you might need it, what problem it solves, what it guarantees, what the simplest example is and how it fails. Five answers on one page beat a weekend of reading, and they tell you what to learn next — or that you do not need to.
Every technology exists for one problem, and the question that cuts through its feature list is which. Ask it of the tool, then ask whether your system has that problem — and if the two do not match, no amount of the tool's other virtues makes it the right choice.
A technology evaluation has six parts: the problem it solves, its mental model, its guarantees, its costs, its failure modes and its alternatives. Filled in honestly for two or three options — including the one you already have — it decides; filled in for one option, it advocates.
Read a technology for what it promises and what happens when the promise cannot be kept. "Fast" and "reliable" are not guarantees; "a write acknowledged is durable across restart" is, and "the most recent writes may be lost on crash" is the failure mode that goes with it. Your design depends on which one you assumed.
A search returns claims, not answers. Four checks turn a claim into something you can build on: is the source authoritative for this tool, is it current, does it match your version, and does it explain why. A result that fails the fourth is a patch, and the first three decide whether it is even a correct one.
Told to add video calls, the reflex is to learn WebRTC's API. The move is to ask what problem it solves, which of its concepts your problem touches, and what the smallest experiment is that proves the pieces connect for you — and to build that, not the feature, first.
Asking Better Questions
4 lessonsFrom "how do payments work?" to "which system should be authoritative for whether an order has been paid?" — questions that reduce uncertainty, and how to ask people.
"How do payments work?" cannot be answered in a way that changes what you build. "Which system should be authoritative for whether an order has been paid?" can. The move is rewriting the question until an answer would decide something.
A question is vague, researchable or decisive, and the difference is checkable: does it name a component, name a boundary, ask for a mechanism, scope itself to my system, and ask for a decision? Five criteria, applied to your own questions before you send them anywhere.
Among all the questions you could ask, the one worth asking next is the one whose answer removes the most uncertainty per hour spent — and a question is an experiment, with a cost, an expected answer, and a decision that depends on it.
A stakeholder, a senior, a provider's support desk: each can answer in minutes what would take you days — if the question arrives with what they need. Bring the expected, the actual, the evidence and your hypothesis, so the answer is usable and so the asking is not delegating your understanding.
Trade-Off Analysis
6 lessonsSimplicity, performance, reliability, cost, security, time, maintainability. Matrices without fake precision, reversible against irreversible, deciding under uncertainty.
"Which is better?" has no answer; "better at what, and worse at what?" does. Every engineering choice moves along simplicity, performance, reliability, cost, security, time and maintainability at once, and the move is naming which ones this decision actually touches before comparing anything.
Simplicity, performance, reliability, cost, security, time, maintainability — each is a question you can ask of a design, with an observation that would answer it. The move is turning the axis names into questions for your system, and noticing which questions this decision does not change.
A matrix of options against axes is useful for seeing which axis the argument is really about — and dangerous the moment the numbers are added up. Monolith against microservices on simplicity, scale and operational cost, scored as positions rather than measurements, with the caveat the numbers cannot contain.
Renaming an internal function costs a minute to undo; a public API, a database schema, a partition key and a cloud architecture cost progressively more — some of them almost everything. Spend decision effort in proportion to the cost of reversing, and make the expensive ones as late and as small as you can.
The evidence is not in and the decision cannot wait. The move is not to guess harder: take the current evidence, make the best choice that is cheap to reverse, build the feedback that would tell you it was wrong, and adjust — a loop, not a bet.
A decision written down with the evidence, the alternatives, the reason, the assumptions and when to revisit is a decision that can be re-made on purpose. One that lives only in the code is re-argued every time the code surprises someone.
Estimation & Ordering
4 lessonsEstimation as decomposition, uncertainty communicated instead of hidden, the riskiest unknown first, and what to build first.
"This will take two weeks" is a feeling about a list you have not written. Break the store into product model, cart, checkout, payment, testing and deployment, estimate each with a range, and the number becomes something you can defend — and revise.
A range you know and a point you say is a lie by rounding. Estimates should carry their uncertainty — what is known, what is assumed, what has not been discovered yet — in a form the listener can plan with, without false precision and without hiding behind "it depends".
When everything is easy except one thing, the one thing decides the project. Prototype the payment integration before polishing product cards — not because payment is most valuable, but because it is the piece whose answer could change everything else.
Dependency, risk, user value and learning value — four questions asked of each piece, and the first vertical slice falls out. One possible sequence for the store, and the other sequences that are also right, and when.
Build vs Buy
4 lessonsIs this core differentiation, can a provider solve it, what does integration cost, what does operating it cost, what happens when the provider fails.
Before "should we build payments ourselves?" can be answered, five questions have to be: is this our core differentiation, can a provider solve it, what does integrating cost, what does operating it cost, and what happens when the provider fails. The answers are the decision; the slogan is not.
"Core" does not mean important; payment is essential and nobody chooses a store for it. Core means the reason a customer picks you — and that is the one thing you cannot buy, because a provider that solves it solves it for your competitors too.
"Build" is estimated for the week it takes; "buy" is estimated for the SDK call. Both are wrong the same way: the cost of a capability is what it takes to connect it and what it takes to live with it, and neither appears in the first estimate.
Every bought capability will fail, be slow, change under you, or rate-limit you — not as a possibility, as a schedule. The build-vs-buy decision owes the design a paragraph for each, and the payment provider and the email provider need very different paragraphs.
Iterative Development
7 lessonsImplementation order, dependency-first and value-first, architecture that emerges from requirements, and the complexity ledger every new component must answer.
Change → run → observe → fix, in loops small enough that each one teaches something. A system is grown from one that already works; scale is added after a working system and a measured bottleneck, not before either exists.
Product model → display products → cart → checkout without payment → order persistence → payment → failure handling → admin: one defensible sequence for the store, with the reason for each step. Not the only sequence — and the device says so.
If B requires A, build or understand A first. Obvious when said, skipped constantly in practice: checkout built before the cart exists, payment integrated before anyone knows what an order is, a feature started on top of a concept nobody has understood.
When the dependencies permit, build the thing a user could actually use. A store that shows products and takes orders without payment is usable in a way that a perfect cart on top of nothing is not. Contested — against building the riskiest thing first — and this lesson says how.
Need persistence → a database. A browser interface → a frontend. Logic the client must not control → a backend. Payment → an external provider. Result: Browser → Backend → Database, plus the provider. Every box has a requirement that put it there, and no box is there without one.
Single server and database. Repeated expensive reads → maybe a cache. Long-running work in the request → maybe a queue and a worker. Each "maybe" waits for the symptom it treats; complexity should have a reason, and the reason should be something you observed.
Every new component answers three questions in writing: what problem does it solve, what complexity does it introduce, what happens if it fails. A component that cannot answer all three is not yet justified; one that can is accountable for as long as it lives.
Learning Without AI
6 lessonsYour understanding, your decomposition, your pseudocode, your attempt — then hints one rung at a time. Explain it back, build from memory, find where you hesitate.
In this mode the AI does not write solution code. It may ask questions, explain a concept, review your reasoning, give a hint, or point at the documentation — everything except the part that would replace your attempt. The rule is about what is delegated, not about the tool.
Problem → your understanding → your decomposition → your pseudocode → your attempt → then ask for help. The order is the lesson: help given after an attempt corrects it; help given before one replaces it.
Hint one: the conceptual direction. Hint two: the relevant abstraction. Hint three: pseudocode. Hint four: a partial implementation. Then the reference. Each rung gives less than the next, and the rung you stop at is the measure of what you still own.
Explain it in your own words, draw it, apply it to a case it was not explained with. Three checks that turn "I read it and it made sense" into evidence — and that fail, usefully, exactly where understanding stops.
Close the reference. Rebuild the core idea from nothing. Compare with the reference. Fix the understanding, not the code. The differences are the only precise map of what you did and did not learn.
Can you explain it simply? Where do you hesitate? Which term are you hiding behind? The hesitation is not a failure of nerve — it is the most precise locator of a gap in understanding there is, and the term you reach for is the gap's name.
Using AI Without Losing Understanding
7 lessonsThink, form the question, use the tool, understand the answer, verify. AI as reviewer and debugging partner, the dependency check, and what you still own.
Problem → Think → Form Question → Use Tool → Understand Answer → Apply → Verify. The tool is in the middle of the sequence, not at the start of it — because a question you have not formed produces an answer you cannot judge.
Your design → AI critiques → you decide → you implement. The assistant is strongest as a second reader of something you made, because then its output is a list of objections you can judge, not a design you have to adopt.
Before the assistant can help with a bug, you must bring four things: expected behaviour, actual behaviour, evidence, and a hypothesis. Bring fewer and it guesses; bring all four and it investigates with you.
Four questions about any code the assistant helped with: could you explain it without the tool, recreate its core idea, describe its failure modes, and debug it yourself? Where the answer is no, go one layer deeper — that layer is yours now, whether you understand it or not.
"If you are nothing without the LLM, you should not have it." Read constructively: the tool should amplify an engineering ability that exists, not stand in for the understanding required to own the result. The strongest opposing view — that fluency with the tool is itself the new skill — is stated and taken seriously.
The assistant can produce code, architecture, SQL, tests and explanations. You still own requirements, correctness, trade-offs, security, failure handling and understanding — the things that need to know what the system is for, and the things that are yours when the output is wrong.
Current goal, assumptions, unknowns, decisions, experiments, findings, next step. A notebook in your own words is where independent reasoning is built — and where you can see, at a glance, whether the tool has been doing yours.
Problem Solving Across Domains
7 lessonsHow the loop enters DSA, databases, backend, frontend, system design, ML and agentic engineering — the same questions, different next lessons.
Problem → Constraints → Pattern → Algorithm. The algorithm is the last step, and the step people start from. The constraints — how much data, how often it changes, what "fast enough" means — are what pick the pattern, and the pattern is what picks the algorithm.
Data → Access Patterns → Model → Index → Query. The database domain teaches models, indexes and query plans; this lesson is the step before — discovering that there is data, how it will be read and written, and only then which model and which index.
User Workflow → Request → Business Rules → State → Dependencies. The backend is where the workflow becomes a request, the request meets the rules, the rules change state, and the state depends on things outside the process. Each arrow is a question; the backend domain answers them.
User Goal → Interaction → State → UI. The screen is the last step. What the user is trying to do, the interactions that get them there, and the state each interaction reads and changes come first — and they decide the UI far better than a component library does.
Requirements → Scale → Bottlenecks → Architecture. Architecture is the last step, and the reflex starts there. The requirements say what must hold; the scale says how much load; the bottleneck is what actually breaks first under that load; the architecture is the smallest change that relieves it.
Business Problem → Target → Data → Metric → Model. The model is the last step, and the one the word "AI" makes people start from. The target is what you are actually predicting, the data is whether you can, the metric is what "good" means to the business — and the ML domain teaches each once they are named.
Agentic architecture should emerge from requirements, the way any architecture should. Do not start with a multi-agent system; start with one deterministic workflow and ask, at each step, why it is insufficient. The answer names the one place an agent is needed — or finds that none is.
Case Studies
7 lessonsAn online store, a chat app, a URL shortener, file upload, search, an analytics dashboard, an AI assistant — each started from one sentence, each grown only as far as its requirements justify.
One sentence — "build an online store" — taken from actors and actions through data, state, interfaces, a walking skeleton, the cart decision, checkout in pseudocode, payment research, failure cases, and scale only on evidence. The whole loop, on the running example, with nothing added before it was needed.
"Users send messages to each other" → Users, Conversation, Messages, Participants → Send, Store, Retrieve, Realtime, Read State. The word "realtime" is the unknown; the decomposition puts it fourth, and the first version works without it.
Long URL → Short Code → Redirect. The first version is one table and two endpoints; the interesting engineering arrives only when the requirements do — uniqueness under concurrency, read-heavy traffic, click logging — and the ladder for "we need a distributed ID generator" ends somewhere much smaller.
Upload, Storage, Metadata, Permissions, Download. The first version streams through the server; the requirement that changes the shape is size, and the prototype question — can the browser upload a large file directly to object storage? — is answered in an afternoon before any architecture depends on it.
"Search products by name" is a SQL query until a requirement says otherwise. The case runs the why ladder for Elasticsearch honestly: the first version is a filtered query, the second is a database full-text index, and a dedicated search engine is justified by requirements the store may or may not ever have.
Metric, Source, Aggregation, Freshness, Visualization. "We need a dashboard" is a request for a chart; the requirement is a metric someone will act on, with a definition, a source and a freshness that decide whether the first version is a query against production, a nightly summary table, or a pipeline into a warehouse.
"Answer questions about company documentation" → Documents, Retrieval, Context, Model, Answer, Evaluation. The first version is retrieval plus one prompt, measured against a question set; the why ladder for "we need a multi-agent system" ends at a retrieval fix most of the time — and says when it would not.