Design Philosophy Primer
A lot of developers and architects have asked me about my design philosophy. Traditional design is based on requirements and use cases…
A lot of developers and architects have asked me about my design philosophy. Traditional design is based on requirements and use cases. There’s nothing wrong with that. Then there are the proponents of Domain Driven Design. Well, there’s nothing inherently wrong with that either.
But the key thing you learn as a senior engineer is that like everything else in software, the answer is — It Depends.
The Story Of A Broken Payment System
Last year, I was called in to help a fintech company whose payment system was falling apart. They’d started eighteen months earlier with a beautiful, user-story-driven approach. “As a customer, I want to pay with my credit card.” Three days later, they had a working demo. Users loved it. They shipped fast and gathered great feedback.
The team was proud of their agility. Every new payment method took just days to add. Gift cards? Done. Digital wallets? Shipped. The business was thrilled with their velocity.
Then reality hit.
When marketing wanted split payments — gift card plus credit card for a single purchase — what should have been a simple feature took three weeks and broke existing functionality. Refunds became inconsistent across payment types. International support required a month-long refactoring that touched every part of the system.
The diagnosis was clear: They’d optimized for shipping individual features fast, but each feature added complexity that compounded. Every new payment type meant more conditional logic scattered throughout the codebase. The system had no coherent model of what a “payment” actually was.
Here’s what we did differently:
Instead of thinking about user stories first, we stepped back and modeled the domain. What is a payment method, really? What do authorization, capture, and settlement mean? How do different payment types behave differently, and what do they have in common?
Ontology Based Design
A payment method isn’t just “how you pay” — it’s an entity with specific capabilities and constraints. Credit cards can authorize funds for 7 days before capture, but require CVV validation. Gift cards authorize and capture instantly, but can only refund to store credit. Digital wallets skip authorization entirely and settle immediately, but have daily transaction limits.
Authorization means “reserve the money but don’t transfer it yet.” Capture means “actually transfer the reserved money.” Settlement means “move money between bank accounts.” These aren’t just technical steps — they’re business concepts with different timing, failure modes, and regulatory requirements.
Here’s what this meant concretely: Instead of having separate methods like processCreditCard() and processGiftCard() with completely different logic, we defined what every payment method must be able to do: authorize a specific amount, capture a previously authorized transaction, and report whether refunds go back to the original source or store credit.
When Apple Pay arrived, we didn’t need to write new business logic. Apple Pay authorizes like a credit card, captures immediately like a gift card, and refunds to the original source. It just combined existing behaviors we’d already modeled.
Split payments stopped being a nightmare because we’d realized they’re not a special payment type — they’re multiple payment methods coordinated through a single transaction. A customer paying $100 with a $60 gift card and $40 credit card is just two separate authorizations that either both succeed or both fail.
Same team, same requirements. Completely different outcomes.
The Tale of Two Design Philosophies
This story teaches that there are fundamentally two ways to approach software design:
Requirements-Driven Design: Start with what users can see and do. Build features that solve immediate problems. Let structure emerge from usage patterns.
Ontology-Driven Design: Start by understanding the domain deeply. Model the entities, relationships, and rules that exist in reality. Then build features on top of that foundation.
Most teams unconsciously pick one approach and stick with it everywhere. That’s the mistake.
When Fast and Scrappy Wins
Requirements-driven design is brilliant when:
You’re exploring unknown territory. Startups hunting for product-market fit can’t afford to over-engineer solutions to problems that might not exist.
You’re in a fast-changing market. When pivoting is likely, you want loose coupling to your current assumptions.
You have simple, well-understood domains. A basic blog or todo app doesn’t need complex domain modeling.
You’re building short-lived tools. Migration scripts and internal utilities should optimize for “get it done” over “get it right.”
I saw this work beautifully with a gaming startup that pivoted from trivia games to sports betting in nine months. Their lightweight, story-driven architecture made the transition possible. Heavy domain modeling would have trapped them in the wrong abstractions.
When Deep Modeling Pays Off
But ontology-driven design wins in different contexts:
Highly regulated domains. Banking and healthcare systems need explicit modeling of rules and constraints. You can’t fake your way through compliance.
Long-lived systems. If you’re building something that needs to run for five to ten years, the upfront investment in understanding pays dividends.
Complex business domains. When rules interact in non-obvious ways, explicit modeling prevents subtle bugs that emerge from feature interactions.
Data-centric platforms. Analytics engines and knowledge management systems benefit from stable, well-modeled schemas.
The UK Government Digital Service is a perfect example. They modeled “citizen personas and entitlements” early in their Brexit-related systems. When complex residency rules changed, modifications that could have taken months happened in days. They saved over £2 million in development costs.
Real World: Five Patterns That Actually Work
Here’s what I’ve learned: you don’t have to choose just one approach for an entire system.
Pattern 1: Core Domain First — Model the stable kernel that every feature touches (think User, Money, Time), but build peripheral features through user stories.
Pattern 2: Service Boundaries — Use rich domain models inside service boundaries, but keep interfaces simple and story-driven.
Pattern 3: Delayed Commitment — Start with stories to validate assumptions, then refactor toward domain models once patterns emerge.
Pattern 4: Concept Caging — Place complex domain concepts behind clean APIs. Teams outside the domain use simple interfaces; teams inside use rich models.
Pattern 5: Evolution Reviews — Schedule regular sessions to identify when story-driven code should evolve into domain models.
The Decision Framework I Actually Use
Before starting any significant feature, I ask three sets of questions:
About the Domain:
- How well do we understand this space?
- Are there complex rules that interact?
- How stable are the core concepts?
About the Team:
- Do we have domain expertise?
- How comfortable are we with up-front design?
- What’s our track record with refactoring?
About the Context:
- How long will this system live?
- How quickly do requirements change?
- What’s the cost of getting the abstraction wrong?
A payments team in a fintech company? Probably ontology-driven. A prototype for a new feature in a startup? Probably requirements-driven. An internal tool that might become customer-facing? Start requirements-driven, evolve to ontology-driven.
The Mental Models That Guide My Decisions
“Goldilocks Modeling” — Model what will matter in three years, not every possible nuance. Neither too little abstraction nor too much.
“The Rule of Three” — Don’t create an abstraction until you’ve seen the same pattern at least three times. Premature abstraction is as dangerous as no abstraction.
“Real User Narrative Test” — Every domain concept should trace back to something users actually care about. This prevents ivory tower modeling.
“Total Cost Perspective” — Requirements-driven is cheaper upfront but more expensive long-term. Ontology-driven is expensive upfront but cheaper long-term. Match your approach to your time horizon.
What This Means for Your Next Project
Stop asking “Should we do DDD or not?”
Start asking:
- What’s the right design approach for this specific context?
- Which parts of our system need deep modeling?
- Which parts benefit from lightweight, story-driven development?
- How can we evolve our approach as we learn more?
The goal isn’t to pick the “right” methodology. The goal is to consciously choose the approach that matches your constraints and objectives.
Requirements-driven gives you speed and feedback loops.
Ontology-driven gives you stability and change absorption.
Hybrid approaches give you both, applied where they matter most.
The key is intentional choice, not accidental architecture.
Most teams design backwards because they never consciously chose a design philosophy. They inherited one, or fell into one, or copied what they’d seen before.
The best teams I’ve worked with make this choice explicitly, for each part of their system, based on evidence and context.
Want to learn more about Software Design and how to do it right. Head over to Stackshala to start your journey on software craftsmanship.