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

Pattern-itis: concepts, trade-offs and production lessons

The urge to use every pattern you learned.

Expona Admin7 min read

Pattern-itis: concepts, trade-offs and production lessons

You've just finished reading a book on design patterns, completed a course on software architecture, or attended a conference talk on SOLID principles. The ideas feel fresh, powerful, and suddenly you see them everywhere in your codebase. The urge to refactor, to apply every pattern you learned, becomes almost irresistible. We call this pattern-itis: the tendency to force design patterns into situations where they're not needed, creating unnecessary complexity and making code harder to maintain rather than easier.

Pattern-itis is common in mid-level developers and even experienced engineers when they encounter new paradigms. It's a natural response to learning, but it's also one of the fastest ways to create technical debt. After years building e-commerce platforms, CRM systems, and real-time applications, we've learned that the best code isn't the most patterned code—it's the simplest code that solves the actual problem.

Patterns solve specific problems

Let's start with the fundamental truth: every design pattern in the Gang of Four book, every architectural pattern you've read about, exists because it solves a specific, recurring problem. The Factory pattern addresses object creation complexity. The Observer pattern handles one-to-many notifications. The Strategy pattern lets you swap algorithms at runtime. Each pattern has a precise reason for existing.

Consider the Repository pattern, which we use frequently in data-driven applications. It abstracts data access logic behind an interface, decoupling your business logic from the database. This is valuable when:

  • You have multiple data sources (SQL, NoSQL, APIs) for the same entities
  • You need to test business logic without a database
  • Different teams own the data layer and business layer
  • Your query logic is complex enough to deserve encapsulation

But if you're building a simple CRUD API with a single PostgreSQL database and no complex queries, a Repository adds a layer of indirection that buys you nothing. You're paying the cost of abstraction without receiving the benefit.

We once inherited a Node.js service where every single endpoint used an Adapter pattern to transform data. The adapters were thin wrappers with minimal logic. When we needed to change a field, we had to update the adapter, the database schema, the API contract, and the frontend—five places instead of three. The pattern became a maintenance tax rather than a benefit.

No problem, no pattern

This is the core of avoiding pattern-itis: patterns should emerge from problems, not precede them. Write straightforward code first. When you hit friction—repeated logic, tangled dependencies, difficulty testing—then consider whether a pattern fits.

Here's a real example from a trading platform we maintained. Early versions used a single service class for order processing:

typescript
class OrderService {
  async processOrder(orderId: string) {
    const order = await this.db.orders.findById(orderId);
    this.validateOrder(order);
    this.checkInventory(order.items);
    this.calculateTax(order);
    this.calculateShipping(order);
    await this.db.orders.update(orderId, order);
    await this.publishEvent('order.processed', order);
    return order;
  }
}

This worked fine for months. But as business rules multiplied—different tax rules by region, shipping integrations by carrier, validation rules by product type—the service became 2,000 lines. Testing a single scenario required mocking everything. Adding a new region's rules meant diving into the monolith.

At that point, we had a real problem. The pattern that solved it wasn't predetermined; it emerged from the problem itself. We extracted Tax, Shipping, and Validation as separate services using the Strategy pattern, but only the parts that actually varied by scenario:

typescript
interface TaxCalculator {
  calculate(order: Order): Promise;
}

class OrderService {
  constructor(
    private taxCalculator: TaxCalculator,
    private shippingCalculator: ShippingCalculator,
  ) {}

  async processOrder(orderId: string) {
    const order = await this.db.orders.findById(orderId);
    this.validateOrder(order);
    order.tax = await this.taxCalculator.calculate(order);
    order.shipping = await this.shippingCalculator.calculate(order);
    await this.db.orders.update(orderId, order);
    await this.publishEvent('order.processed', order);
    return order;
  }
}

Notice we didn't extract validation into a separate strategy—it wasn't complex enough to warrant it. We applied the pattern precisely where the problem existed. This is how patterns work in production: surgically, where needed, not everywhere at once.

Simple code is easier to change

There's an underrated principle in software engineering: simplicity enables change. The most complex code isn't always the most patterned code, but premature patterning usually creates complexity.

Simple code has several advantages:

  • Onboarding is faster. A new engineer understands direct logic quicker than abstract patterns. You spend less time explaining your architecture decisions.
  • Bugs are easier to find. When everything is straightforward, the problem is usually visible. When logic is spread across multiple indirections, tracing becomes detective work.
  • Changes are more localized. When you need to add a feature or fix a bug, simple code often requires changes in one place. Highly patterned code forces you to update multiple layers.
  • Optimization is clearer. If performance is an issue, simple code makes the bottleneck obvious. Abstract patterns can hide performance problems behind layers.

