Every project that goes wrong does so in the same way: someone opens an editor and starts typing before they know what the pieces are. Hours later the code is a tangle, the tests are impossible to write, and a simple feature turns out to need a rewrite from the ground up.
A specification is a written plan for what the program must do, and a milestone is a slice of that plan that can be built and tested on its own. Professional developers write the spec first because it is far cheaper to change a sentence than to change a hundred lines of code. The spec does not need to be long; it needs to be clear enough that someone else could read it and know what done looks like.
In this lesson you will learn to break a problem down, pick data structures that match the shape of the data, and write function signatures that act as a contract between the plan and the code.
flowchart LR P["Problem"] --> D["Decompose"] D --> M["Milestones"] M --> S["Data structures"] S --> F["Function signatures"] F --> B["Build + test"] style D fill:#e0900b,color:#fff style S fill:#e0900b,color:#fff style F fill:#e0900b,color:#fff
Milestones: the art of the small slice
A good milestone has three properties. It is testable — you can prove it works without waiting for the rest of the project. It is independent — it does not need tomorrow's code to run today. And it is visible — a human can look at the result and say yes, that part is done.
Bad milestone: Build the app. That is the whole project, not a slice. You cannot know when it is finished because the edges are fuzzy and the definition keeps shifting.
Good milestone: Parse a CSV string into an array of objects. It has a clear input, a clear output, and you can write a test for it right now. You will know it is done when the test passes.
Start every project by listing the milestones in dependency order. If milestone B needs the output of milestone A, A must come first. This order becomes your roadmap and stops you from building the roof before the walls.
const plan = {
project: "Expense Tracker",
milestones: [
{ name: "Store a transaction", done: false },
{ name: "Sum by category", done: false },
{ name: "Build summary report", done: false }
]
};
function nextUp(plan) {
return plan.milestones.find(m => !m.done)?.name ?? "All done!";
}
console.log(nextUp(plan));
plan.milestones[0].done = true;
console.log(nextUp(plan));
Pick the data structure before the algorithm
The second planning decision is how to hold your information. Two structures cover most beginner projects: the array and the object. The choice is not style; it is determined by how you will look the data up later.
Use an array when order matters and you will mostly loop through everything. Use an object when you need to look something up by name and order does not matter.
If you are building a tally, an object is natural because the word is the key. If you are building a log, an array is natural because the entries arrive in sequence. Choosing wrong does not break the program, but it makes every later function harder to write. A log stored as an object loses order; a tally stored as an array forces you to scan the entire list every time you want one count.
flowchart TD
Q{Lookup by name?} -- Yes --> O[Object]
Q -- No --> A2{Order matters?}
A2 -- Yes --> A[Array]
A2 -- No --> O2[Object or Array]
style O fill:#e0900b,color:#fff
style A fill:#e0900b,color:#fff
Function signatures are contracts
Before you write the body, write the signature: the name, the parameters, and what it returns. A signature is a promise. function totalForCategory(expenses, category) promises that if you hand it an array of expenses and a category string, it will hand back a number. That promise is what makes the function reusable across different parts of your program.
Writing signatures first forces you to decide what each milestone actually produces. If you cannot describe the return value in one sentence, the milestone is too big and should be split. Once the signatures are written, the bodies become almost fill-in-the-blank — and the signatures themselves become the outline of your test file. A file full of empty functions with clear names is not failure; it is a plan that compiles.
function splitWords(text) { /* returns array of strings */ }
function countWords(words) { /* returns object { word: count } */ }
function topWord(counts) { /* returns string */ }
function pipeline(text) {
return topWord(countWords(splitWords(text)));
}
console.log(typeof pipeline);
console.log(pipeline("any text"));
flowchart LR T["Too little plan"] --> C["Chaos"] P["Too much plan"] --> N["Never start"] G["Just enough"] --> W["Working code<br/>informs next step"] style G fill:#1e7f3d,color:#fff style C fill:#b91c1c,color:#fff style N fill:#b91c1c,color:#fff
What does this print? Track how find searches the milestones array.
const roadmap = {
milestones: [
{ name: "A", complete: true },
{ name: "B", complete: false },
{ name: "C", complete: false }
]
};
function currentTask(r) {
return r.milestones.find(m => !m.complete)?.name;
}
console.log(currentTask(roadmap));
find returns the first item where the test returns true.
A is complete, so the search moves to B.
Which quality makes a milestone testable?
A testable milestone has a visible input, a visible output, and no hidden dependencies. You can write an assertion for it immediately.
Write allComplete(milestones) that returns true only if every object in the array has complete: true. An empty array should also return true.
function allComplete(milestones) {
// return true if every milestone is complete
}
Array.prototype.every returns true for an empty array.
Check m.complete === true for each milestone.
function allComplete(milestones) {
return milestones.every(m => m.complete === true);
}
This function mixes calculation and display, so nothing it computes can be tested. Fix it to return the status string instead of logging it.
function statusLine(items) {
const done = items.filter(i => i.done).length;
console.log(`${done} of ${items.length} done`);
}
Replace console.log with return.
The template literal itself is the value to return.
function statusLine(items) {
const done = items.filter(i => i.done).length;
return `${done} of ${items.length} done`;
}
Write designMilestones(names) that takes an array of strings and returns an array of milestone objects. Each object must have name set to the string and complete set to false.
function designMilestones(names) {
// return [{ name, complete: false }, ...]
}
Use names.map to transform each string into an object.
Return { name, complete: false } from the mapper.
function designMilestones(names) {
return names.map(name => ({ name, complete: false }));
}
Recap
- A specification breaks a project into milestones that are testable, independent, and visible.
- Pick an array when order and looping matter; pick an object when you look up by name.
- Write function signatures before bodies. They act as contracts and become your test outline.
- Plan enough to start, then let working code refine the plan.
In the next lesson you will build your first capstone tool: a word-frequency tally that applies these planning steps to real code.