Starphix
Software Engineering / Sources
The Lab / Sources / Software Engineering / Architecture/Design-Principles.md

Software Design Principles

Fundamental principles for arranging code so it can be understood, changed, and extended over time. These are compressed experience — tools with a domain of applicability, not laws.

Core Properties: Coupling & Cohesion

Manage coupling and cohesion first — most named principles are indirect ways of improving these two properties.

  • Coupling: Degree of interdependence between modules. Low coupling = changes don't ripple.
  • Cohesion: Degree to which elements within a module belong together. High cohesion = module does one thing well.

When reviewing a design, ask: Which changes ripple across module boundaries? Those ripples are the true measure of coupling.


The Major Acronyms

SOLID — Use as a Lens, Not a Checklist

Principle What It Means When to Apply
Single Responsibility A module has one reason to change (one kind of change) Keep modules cohesive; apply to classes, functions, services
Open/Closed Open to extension, closed to modification Where extension points are real (not speculative)
Liskov Substitution Subtypes must be substitutable without breaking contracts The only verifiable one; violations are real bugs
Interface Segregation Many specific interfaces > one general interface When clients genuinely need different subsets
Dependency Inversion Depend on abstractions, not concretions At genuine architectural boundaries

Anti-pattern: Manufacturing interfaces, factories, and layers just to satisfy the acronym when there's only one implementation and no second one in sight. Indirection has a cost — you pay it on every read.


DRY — Apply to Knowledge, Not Text

Don't Repeat Yourself: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

Key distinction: DRY is about knowledge, not code that looks alike. - Two pieces of code that look similar but change for different reasons should stay separate - Prefer a little duplication over a premature shared abstraction that couples unrelated things - Rule of Three: Extract the abstraction once the real pattern has shown up 2-3 times

WET (Write Everything Twice) = DRY violation. But forced DRY = wrong abstraction.


KISS — Keep It Simple, Stupid

Among solutions that do the job, pick the simplest. Complexity is a cost you pay forever.

Over-engineering signals: - Using microservices, Hexagonal Architecture, or Clean Architecture for simple CRUD - Excessive design pattern application - Configurable frameworks, plugin systems, extension points nobody asked for

Four Rules of Simple Design (priority order): 1. Passes the tests 2. Reveals intent 3. No duplication 4. Fewest elements


YAGNI — You Aren't Gonna Need It

Implement a feature only when you have strong evidence it will be used.

Not: "Never build flexibility" — some flexibility (stable interfaces, clean seams) is cheaper to build in early.

Balance: YAGNI argues against speculative implementation, not against thoughtful boundaries.


Additional Principles

CQS — Command Query Separation (Bertrand Meyer)

Methods should be either: - Commands: Return void, have side effects (modify state) - Queries: Return values, no side effects

Asking a question should not change the answer. Queries are inherently safer — easier to understand, call, and test.


Law of Demeter — Talk Only to Your Immediate Friends

Don't reach through a.b.c.d chains. Reduces coupling by limiting knowledge of internal structure.


Composition Over Inheritance

Assemble behavior by composition by default; reserve inheritance for genuine "is-a" relationships. Inheritance breaks encapsulation of the parent.


Tell, Don't Ask

Give the behavior to the owner of the data instead of pulling data out to manipulate elsewhere. Encapsulates behavior with its data.


Design by Contract

State preconditions, postconditions, and invariants explicitly. Say what "correct" means.


Fail Fast

Surface an error as early and as close to its cause as possible. Don't let a bad state spread far before it's noticed.


Validate at the Boundary, Trust the Inside

Clean untrusted data once at the entrance so core logic doesn't guard everywhere.


Make Illegal States Unrepresentable

Use types to rule out impossible states at compile time instead of checking at runtime.


Least Privilege

Each module gets only the minimum access and information it needs to do its job.


Single Level of Abstraction

Within one function body, don't mix high-level intent with low-level detail. It should read at one altitude.


Encapsulation / Information Hiding

Expose what, hide how. The implementation may change; the contract may not.


Principle of Least Astonishment

An interface should behave the way its user expects. Surprise is where defects breed.


Orthogonality

Changing one thing doesn't ripple into an unrelated thing. Modules don't reach into each other's internals.


Reversibility

Don't weld decisions in place. Keep critical choices (database, framework, vendor) easy to replace.


Boy Scout Rule

Leave code a little cleaner than you found it. Small steps — small changes, frequent verification.


Good Enough

Stop at "good enough" and spend saved energy where it matters more. Don't polish endlessly.


Principle Conflicts — Where Judgment Lives

Conflict How to Settle
DRY vs Decoupling Removing duplication sometimes creates coupling. Test: will they change for the same reason? If yes, merge; if they just resemble each other today, leave them apart.
Abstraction vs YAGNI Premature abstraction is speculative complexity. Wait for the third repetition (Rule of Three) before extracting; tolerate the first two.
Flexibility vs Simplicity Open an extension point only along the axis where change will actually come; keep other dimensions closed and simple. Extensible everywhere = over-engineering.
Performance vs Readability Be correct and readable first; once a profiler finds the bottleneck, trade readability for speed locally and with a comment.
Consistency vs Improvement In a codebase with poor conventions, "consistent" and "better" collide. Improve in place at small scope; at large scope, unify the convention first — don't add a third style.

Tie-breaker: Correct > Clear > Simple > everything else.


Key Takeaways

  1. Principles are tools, not laws — each has a failure mode
  2. Coupling and cohesion are the deeper properties — acronyms serve them
  3. Optimize for the changes you'll actually make — not imaginary ones
  4. Duplication is cheaper than the wrong abstraction — wait for the pattern to clear
  5. Make dependencies explicit and point them toward stable things
  6. Model the domain in the language of the domain — align software boundaries with business boundaries
  7. Choose paradigms to fit the problem, not ideology — most large systems are pragmatically mixed

  • Architectural Styles — How principles map to architectural styles
  • Design Patterns — Patterns as vocabulary for recurring solutions
  • Extreme Programming — XP practices that embody these principles
  • Test Driven Development — TDD as a design practice

Sources: Software Engineering Guide (software-engineering-guide.github.io), Steven's Knowledge (stevenzg.com), Software Engineering: A Modern Approach (softengbook.org), Tenhaw Engineering Handbook, O'Reilly Clean Architecture

From the Software Engineering research vault · CC BY 4.0 · all notes · raw markdown