We built a ride-hailing service that initially applied the CQRS (Command Query Responsibility Segregation) pattern everywhere. The architecture was beautiful: separate command and query models, event sourcing, eventual consistency. It looked like a textbook example.

But when we needed to add driver acceptance rates to the matching algorithm, we had to understand the entire CQRS pipeline: where commands were emitted, how they projected to read models, when those models became consistent. A simple feature took three weeks. In a simpler architecture, it would have taken two days.

We weren't wrong to use CQRS—we were wrong to use it everywhere. Only the matching engine truly benefited from separated read and write concerns. The rest of the system went back to boring, simple code. And paradoxically, after simplifying, we shipped features faster.

Refactor to patterns when the need appears

The antidote to pattern-itis is a pragmatic approach: start simple, monitor for pain, and refactor to patterns when the pain becomes clear. This is the essence of evolutionary architecture.

In practice, here's how we approach it:

  1. Write the simplest code that works. Make it clear and direct. Don't think about patterns yet.
  2. Watch for repeated friction. If you're copy-pasting logic, struggling to test a component, or finding a file growing without bound, you've identified a problem.
  3. Name the problem explicitly. "This module is responsible for too many things" (Single Responsibility Problem). "I can't test this without three mocks" (Testability Problem). "This logic varies by context" (Variability Problem).
  4. Find the pattern that solves that problem. Use an index like the Design Patterns book or architecture guides to match problem to pattern.
  5. Apply the pattern minimally. Often you don't need the full textbook implementation. Take what solves your problem, leave the rest.
  6. Measure the improvement. Did code become easier to test? Easier to change? Did readability improve or decline? Let results, not aesthetics, guide you.

In a SaaS platform we built, the first version of the payment processor was a direct integration with Stripe. As features grew, we added PayPal, then Square. At that point, multiple payment gateways became a real problem. The Strategy or Factory pattern suddenly made sense because we actually had multiple strategies.

typescript
interface PaymentGateway {
  charge(amount: number, token: string): Promise;
  refund(chargeId: string): Promise;
}

class PaymentProcessor {
  constructor(private gateway: PaymentGateway) {}
  
  async processPayment(amount: number, token: string) {
    return this.gateway.charge(amount, token);
  }
}

This pattern wasn't useful on day one. It became essential on day 200, when the business demanded it. We refactored to it then, not before.

Common mistakes

Over-abstracting early: The most common form of pattern-itis is adding abstraction layers before you understand the problem domain. You create interfaces for things that don't vary. You split code into multiple classes when one would do. The remedy: resist the temptation. Code duplication is acceptable in the short term. Unnecessary abstraction is costly immediately.

Using patterns as resume-building: Sometimes we apply patterns because they look impressive, because they demonstrate sophisticated thinking. This is ego-driven architecture. The measure of good design isn't how many patterns you use; it's how easily the team can build features and fix bugs. A simple codebase is more impressive than a patterned one.

Confusing familiar patterns with appropriate patterns: You learned Dependency Injection and it's powerful, so you apply it everywhere. You read about Domain-Driven Design and start defining Bounded Contexts in a CRUD app. Patterns are tools; use the right tool for the job, not the tool you most recently learned.

Premature "future-proofing": "We might need to support multiple databases someday, so let's build a Repository pattern." You're solving a problem that doesn't exist, using complexity as insurance. Real future-proofing comes from good code practices—clear naming, separated concerns, good tests—not from pre-emptive patterns.

Key takeaways

  • Design patterns exist to solve specific, recurring problems. Before applying any pattern, identify the actual problem it solves and whether you have that problem.
  • Write simple, straightforward code first. Patterns should emerge from friction and pain, not precede them.
  • Simplicity enables faster onboarding, easier debugging, more localized changes, and better optimization. It's a feature, not a limitation.
  • Refactor to patterns incrementally as problems become clear. The code that's right today might need refactoring tomorrow, and that's fine—that's how good systems evolve.
  • Measure patterns by their practical impact: Do they reduce bugs? Speed up feature delivery? Make the codebase easier for new engineers to understand? If the answer is no, the pattern isn't paying for itself.
  • The best architecture isn't the most sophisticated; it's the one that lets your team ship features reliably and change course quickly.
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