Design Culture
@ Kappal
Ship clean, or don't ship at all.
The Foundations
One Method, One Job
A function should do exactly one thing and do it well. If you need 'and' to describe what it does, split it. Small, named functions are the atomic unit that makes every other principle possible.
DRY — Don't Repeat Yourself
Every piece of knowledge lives in exactly one place. When you copy-paste logic, you create two sources of truth — and a future bug waiting to be born in the one you forgot to update.
The Boy Scout Rule
Leave every file cleaner than you found it. Rename a confusing variable, remove a dead block, fix an off-by-one comment — even when the ticket doesn't ask for it. Quality is a daily habit, not a sprint.
Names Are Documentation
A well-chosen name eliminates the need for a comment. `calculateMonthlyRevenue()` needs no explanation; `doStuff()` demands one. Invest the extra 30 seconds to name things honestly.
Small Commits, Clear Intent
A commit should capture one logical change — small enough to review in five minutes, clear enough that its message reads like a sentence. 'Fix null check in user login' beats 'various fixes' every time.
Fail Fast, Fail Loudly
Validate inputs and assert invariants at the entry point. A system that crashes with a clear error message at the boundary is infinitely easier to debug than one that silently propagates bad data five layers deep.
Foundations We Follow
SOLID Workflow
Clean Code makes Single Responsibility possible — and without it, the rest of the chain collapses. Each principle only holds if the one before it does. Master the chain, and the Strategy Pattern emerges naturally as its ultimate expression.
Single Responsibility
A class should have only one reason to change.
Open / Closed
Open for extension, closed for modification.
Liskov Substitution
Subtypes must be substitutable for their base types.
Interface Segregation
Clients should not depend on interfaces they don't use.
Dependency Inversion
Depend on abstractions, not concretions.
The Architecture
Clean Architecture
Your business logic must not depend on your database, your framework, or your HTTP layer. Draw a hard boundary: the core domain flows inward; infrastructure adapts outward. Swap Postgres for SQLite — the domain never notices.
Strategy over If-Else
Nested conditionals are a design smell. When logic branches into many cases, replace the branching with polymorphism or a strategy map. Behaviour becomes composable rather than hard-coded.
Composition over Inheritance
Favour assembling small behaviours over building tall class hierarchies. A `Logger` composed into a `Service` is easier to swap, test, and reason about than a `LoggableService` that inherits from `BaseService`.
Respect the Library
No stowaways. Before a dependency enters the codebase, the team understands its capabilities, limitations, and maintenance posture. We own every line we ship — including the ones we didn't write.
Design to the Interface
Depend on abstractions, not concrete implementations. When a module accepts an interface instead of a class, you can substitute any conforming implementation — mock, stub, or real — without touching the consumer.
Modules Over Monoliths
Break the system into cohesive, loosely coupled modules with clear ownership. A module owns its data, exposes a narrow public API, and hides its internals. Other modules knock on the door — they don't walk through the walls.
The Interface
Hierarchy Before Decoration
Visual hierarchy is the first thing a user reads — before a single word. Size, weight, and spacing communicate importance. Every pixel of decoration that doesn't reinforce hierarchy is noise that competes with your message.
Feedback & Affordance
Every interaction needs a visible consequence. A button without a hover state feels broken. A form without inline validation feels hostile. The interface should communicate its state at all times — idle, loading, success, error.
Progressive Disclosure
Show only what the user needs for the decision at hand. Reveal complexity gradually as they advance. A settings panel that exposes every option at once is as unhelpful as a map that shows every road at street zoom.
Accessibility Is the Floor
Semantic HTML, sufficient colour contrast, keyboard navigability, and screen-reader labels are not enhancements — they are the baseline. Inaccessible software is broken software.
Design for the Error State First
The happy path is easy; the error path reveals your real design skill. Empty states, validation errors, network failures, and partial loads must be designed intentionally — not left as afterthoughts.
Consistent Patterns, Predictable Outcomes
Use the same interaction model for the same class of action across the entire product. If 'delete' is always a destructive red action with a confirmation modal, users build a mental model they can rely on everywhere.
The Safety Rig
Test Behaviour, Not Implementation
Your tests should not care how a function achieves its result — only that it produces the right outcome. Testing private state or internal call order wires your test suite to your implementation, making every refactor a test-maintenance chore.
TDD: Red → Green → Refactor
Write the failing test first. Make it pass with the simplest possible code. Then clean up. This loop forces you to design the public interface before the internal wiring, producing naturally testable, minimal APIs.
Unit Tests: Zero External Noise
No database, no network, no filesystem. A unit test must run in milliseconds and a failure must point at a single module. If your test calls the real database, it's an integration test — label it and treat it accordingly.
Integration Tests: Own the Boundary
Test the contracts between layers — the API endpoint, the database query, the message queue consumer. Use a real (or containerised) dependency. These tests catch the gaps unit tests structurally cannot see.
One Assert Per Test
Each test should verify exactly one outcome. When a test asserts five things and fails, you know it failed — but not why. One focused assertion per test transforms a failure into an immediate, unambiguous signal.
The Testing Pyramid
Build confidence from the bottom up: many unit tests (fast, cheap, isolated), fewer integration tests (moderate cost, test contracts), and a thin layer of end-to-end tests (slow, expensive, test journeys). Invert this pyramid and your suite becomes slow and brittle.
The Pipeline
Automate the Deployment Path
Every release is an automated, immutable artifact built from a known commit and validated by the full test suite. Manual deployment steps are undocumented, unrepeatable, and the primary cause of 3 a.m. incidents.
Feature Flags over Long-Lived Branches
Merge to main early and hide unfinished work behind a flag. Long-lived branches accumulate merge conflicts and hide integration problems until the worst possible moment.
Observe Before You Optimise
Add structured logging, traces, and metrics before you optimise. Every performance fix should be preceded by data that identifies the bottleneck. Optimising without measurement is guessing at scale.
Rollback Is a Feature
Every release needs a documented, tested rollback plan before it ships. If you can't undo it in under five minutes, the deployment is incomplete. Blue-green deployments, database migration reversals, and flag kills are not optional.
Environments Must Mirror Production
Staging, QA, and local environments that diverge from production are not test environments — they are false confidence machines. Data shapes, service versions, and infrastructure config should be as close to production as feasible.
Own Your Incidents
When something breaks in production, the first goal is restoration — not blame. Run a blameless post-mortem, identify the systemic cause, and ship a fix that makes the same failure impossible or immediately detectable.