Skip to content

Instantly share code, notes, and snippets.

@carrickkv2
Last active October 30, 2025 16:25
Show Gist options
  • Select an option

  • Save carrickkv2/4cff890eede17420aab69a1db80d7fe2 to your computer and use it in GitHub Desktop.

Select an option

Save carrickkv2/4cff890eede17420aab69a1db80d7fe2 to your computer and use it in GitHub Desktop.

1. Prefer deep modules with simple interfaces

Design modules that encapsulate rich internal logic but expose a small, clear interface.

A few deep, meaningful abstractions are better than many shallow ones.

Simplicity at the boundary makes complexity inside manageable and maintainable.

2. Use the SOLID principles

Follow the principles of Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

They encourage modular design, flexible composition, and separation of concerns across the codebase.

3. Always use a design pattern if applicable

Choose and apply design patterns that suit the problem and the existing system. Patterns provide proven solutions for structuring behaviour and relationships between objects or modules. Avoid forcing a single architectural pattern like MVC onto every project. Large systems or legacy environments often require hybrids or layered patterns that better fit their context. The goal isn’t to follow a pattern for its own sake, but to use patterns to make intent, extensibility, and maintainability explicit.

4. Favour general-purpose abstractions over feature-specific APIs

Build modules that represent the domain itself, not specific use cases.

Avoid letting UI or application details leak into your core logic.

Expose a small set of general, composable operations that higher layers can combine to produce behaviour.

This promotes reuse, clarity, and adaptability.

5. Separate generality and specialisation clearly

Specialised behaviour is inevitable, but it must be kept distinct from general-purpose logic.

Push specialisation upwards into orchestration and feature composition, or downwards into concrete implementations of shared interfaces.

Keep the system’s core general, reusable, and guided by stable contracts.

Higher layers decide how abstractions are used; lower layers decide how they’re realised.

6. Design normal cases to absorb edge cases

Eliminate special-case logic and unnecessary conditionals by designing data models and algorithms that naturally handle edge conditions.

Let objects behave consistently; an empty selection, a null object, or a default strategy should all act as valid participants, not exceptions.

Consistent abstractions reduce branching, lower cognitive load, and make bugs far less likely.

7. Practise Test-Driven Development (TDD)

Use tests to shape design, not just verify it.

Write tests first to clarify intent, define boundaries, and force simplicity in interfaces.

A good test suite makes refactoring safe, encourages modular thinking, and gives you immediate feedback on design quality.

Closing philosophy

Build systems that are deep, deliberate, and dependable.

Every module should express one clear idea, every interface should be simple and honest,

and every piece of logic should belong exactly where it makes the most sense, not higher, not lower.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment