Skip to content
ExponaBLOG
DESIGN PATTERNS IN PRACTICE · DAY 100 / 100

100 days of design patterns: lessons — a practical guide

Wrapping up the series.

Expona Admin7 min read

A hundred days ago, I started this journey to explore design patterns beyond theory—to see them as they actually live in production code. Today, I'm wrapping up 100 days of design patterns: lessons, and I want to share what I've learned, because the most important patterns aren't the ones in textbooks.

This series pushed me to think differently about how we design systems. It's not about collecting patterns like trading cards. It's about recognizing problems, naming solutions, and building codebases that people actually want to maintain.

Patterns are vocabulary — They make design conversations faster

Before I understood this deeply, I'd spend thirty minutes explaining an architecture to a teammate. I'd draw boxes, talk about responsibilities, describe how state flows through layers. Then someone would say, "Oh, that's the Observer pattern," and suddenly we all knew exactly what we were building.

Patterns compress communication. When you say "repository pattern," a mid-level developer already knows you're abstracting data access. When you say "middleware," a NestJS developer knows the request pipeline. You've eliminated five minutes of explanation.

I saw this most clearly when onboarding new engineers. Instead of explaining, "We have this layer that validates input and transforms it before it hits the service," I could say, "We use the Decorator pattern for validation." The junior dev would nod, search it up, and suddenly the code made sense.

The same applies in code reviews. Pattern names let you be precise without being verbose. Instead of:

"This function is doing too many things. Maybe we should split the data fetching from the business logic from the presentation."

You can say:

"This violates Single Responsibility. Let's separate concerns with a Repository and Service layer."

Both say the same thing, but the second one gives the reviewer a mental framework to work from. They can immediately picture the structure you're proposing.

This doesn't mean you need to know every pattern by heart. It means: when you encounter a pattern in the wild, name it. Use it in conversation. Over time, your team builds a shared vocabulary, and your design reviews become faster and your architecture decisions become clearer.

Problems first — The best pattern is the one the problem asks for

Early in this series, I made a mistake. I tried to apply patterns preemptively. "This codebase might need caching, so let me build a Strategy pattern for cache providers." I built abstractions before the problem existed.

Then I watched the code rot. The abstraction supported four different caching strategies that we never used. It added complexity nobody needed. The pattern became a liability instead of a tool.

The insight I had around day 35 changed how I approach design: the problem comes first. Always. The pattern is the answer to a problem you actually have, not a problem you might have.

Let me show you a real example. I was building a trading platform in .NET where different clients needed different order routing rules. Some orders go to broker A, some to broker B, some need special compliance checks.

The wrong approach: "I'll build a comprehensive Strategy pattern that can handle any routing rule ever invented."

The right approach: "What's the actual problem? Right now, we're hardcoding routing logic in a massive if-else chain. This is impossible to test and new rules break existing ones."

The pattern answer: Strategy. But only the parts we need.

typescript
interface OrderRoutingStrategy {
  canHandle(order: Order): boolean;
  route(order: Order): Promise<RoutingResult>;
}

class BrokerAStrategy implements OrderRoutingStrategy {
  canHandle(order: Order): boolean {
    return order.size < 10000;
  }
  
  async route(order: Order): Promise<RoutingResult> {
    // Route to broker A
  }
}

class Router {
  constructor(private strategies: OrderRoutingStrategy[]) {}
  
  async route(order: Order): Promise<RoutingResult> {
    const strategy = this.strategies.find(s => s.canHandle(order));
    if (!strategy) throw new Error('No routing strategy found');
    return strategy.route(order);
  }
}

This is a clean solution to the actual problem. Not over-engineered. Not under-engineered. It solves the problem we have today and makes it easy to add a new broker next month without touching existing code.

The pattern I chose (Strategy) emerged from the problem, not the other way around. This is how design patterns should work. Read your code, find the pain point, and match a pattern to it.

Simplicity wins — Most codebases need fewer abstractions, not more

I've worked on five different production platforms across e-commerce, CRM, trading, and SaaS. The highest-performing codebases I've seen aren't the most heavily patterned. They're the most simply patterned.

