This document captures the core principles and beliefs that guide how Rob Pike approaches software design and implementation. These rules form the foundation for all code decisions in this project.
- Simple is not easy. Simple means having fewer parts, fewer concepts, fewer moving pieces.
- Clear is better than clever. Code should be obvious to read and understand.
- Complexity is the enemy. Every line of code is a liability, not an asset.
- Single responsibility principle. Each function, module, and interface should have one clear purpose.
- Composability over monoliths. Small, focused pieces that work together.
- Orthogonality. Changes in one area shouldn't require changes elsewhere.
- Show me your data structures, and I won't usually have to see your code.
- Design the data first. Get the data structures right, and the algorithms will follow.
- Reflect the problem domain. Data structures should mirror the real-world concepts they represent.
The most powerful design pattern is one method per interface. This creates maximum composability and minimum coupling.
// Good: Single-method interfaces
abstract class Reader {
Future<int> read(List<int> buffer);
}
abstract class Writer {
Future<int> write(List<int> data);
}
abstract class Closer {
Future<void> close();
}Build complex behaviors by combining simple interfaces, not by extending base classes.
// Composed interfaces
abstract class ReadWriter implements Reader, Writer {}
abstract class ReadWriteCloser implements Reader, Writer, Closer {}
// Usage: Accept the minimal interface you need
Future<void> copyData(Reader source, Writer destination) async {
final buffer = List<int>.filled(1024, 0);
while (true) {
final bytesRead = await source.read(buffer);
if (bytesRead == 0) break;
await destination.write(buffer.sublist(0, bytesRead));
}
}Functions should accept the most abstract interface possible but return concrete types.
// Good: Accept interface, return concrete type
FileWriter openFile(String path) => FileWriter(path);
Future<void> processData(Reader input) async { /* ... */ }
// Bad: Return interface (limits future optimization)
Reader openFile(String path) => FileWriter(path);- Modules shouldn't expose their internals. Hide implementation details.
- Minimal public surface. Export only what consumers truly need.
- Information hiding. Internal state should be truly internal.
- Share memory by communicating. Use message passing over shared state.
- Immutable data structures. Prefer immutable data when possible.
- Explicit state management. Make state changes visible and intentional.
- Don't abstract until you have 3+ similar cases.
- Duplication is cheaper than wrong abstraction.
- Wait for patterns to emerge naturally.
- Don't build frameworks, build tools.
- Libraries over frameworks. Libraries are called by your code; frameworks call your code.
- Configuration is code smell. Prefer convention and simplicity.
- Use real objects when possible. Mocks test the mock, not the code.
- Tiny fakes over complex mocks. Simple test doubles that implement the real interface.
- Integration tests over unit tests. Test the whole system working together.
- Don't add layers just because.
- Every indirection should solve a real problem.
- Interface segregation. Don't make interfaces to justify a pattern.
- Code shows what, comments show why.
- Document decisions and tradeoffs.
- Examples over explanations. Show how to use it, don't just describe it.
- Use full words, not abbreviations.
- Context provides scope. Shorter names in smaller scopes.
- Consistency over cleverness. Use the same name for the same concept.
- Black box testing. Test the public interface, not internal details.
- Happy path and edge cases. Cover normal flow and boundary conditions.
- Real data over synthetic. Use realistic test data when possible.
- Tests should run quickly. Slow tests don't get run.
- No flaky tests. Fix or delete unreliable tests.
- Clear failure messages. Make test failures self-explanatory.
Rob Pike believes in pragmatism:
- Rules are guidelines, not laws. Sometimes context demands flexibility.
- Performance might require complexity. But measure first.
- Compatibility can force compromise. But isolate the ugly parts.
- Deadlines are real. But pay down technical debt quickly.
The meta-rule: When you break a rule, document why. Make the exception explicit and temporary.
- Errors: It's ok to throw exception in Dart. Let's not overcomplexify with Result types or tuples.