1.0 What is Software Design?
Software design is the process of deciding what a software system should do, how it
should be structured, and how its parts should communicate - before writing the bulk
of the code. A good design makes the intent of the system visible, keeps components
loosely coupled, and produces documentation that guides both implementation and future
maintenance.
Design need not be a large bureaucratic exercise. Its scope should match the project.
For a small project like TextFinder, a day or two of thinking and a few pages of notes
is enough. For a system with dozens of developers, a more formal process is warranted.
In both cases the core questions are the same:
- Who will use this, and what do they need to accomplish?
- What components are needed, and what are their responsibilities?
- How do the components communicate and share data?
- What constraints (performance, security, cost) must the design satisfy?
1.1 Concept Development
Concept development is the earliest design activity. Its goal is to answer the question:
what problem are we solving?
Good concept development involves:
-
User analysis - who are the users, what are their goals, and
what activities do they perform?
-
Use cases - brief narratives describing how users interact with
the system to accomplish specific goals.
-
Constraint identification - performance targets, platform
requirements, budget and schedule limits, security and compliance obligations.
-
Scope definition - what the system will and will not do,
stated clearly enough that the team can build to it.
For TextFinder, concept development is brief: the user is a developer who needs to
find text matching a regex in a directory tree. The scope is command-line input,
recursive directory traversal, and matched-line output. The constraints are: fast
enough for interactive use, portable across Rust/C++/C#/Python.
1.2 Architecture
Architecture describes the high-level structure: what packages (modules, libraries,
assemblies) exist, what each is responsible for, and how they depend on one another.
A clean architecture has a few key properties:
-
Single responsibility - each component does one thing well.
DirNav traverses directories; TextSearch matches text; Executive coordinates them.
-
Directed dependencies - dependencies flow in one direction,
typically from higher-level coordinators down to lower-level utilities. Cycles
between packages make code harder to test and reason about.
-
Defined interfaces - components communicate through explicit
interfaces or type signatures, not through shared global state.
-
Testability - components with clear boundaries and injected
dependencies can be unit-tested without spinning up the whole system.
TextFinder's architecture consists of four packages:
TextFinder Package Architecture
| Package |
Responsibility |
| Executive |
Parses command-line arguments and coordinates the other packages. |
| DirNav |
Recursively traverses a directory tree and invokes a callback for each matching file. |
| TextSearch |
Opens a file, applies the regex, and returns matched lines. |
| Display |
Formats and writes matched results to standard output. |
1.3 Specification
A specification states precisely what each component must do - its inputs, outputs,
preconditions, postconditions, and error behavior. Specifications are the contract
between the designer and the implementer.
A useful specification for a function or method describes:
- Signature - parameter types, return type
- Preconditions - what must be true before the call
- Postconditions - what is guaranteed after a successful call
- Error conditions - what happens when inputs are invalid or
operations fail (panic, return an error type, throw an exception)
Example - DirNav specification (language-independent):
DirNav::new(root: path, extensions: list<string>) -> DirNav
pre: root exists and is a directory; extensions is non-empty
post: DirNav ready to traverse; no filesystem access yet
DirNav::search(visitor: fn(path)) -> Result
pre: DirNav constructed; visitor accepts a file path
post: visitor called once for each file matching extensions
err: returns Err if root is not accessible
Specifications need not be this formal. What matters is enough precision that an
implementer does not have to guess, and a reviewer can tell whether the implementation
is correct.
1.4 Design Documentation
Documentation serves two audiences: the team building the software now, and
maintainers working on it later. Useful design documents include:
-
Architecture diagrams - package dependency graphs,
data flow diagrams, sequence diagrams for key interactions.
-
Interface tables - one row per function, listing its
signature, preconditions, and postconditions.
-
Design rationale - why a key decision was made, and what
alternatives were considered. This is the most valuable documentation
because it cannot be recovered from the code alone.
-
Inline comments - for non-obvious constraints, subtle
invariants, or workarounds for specific bugs. Code should not need comments
to explain what it does, only why it does something surprising.
For small projects, a single README or wiki page covering architecture and key
decisions is usually sufficient. Keep documentation close to the code - in the
repository itself - so it stays in sync with changes.
1.5 References
Software Design References