DEV Community

Rahul Dhawan
Rahul Dhawan

Posted on

Design Patterns Without the Overengineering: Create, Connect, Collaborate

Estimated reading time: 5 minutes

Your checkout service started small. Now it supports three payment providers, two shipping rules, and notifications scattered across the codebase.

Every new feature means another conditional. Every change feels riskier.

This is where design patterns can help—not as fancy class diagrams, but as reusable approaches to recurring design problems.

The trick? Choose a pattern because you have a problem, not because you recognise its name.

Let's explore three families through an online-store example.

The three families at a glance

These are the three categories used for classic object-oriented design patterns—not a classification of every software architecture pattern.1

Family Main question Examples covered here
Creational How should objects be created? Factory Method, Builder, Singleton
Structural How should objects fit together? Adapter, Decorator, Facade
Behavioural How should objects collaborate? Strategy, Observer

1. Creational patterns: Control object creation

Factory Method: Let subclasses choose what gets created

Factory Method defines a creation operation that subclasses override to return different implementations. The consuming workflow works with a shared interface.2

Scenario: A base checkout flow requests a payment processor. Regional checkout subclasses supply different processors without rewriting the checkout steps.

  • Choose it when: An extensible workflow should delegate object creation to subclasses.
  • Pro: The workflow isn't tied to concrete processor classes.
  • Con: Extra subclasses can make a simple design unnecessarily complicated.

Watch the terminology: A function that selects a processor using if or switch is often called a simple factory. Useful, but not the same as classic Factory Method.

Builder: Assemble complex objects step by step

Builder separates construction from the finished object. A fluent builder is one practical variation, especially for objects with many optional settings.3

Scenario: Create an order with delivery preferences, gift wrapping, discounts, and billing details.

Illustrative usage—not a complete implementation:

Order order = new Order.Builder(customerId)
    .withGiftWrap(true)
    .withDeliveryInstructions("Leave at reception")
    .build();
Enter fullscreen mode Exit fullscreen mode
  • Choose it when: Constructor calls become hard to read or construction requires several steps.
  • Pro: Named steps improve readability; build() can validate the final combination.
  • Con: Builder code adds maintenance overhead. For two required fields, a constructor is probably clearer.

Singleton: Restrict a class to one instance

Singleton controls instantiation and provides a shared access point.4

Scenario: An application requires one configuration registry within a particular runtime scope.

  • Choose it when: A genuine single-instance constraint exists—not merely because sharing is convenient.
  • Pro: Centralises access to that instance.
  • Con: Global mutable state creates hidden dependencies and makes tests harder to isolate.

Important: A Singleton is not automatically one instance across multiple servers. Its scope depends on the runtime. Consider explicit dependency injection when the real requirement is sharing an object.

2. Structural patterns: Connect objects cleanly

Adapter: Translate an incompatible interface

Adapter wraps an existing component so clients can use the interface they expect.5

Scenario: Your store expects charge(amount), but a legacy payment library exposes submitTransaction(request). An adapter translates the call and response.

  • Choose it when: Integrating legacy code or third-party libraries you cannot change.
  • Pro: Vendor-specific details stay outside business logic.
  • Con: Translation introduces another layer; incompatible semantics can still leak through.

Decorator: Add behaviour without changing the interface

Decorator wraps an object while preserving its interface, allowing responsibilities to be composed.6

Scenario: Wrap a product repository with caching, then wrap that with request logging. Callers still use the same repository operations.

  • Choose it when: Optional behaviours need flexible combinations without a subclass for every combination.
  • Pro: Adds capabilities without modifying the underlying component.
  • Con: Wrapper order matters, and deep chains can complicate debugging.

Facade: Give a subsystem a simpler front door

Facade offers a simplified interface over several components.5

Scenario: placeOrder() coordinates inventory reservation, payment, and shipment creation instead of making every caller orchestrate them.

  • Choose it when: Callers repeatedly interact with the same complicated subsystem.
  • Pro: Reduces coupling to subsystem details.
  • Con: The facade can become an oversized “god object.” It also doesn't magically provide transactions or failure recovery.

Remember: Adapter translates an interface; Decorator adds responsibilities; Facade simplifies access.

3. Behavioural patterns: Organise collaboration

Strategy: Swap the algorithm, not the caller

Strategy puts interchangeable algorithms behind a common contract.7

Scenario: Checkout delegates shipping calculations to standard, express, or pickup strategies.

  • Choose it when: Several versions of an operation change independently of their caller.
  • Pro: New algorithms don't require expanding conditionals throughout checkout.
  • Con: Someone still needs to select the strategy, and trivial variations may not justify separate classes.

In languages with first-class functions, a strategy can be a function—not necessarily a class hierarchy.

Observer: Notify subscribers about changes

Observer lets interested objects subscribe to notifications from a subject.8

Scenario: When an order changes status, subscribed components update the dashboard and notify the customer.

  • Choose it when: Multiple components should react without the subject knowing their concrete implementations.
  • Pro: Subscribers can evolve independently.
  • Con: Indirect control flow complicates debugging; subscriptions need lifecycle management.

An in-process Observer does not automatically provide durable messaging, retries, or cross-service delivery.

Patterns are tools, not a checklist

Before introducing one, ask:

  1. What actually varies? Creation, composition, or behaviour?
  2. What becomes easier to change or test?
  3. Is the abstraction simpler than the problem?

Start with straightforward code. Introduce a pattern when its benefits outweigh its moving parts.

Create deliberately. Connect cleanly. Collaborate loosely.

Which pattern solved a real problem for you—and which one did you later remove?


References


  1. University of Washington, CSE 331, Object-Oriented Design, Summer 2025: design-pattern categories. ↩

  2. Microsoft Learn, Discovering the Design Patterns You're Already Using in .NET: Factory Pattern. ↩

  3. Vaskaran Sarcar, Creational Design Patterns in Java, Chapter 3: Builder Pattern, Apress, 2026. ↩

  4. Microsoft Learn, Solidify Your C# Application Architecture with Design Patterns: Singleton. ↩

  5. Microsoft Learn, Design Patterns: Adapter and Façade, Visual Studio Toolbox. ↩

  6. Microsoft Learn, Discovering the Design Patterns You're Already Using in .NET: Decorator Pattern. ↩

  7. Microsoft Learn, Design Patterns: Strategy, Visual Studio Toolbox. ↩

  8. Microsoft Learn, Observer design pattern, .NET documentation. ↩

Top comments (0)