A lot of developers hear "design patterns" and think that means maximum abstraction. Build interfaces for everything. Make every dependency injectable. Create adapter layers between your code and your framework. If one pattern is good, two must be better.

That's wrong. The cost of a pattern is cognitive load. Every abstraction you introduce is another thing a developer needs to understand before they can change the code. Every interface is another file to navigate. Every factory is another layer of indirection.

In a ride-hailing platform I worked on, I inherited a payment module with eleven different abstraction layers. Each layer justified itself individually—they all solved real problems. But together, tracing a payment from user action to database took me through seven files. A bug in payment confirmation took me six hours to find, not because the code was complex, but because finding it required understanding eleven different patterns.

I refactored it down to three layers: Controller → Service → Repository. Same patterns, simpler structure. Bug-finding time dropped to forty minutes. Code review time dropped by half.

This is the pattern lesson nobody wants to hear: the best abstraction is no abstraction. Use a pattern when it solves a real, immediate problem. Not before. Not "just in case."

Here's a principle I follow now: if I can't explain why I need an abstraction in two sentences, I don't build it.

Patterns are everywhere — Recognizing them made every framework easier to learn

Around day 50, something shifted. I was learning NestJS for a SaaS backend, and instead of memorizing docs, I started asking: "What patterns is this built on?"

NestJS decorators are the Decorator pattern. Dependency injection is a pattern. Middleware is the Chain of Responsibility pattern. Pipes and Guards are variations of the strategy pattern. The request lifecycle is fundamentally a pipeline—a classic architectural pattern.

Suddenly the framework made sense. Not because I read more docs, but because I understood the patterns underneath.

Same thing happened with React. Props flowing down and callbacks coming up—that's the Observer pattern with a unidirectional data flow. Custom hooks—those are utility patterns for reusable logic. Context API is the Mediator pattern helping you avoid prop drilling.

I've picked up Flutter, React Native, and PostgreSQL during this series. And in every single one, patterns emerged. Flutter's widget tree is a Composite pattern. Redis persistence strategies are different implementations of the Strategy pattern. PostgreSQL transactions follow the ACID guarantee structure that's a pattern in its own right.

Here's what this meant practically: I spent less time learning frameworks and more time understanding how they work. Documentation became optional because I could reason about what should exist. When I hit an unfamiliar part of the framework, I'd think about the problem it's solving, recognize the pattern, and suddenly it was obvious.

The lesson: design patterns are the language of software architecture. Learn the patterns, and you'll learn any framework, language, or platform faster. You're not learning new concepts—you're learning new syntax for the same concepts.

Common mistakes

  • Pattern shopping before problem definition. Don't start with "Which pattern should I use?" Start with "What problem am I solving?" and let the pattern emerge.
  • Over-abstracting for flexibility you don't need. Building a plugin system for something that will never have plugins adds maintenance burden. Wait until you actually need the flexibility.
  • Using patterns as job security. Some developers make code complex with unnecessary patterns to seem smart or to make themselves indispensable. This backfires. Simplicity is the mark of a strong designer.
  • Ignoring framework patterns. Every major framework implements patterns. Learn them. Use them. Don't fight against the framework's design philosophy by forcing different patterns on top.
  • Confusing naming with understanding. Just because you can name the pattern doesn't mean you understand when to use it. The name is a starting point, not the endpoint.

Key takeaways

  • Patterns are vocabulary for design. They make communication precise and faster. Use pattern names in code reviews and design discussions.
  • Always solve the problem you have, not the problem you might have. Let the pattern emerge from the problem, not the other way around.
  • Simplicity is the goal. Each abstraction you add has a real cost. Use patterns deliberately, not speculatively.
  • Patterns are the foundation of every framework and platform. Learning to recognize them makes you faster at picking up new tools.
  • The best codebase is one your team can modify confidently. That requires clarity over cleverness, and appropriate use of patterns over maximum abstraction.
  • Keep learning. These hundred days weren't the end of the journey. They were the foundation. The patterns that matter most are the ones you discover in your own code.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles