Principles
These are the beliefs that guide how I build software. They're not rules—they're lenses for making better decisions under uncertainty.
- 01
Clarity over cleverness
Use intention-revealing names, extract complex logic into small well-named functions, and flatten nested conditionals with guard clauses. Keep each function at a single level of abstraction so readers never have to jump between high-level flow and low-level detail. When inheriting unclear code, wrap and replace incrementally rather than rewriting blind.
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
- 02
Purpose driven
Apply YAGNI ruthlessly and never write code for hypothetical future requirements. Model code around the business domain so the structure itself communicates intent. Trace features back to user outcomes, deliver thin end-to-end slices of functionality, and keep each function doing one thing: either commanding or answering, never both.
“Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.”
- 03
Intentional, not incidental
Handle errors explicitly and never swallow them silently. Use the type system to encode constraints so accidental misuse becomes a compile-time error rather than a runtime surprise. Document the reasoning behind decisions with ADRs, enforce consistency through automated linting, and fail fast at the boundary where problems originate.
“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies.”
- 04
Simple enough, flexible enough
Encapsulate varying behavior behind stable interfaces. Favor composition over inheritance, and resist abstracting until you've seen the same pattern at least three times. Isolate core logic from external dependencies using ports and adapters so the core stays clean while adapters handle the messy real world.
“Make the change easy, then make the easy change.”
- 05
Replaceability as a feature
Give each module a single reason to change so it can be swapped without collateral damage. Depend on abstractions, not concretions. Wrap complex or legacy subsystems behind clean interfaces, draw clear boundaries between subsystems, and never let a vendor's API bleed across your codebase. Wrap it so that switching vendors means changing one file.
“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application.”
- 06
Testable by design
Write pure functions wherever possible: same input, same output, no side effects. Pass collaborators in through dependency injection so tests can substitute lightweight alternatives. Push decision-making into pure logic and keep I/O at the edges. Favor integration tests over heavily mocked unit tests that break on refactors but catch no real bugs.
“The more your tests resemble the way your software is used, the more confidence they can give you.”
- 07
Consistency with context
Automate stylistic consistency with shared linting configs and formatters. Standardize commit messages with conventional commits, document agreed-upon patterns in internal playbooks, and use code review as a tool for cultural alignment. When there's a choice, pick the approach that would surprise a reader the least given the conventions already in place.
“When in Rome, code as the Romans code.”
- 08
Built to last
Write the README before the code. If you can't explain the why and how to a newcomer, you're not ready to build. Use inline comments for why, not what. Maintain changelogs and ADRs so future maintainers can reconstruct the decision history, and communicate the impact of changes through semantic versioning.
“Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.”