Pragmatic Programming Philosophy
Our philosophy on software development - pragmatism over purity
Pragmatic Programming Philosophy
Source: This page is based on docs/PRACTICE_AND_POLICY/PRAGMATIC_PROGRAMMING_PHILOSOPHY.md
Last Updated: January 1, 2026
"There is no shortcut. Work hard, get smart."
Introduction
This document distills pragmatic wisdom from experienced developers discussing real-world software development. Unlike academic principles or theoretical ideals, these guidelines are forged from actual battle scars and candid discussions about what works in practice.
Core Belief: Software development is about solving real problems, not following rules. Principles are guidelines, not laws. Context matters more than dogma.
Part I: On Simplicity & Complexity
Start Simple, Scale When Needed
The Radical Simplicity Principle: Most complexity in modern software is accidental, not essential. We create complexity with SPAs, microservices, message queues, caching layers, and orchestration tools when a monolith with PostgreSQL would suffice.
Key Guidelines:
-
Start with a monolith and PostgreSQL
- PostgreSQL can handle: data storage, JSON, full-text search, pub/sub, caching, columnar data
- Don't reach for Redis, Elasticsearch, Kafka, or specialized databases until you feel the pain
- 90% of applications will never outgrow a well-tuned PostgreSQL instance
-
Vertical scale before horizontal scale
- Vertical scaling is simple: change a number in your config, deploy
- Horizontal scaling requires distributed systems knowledge, coordination, eventual consistency
- A $10K/month machine is cheaper than a $200K/year engineer managing microservices
-
Measure complexity by business logic vs framework code
- If your app is 95% framework integration and 5% unique business logic, you have no moat
- The value is in solving domain problems, not plumbing services together
-
Question every new technology
- "We need this for scale" - Do you have the traffic to prove it?
- "It's industry best practice" - For whose industry and what problem?
- "It makes hiring easier" - Does it? Or does it just sound good in meetings?
The Problem with Premature Complexity
Symptoms of over-engineering:
- More microservices than users
- Multiple full-time engineers working on build systems
- Can't add a form field without touching 5+ services
- "It takes hours to do something that should take minutes"
The Kubernetes Problem: If you're using Kubernetes to manage complexity you artificially created, you're solving the wrong problem.
Part II: On Abstraction & Design Patterns
Abstractions Should Be Discovered, Not Invented
The biggest mistake is abstracting before you understand the problem.
The Right Way to Abstract:
-
Write the concrete version first
- Solve the problem in the most straightforward way
- Don't plan for reusability yet
- Accept that it might look "ugly" or repetitive
-
Build a second concrete version when needed
- Now you have two implementations
- The abstraction reveals itself naturally
- You understand what varies and what stays constant
-
Abstract on the third iteration (Rule of Three)
- "When you repeat yourself three times, then refactor"
- By the third time, you truly understand the pattern
- The abstraction will be simpler and more useful
Abstraction Is Not Cost-Free
Every time you create an abstraction—turning an if statement into dynamic dispatch, creating a new class, or adding an interface—you introduce cognitive overhead.
- More Names to Remember: Every new concept adds to the vocabulary a developer must hold in their head
- Increased Indirection: Understanding the code now requires jumping through multiple files and layers
- Hidden Complexity: Abstractions can hide the true nature of what's happening
An abstraction should only be introduced if its benefit in simplifying the system outweighs these inherent costs.
The Myth of "Good Naming"
A common defense of high abstraction is, "If you just give things good names, it's easy to understand." This is a dangerous fallacy.
- Names are Lossy Compression: A function name is an attempt to summarize what could be hundreds of lines of code
- If Names Were Perfect, They'd Be the Language: The reason we write code is because names are insufficient
- Names Create a False Sense of Understanding: Reading a clean, high-level function can make you think you understand what's happening, while the real, messy complexity is hidden layers deep
A good name is better than a bad name, but it is never a substitute for understanding the underlying implementation.
Locality of Behavior vs Perfect Abstraction
Locality of Behavior (LoB): The behavior of a unit of code should be as obvious as possible by looking only at that unit of code.
Example:
// ❌ BAD: Behavior scattered across files
function processOrder(order) {
validateOrder(order) // validation.ts
calculateTotal(order) // pricing.ts
applyDiscounts(order) // discounts.ts
chargeCustomer(order) // payments.ts
sendConfirmation(order) // notifications.ts
}
// ✅ GOOD: Behavior visible in one place
function processOrder(order) {
if (!order.items || order.items.length === 0) {
throw new Error('Order must have items')
}
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0)
const discount = order.coupon ? subtotal * 0.1 : 0
const total = subtotal - discount
chargeCard(order.paymentMethod, total)
sendEmail(order.email, `Order confirmed: $${total}`)
}The second version is longer, but you can understand the entire flow without jumping between files.
Part III: On Code Quality & Practices
Real Code Quality Measures
Good code is:
- Easy to change
- Easy to delete
- Easy to understand (by reading, not by naming)
- Solves the actual problem
- Ships to users
Good code is NOT:
- Perfectly abstracted
- Following every SOLID principle
- 100% test coverage
- Zero duplication
- Impressing other developers
The DRY Trap
"Don't Repeat Yourself" is misunderstood. It's not about code duplication - it's about knowledge duplication.
Two pieces of code can look identical but represent different concepts.
// These look the same but are different concepts
function validateUserEmail(email) {
return email.includes('@')
}
function validateInvoiceEmail(email) {
return email.includes('@')
}If user email validation rules change (e.g., require company domain), you don't want invoice validation to change too. The duplication is correct.
Rule: Duplicate code is cheaper than the wrong abstraction.
On Functions and Complexity
Big functions can be good. A 200-line function that does one thing sequentially is often better than 20 small functions scattered across files.
When to split a function:
- When it does multiple unrelated things
- When you need to reuse part of it
- When it's hard to understand as a whole
When NOT to split:
- Just because it's "too long"
- To follow arbitrary line count rules
- To make it "more testable" (test the behavior, not the implementation)
Part IV: On Testing
A Lifecycle Approach to Testing
Early Stage (0-1):
- Manual testing is fine
- E2E tests for critical paths
- Don't over-invest in tests for code that will change
Growth Stage (1-10):
- Add integration tests for core flows
- Unit tests for complex business logic
- E2E tests for user journeys
Mature Stage (10+):
- Comprehensive test suite
- High confidence in changes
- Tests as documentation
Test What Matters
High value tests:
- User-facing behavior
- Business logic
- Integration points
- Edge cases that caused bugs
Low value tests:
- Implementation details
- Getters/setters
- Framework code
- Code that will change soon
Part V: On Tools & Technology
Read the Reference Documentation
Not tutorials. Not blog posts. The actual documentation.
- Tutorials are often outdated
- Blog posts show one person's opinion
- Stack Overflow answers are context-specific
- Official docs are the source of truth
Read Source Code
When you don't understand how something works, read the source code. It's the only place that tells you exactly what happens.
On Language and Framework Choices
Pick boring technology.
- Use what you know
- Use what has been proven
- Use what has good documentation
- Use what will still be around in 5 years
Don't chase the new shiny.
Part VI: On Learning & Growth
Never Stop Learning
But learn the right things:
- Fundamentals (data structures, algorithms, systems design)
- Your domain (the business you're building for)
- Your tools (the languages and frameworks you use daily)
Don't learn:
- Every new JavaScript framework
- Technologies you'll never use
- Buzzwords to put on your resume
Don't Be Afraid to Touch Code
The worst thing you can do is be afraid of your codebase.
- If code is scary to change, that's a sign it needs to change
- Refactor as you go
- Leave code better than you found it
- Don't let "legacy code" become untouchable
Say "I Don't Know"
It's okay to not know things. It's not okay to pretend you do.
- Ask questions
- Admit when you're stuck
- Learn from others
- Document what you learn
Part VII: Anti-Patterns to Avoid
Conference-Driven Development
Don't adopt technologies just because they were cool at a conference. Ask:
- Do we have the problem this solves?
- Do we have the scale this requires?
- Do we have the expertise to use it well?
The Rewrite on Weekend Syndrome
"I could rewrite this entire system in a weekend in Rust/Go/Elixir."
No, you couldn't. You'd rewrite the features you remember, miss all the edge cases, and create new bugs.
Premature Microservices
Microservices are not a starting architecture. They're a solution to organizational scaling problems, not technical ones.
The Test Everything Cult
100% test coverage is not a goal. It's a vanity metric. Test what matters, not what's easy to test.
Chasing the New Shiny
Every few years, the industry decides everything we've been doing is wrong and we need to rewrite everything in the new paradigm.
Ignore the hype. Use what works.
Part VIII: Practical Wisdom
On Code Reviews
Good code reviews:
- Catch bugs
- Share knowledge
- Ensure consistency
- Improve code quality
Bad code reviews:
- Nitpick style
- Enforce personal preferences
- Block progress
- Create resentment
On Making Changes
When changing code:
- Understand why it exists
- Read the git history
- Check for tests
- Make the change
- Test thoroughly
- Document if needed
Don't:
- Delete code you don't understand
- Rewrite working code "because it's ugly"
- Change patterns without team discussion
On Working with Legacy Code
Legacy code is code without tests. It's also code that works and makes money.
- Add tests before changing
- Refactor incrementally
- Don't rewrite unless absolutely necessary
- Respect the code - someone solved real problems with it
Part IX: Pragmatic Decision Framework
When making technical decisions, ask:
- Does this solve a real problem we have right now?
- Is this the simplest solution?
- Can the team maintain this?
- What happens if this fails?
- Can we change our mind later?
If you can't answer these clearly, you don't understand the problem well enough.
Conclusion: Pragmatism Over Purity
Good software:
- Solves real problems
- Ships to users
- Can be changed
- Makes money
Good software is NOT:
- Perfect
- Following all best practices
- Impressing other engineers
- Using the latest technology
Be pragmatic. Solve problems. Ship code.