| name | Designing Before Coding |
| description | Design in pseudocode first, iterate approaches, then translate to code |
| when_to_use | Before implementing any non-trivial routine or class. When jumping straight to code. When code feels hack-y or you're unsure how to proceed. When you catch yourself in "just one more compile" cycle. When tempted to code first and figure it out later. |
| version | 1.0.0 |
| languages | all |
Designing Before Coding
Overview
Write the design in pseudocode BEFORE writing implementation code. Iterate through multiple approaches in pseudocode, pick the best, THEN translate to code.
Core principle: Once you start coding, you get emotionally involved with your code and it becomes harder to throw away a bad design. Design is cheap to change; code is expensive.
Violating the letter of the rules is violating the spirit of the rules.
When to Use
Use for ANY programming task beyond trivial one-liners:
- New features or functionality
- Non-trivial bug fixes
- Refactoring
- Any routine longer than ~5 lines
- When you're unsure how to proceed
- When solution feels complex
Red flags that you need this:
- Jumping straight to implementation code
- "Just one more compile" syndrome
- Coding yourself into a corner
- Losing train of thought mid-implementation
- Forgetting to write parts of a routine
- Staring at screen not knowing where to start
- Code feels hack-y or patched together
Don't skip when:
- Under time pressure (designing first is FASTER than debug cycles)
- Problem seems simple (simple problems benefit from design too)
- You're confident you know the solution (overconfidence causes mistakes)
The Process
Step 1: Check Prerequisites
Before any design work:
- Is the job well-defined?
- Does it fit cleanly into overall design?
- Is it actually required by the project?
- What information will this routine hide?
- What are the inputs and outputs?
- What are preconditions (guaranteed true before routine called)?
- What are postconditions (guaranteed true after routine returns)?
If unclear, STOP. Get clarification before proceeding.
Step 2: Name the Routine
Name it BEFORE designing internals.
If you struggle to create a good name = WARNING SIGN.
- Vague name = vague design
- Wishy-washy name = wishy-washy purpose
- Can't name it clearly? Back up and clarify what it should do.
Good name = clear, unambiguous, describes what routine does.
Step 3: Think Through Error Handling
Before writing pseudocode:
- What could go wrong? (bad input, invalid return values, etc.)
- How will this routine handle errors?
- Does architecture define error strategy? Follow it.
- Which error approach: return neutral value, substitute valid data, log warning, return error code, throw exception, shut down?
Step 4: Research Libraries and Algorithms
Don't reinvent the wheel:
- Check standard libraries
- Check company/project libraries
- Check algorithm books if complex
- Reuse good code > writing from scratch
Step 5: Write High-Level Pseudocode
Use your code editor (it will become comments):
# Write general statement of purpose first
This routine outputs an error message based on an error code
supplied by the calling routine. The way it outputs the message
depends on the current processing state, which it retrieves
on its own. It returns a value indicating success or failure.
# Then write high-level pseudocode
set the default status to "fail"
look up the message based on the error code
if the error code is valid
if doing interactive processing, display the error message
interactively and declare success
if doing command line processing, log the error message to the
command line and declare success
if the error code isn't valid, notify the user that an internal
error has been detected
return status information
Pseudocode characteristics:
- Uses plain English, not programming language syntax
- Describes WHAT to do, not HOW to do it (intent, not implementation)
- High-level enough that you could implement in any language
- Low-level enough that translating to code is mechanical
Step 6: Review the Pseudocode
Critical step - don't skip:
- Read it mentally - does it make sense?
- Explain it to someone else (or imagine explaining it)
- Does it handle all cases?
- Is the logic clear?
- Are there edge cases missed?
If hard to explain or unclear, iterate the pseudocode. Don't proceed to code.
Step 7: Try Multiple Approaches (Iterate)
Don't settle for first design.
Try 2-3 different approaches in pseudocode:
- Different algorithms
- Different data structures
- Different control flow
- Different error handling strategies
Compare them:
- Which is simpler?
- Which is more maintainable?
- Which handles edge cases better?
- Which is easier to test?
Pick the best BEFORE coding.
Step 8: Translate Pseudocode to Code
Only after pseudocode is solid:
- Write routine declaration
- Turn header comment into language comment
- Turn each pseudocode line into a code comment
- Fill in code below each comment
- Keep comments (they explain intent at higher level)
def report_error_message(error_code):
"""
This routine outputs an error message based on an error code
supplied by the calling routine. The way it outputs the message
depends on the current processing state, which it retrieves
on its own. It returns a value indicating success or failure.
"""
# set the default status to "fail"
status = Status.FAILURE
# look up the message based on the error code
error_message = lookup_error_message(error_code)
# if the error code is valid
if error_message.is_valid():
# determine the processing method
processing_method = current_processing_method()
# if doing interactive processing, display the error message
# interactively and declare success
if processing_method == ProcessingMethod.INTERACTIVE:
display_interactive_message(error_message.text)
status = Status.SUCCESS
# if doing command line processing, log the error message
# to the command line and declare success
elif processing_method == ProcessingMethod.COMMAND_LINE:
log_to_command_line(error_message.text)
status = Status.SUCCESS
else:
# if the error code isn't valid, notify the user that an
# internal error has been detected
display_interactive_message("Internal Error: Invalid error code")
# return status information
return status
Notes:
- Each comment expands to 1-10 lines of code
- Comments kept to show intent
- Code mechanical translation from pseudocode
- If one comment explodes to many lines → extract to new routine
Step 9: Check the Code
- Mentally execute each path
- Check for off-by-one errors, initialization problems
- Verify error handling works
- Step through in debugger
- Test with planned test cases
If routine is unusually buggy: start over. Don't hack around it.
Step 10: Clean Up
- Remove redundant comments (if comment just restates obvious code)
- Check variables are well-named
- Verify routine does one thing well
- Check for proper initialization
- Verify all parameters used
Quick Reference
| Stage | Action | Warning Sign |
|---|---|---|
| Prerequisites | Define inputs, outputs, pre/postconditions | Can't explain what routine does |
| Naming | Clear, unambiguous name | Struggling to name = design unclear |
| Errors | Think through what could go wrong | Forgetting error cases |
| Research | Check libraries and algorithms | Reinventing the wheel |
| Pseudocode | Write high-level design in English | Jumping to code syntax |
| Review | Explain to someone else | Can't explain clearly |
| Iterate | Try 2-3 approaches | Settling for first idea |
| Translate | Mechanical conversion to code | Fighting the language |
| Check | Mental execution, testing | "Just one more compile" |
Common Mistakes
❌ Skipping pseudocode: "It's simple, I'll just code it" ✅ Fix: Even simple routines benefit. Takes 30 seconds, saves debugging time.
❌ Pseudocode too low-level: Writing programming syntax in comments ✅ Fix: Use plain English describing WHAT, not HOW. Should work in any language.
❌ Settling for first design: Implementing first idea that comes to mind ✅ Fix: Try 2-3 approaches, compare, pick best.
❌ Compiling too early: "Let me compile and see if it works" ✅ Fix: Don't compile until you're convinced it's right. Avoid hack-compile-fix cycle.
❌ Coding before understanding: "I'll figure it out as I code" ✅ Fix: If you don't understand at pseudocode level, you won't understand at code level.
❌ Vague name accepted: "I'll think of a better name later" ✅ Fix: Vague name = vague design. Fix the design NOW.
If You've Already Written Code
If you wrote implementation code without pseudocode first, you've violated the process.
No exceptions:
- Don't keep it as "reference"
- Don't claim "it works so it's fine"
- Don't skip pseudocode now "to save time"
- Don't "adapt" existing code while writing pseudocode
- Don't look at your implementation while designing
Required steps:
- Set code aside - Don't delete yet, but don't look at it
- Write pseudocode from scratch - Describe what routine SHOULD do (not what it does)
- Try 2-3 alternative approaches - Design without bias from your implementation
- Pick the best approach - Based on simplicity, maintainability, clarity
- Compare with your implementation:
- If your code matches the best design: keep it, add pseudocode as comments
- If a different design is better: delete your code and re-implement from pseudocode
The hours already spent are sunk cost. Don't throw good time after bad by keeping suboptimal design.
"But it works!" doesn't mean it's the best design. Working code without good design is technical debt.
Production Emergencies: Pseudocode is Still Faster
Even with production down and $15k/minute at stake:
Time comparison:
- Direct coding: 3 min code + 10 min debugging edge cases = 13 minutes
- Pseudocode first: 2 min pseudocode + 3 min code = 5 minutes
The pressure makes pseudocode FEEL wasteful. It's not. It's faster.
For true 5-line fixes:
- Pseudocode: 30 seconds (literally write 5 bullet points)
- Review: 15 seconds (read the bullets)
- Code: 2 minutes (mechanical translation)
- Total: 3 minutes vs jumping to code and missing an edge case
Emergency doesn't override the principle. It reinforces it.
Debug cycles under pressure are even more expensive (stress, mistakes, production impact).
Common Rationalizations
| Excuse | Reality |
|---|---|
| "It works already" | Working ≠ best design. Pseudocode reveals better alternatives. |
| "Keep code as reference" | You'll bias toward it. Set it aside completely. |
| "Too simple to design" | Simple code still benefits. Takes 30 seconds. |
| "Emergency, no time" | Pseudocode is faster than debug cycles. Always worth 2 minutes. |
| "I designed in my head" | Undocumented mental design = didn't happen. Must write it. |
| "Ship it, document after" | Pseudocode-after doesn't catch design problems. Too late. |
| "Production down, skip process" | Broken process = how production got broken. Don't compound it. |
| "Just this once" | "Just this once" becomes "always this once." No exceptions. |
Verification Checklist
Before marking work complete:
- Wrote pseudocode BEFORE any implementation code
- Tried at least 2 alternative approaches in pseudocode
- Compared approaches and consciously picked best
- Pseudocode turned into comments in final code
- Can explain routine clearly using the pseudocode
- If coded first: set aside, designed from scratch, compared approaches
Can't check all boxes? Return to Step 5 (Write pseudocode).
Red Flags - STOP and Design First
Before coding:
- Jumping straight to implementation code
- "I'll figure it out as I code"
- "Let me try this and see if it compiles"
- "Just one more compile"
- Can't explain what routine does
- Struggling to name something
- Code feels hack-y
- Coded yourself into a corner
- Lost train of thought mid-coding
- Forgot to implement part of routine
After coding (without pseudocode):
- "It works so it's fine"
- "Keep as reference while designing"
- "I designed it mentally"
- "Too late now"
- "Ship it, document after"
- "Emergency, can't afford to redo"
All of these mean: Stop coding. Return to pseudocode.
Benefits
- Fewer bugs - Design mistakes caught before coding
- Faster development - Less debug time than hack-compile-fix
- Better designs - Easier to try alternatives in pseudocode
- More maintainable - Comments explain intent, not just mechanics
- Less emotional attachment - Easier to throw away bad pseudocode than bad code
- Self-documenting - Pseudocode becomes comments explaining why
Integration with TDD
PPP and TDD work together. Both require design before implementation.
Combined process:
- Design test in pseudocode - What behavior should this verify?
- Translate test pseudocode to test code - Keep pseudocode as test comments
- Run test - Watch it FAIL (RED)
- Design implementation in pseudocode - Try 2-3 approaches
- Pick best approach - Based on simplicity, clarity
- Translate implementation pseudocode to code - Keep as comments
- Run test - Watch it PASS (GREEN)
- Refactor - Update pseudocode comments if logic changes
Order is critical:
- Test pseudocode → test code → FAIL
- Implementation pseudocode → implementation code → PASS
Both skills mandate: NO implementation code without design first.
TDD says write the test first. PPP says design the test in pseudocode before writing test code. They reinforce each other.
Real-World Impact
From Code Complete research:
- Students using pseudocode scored 30% higher on comprehension
- Names averaging 10-16 characters minimized debugging effort
- 95% of errors are programmer errors (not compiler/hardware)
- Complexity overload = applying irrelevant methods mechanically
Key quote: "The picture of the software designer deriving his design in a rational, error-free way from a statement of requirements is quite unrealistic. No system has ever been developed that way." - David Parnas
Design is sloppy and iterative. Pseudocode keeps that sloppiness cheap.