Skip to content

Instantly share code, notes, and snippets.

@gabsn
Created September 12, 2025 10:46
Show Gist options
  • Select an option

  • Save gabsn/77b693fdf2020d893e16692c7bca31d9 to your computer and use it in GitHub Desktop.

Select an option

Save gabsn/77b693fdf2020d893e16692c7bca31d9 to your computer and use it in GitHub Desktop.

Rob Pike's Software Engineering Philosophy

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.

Principles

1. Simplicity is the Ultimate Sophistication

  • 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.

2. Do One Thing Well

  • 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.

3. Data Dominates Algorithms

  • 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.

4. Small Interfaces Are Powerful Interfaces

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();
}

5. Composition Over Inheritance

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));
  }
}

6. Accept Interfaces, Return Structs

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);

7. Minimize Public API

  • 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.

8. Don't Communicate by Sharing Memory

  • 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.

Anti-Patterns to Avoid

Premature Abstraction

  • Don't abstract until you have 3+ similar cases.
  • Duplication is cheaper than wrong abstraction.
  • Wait for patterns to emerge naturally.

Framework-itis

  • 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.

Mock Theater

  • 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.

Indirection Without Abstraction

  • Don't add layers just because.
  • Every indirection should solve a real problem.
  • Interface segregation. Don't make interfaces to justify a pattern.

Documentation Philosophy

Explain WHY, Not WHAT

  • Code shows what, comments show why.
  • Document decisions and tradeoffs.
  • Examples over explanations. Show how to use it, don't just describe it.

Names Should Be Self-Documenting

  • Use full words, not abbreviations.
  • Context provides scope. Shorter names in smaller scopes.
  • Consistency over cleverness. Use the same name for the same concept.

Testing Principles

Test Behavior, Not Implementation

  • 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.

Fast, Reliable, Debuggable

  • 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.

When to Break the Rules

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.

When to depart from Rob Pike’s original views

  • Errors: It's ok to throw exception in Dart. Let's not overcomplexify with Result types or tuples.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment