Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Software Development

Design Patterns for Modern Software Development

The Gang of Four patterns were written for C++ in 1994. Some still apply perfectly. Others need rethinking for async, event-driven, cloud-native code. Here are the patterns that actually matter in 2026.

📚 Architecture January 16, 2026 15 min read

In This Guide

Design patterns are reusable solutions to common problems. The original 23 GoF patterns were groundbreaking, but modern development looks different — we write async code, deploy to clouds, communicate through events, and build distributed systems. Some classic patterns (Strategy, Observer, Factory) are more relevant than ever. Others (Singleton, Template Method) often cause more harm than good. This guide covers the patterns we actually use in production systems, with real TypeScript and Node.js examples, not abstract UML diagrams.

1. GoF Patterns That Still Matter

Pattern Still Useful? Modern Form
StrategyEssentialPass functions instead of strategy classes. First-class functions replace the class hierarchy.
ObserverEssentialEventEmitter, RxJS, domain events, pub/sub. The foundation of reactive programming.
FactoryEssentialDI containers, factory functions. Creating objects based on runtime config.
AdapterEssentialAnti-corruption layers, API wrappers. Translating between incompatible interfaces.
DecoratorVery usefulMiddleware chains, TypeScript decorators, higher-order functions.
ProxyVery usefulJS Proxy, API gateways, caching proxies, lazy loading.
SingletonAvoidUse DI containers for shared instances. Singletons hide dependencies and break testing.
Template MethodRarelyComposition over inheritance. Use Strategy with function injection instead.

Strategy Pattern — Modern Style

// Classic GoF: class hierarchy
interface PaymentStrategy { pay(amount: Money): Promise<Receipt>; }
class StripePayment implements PaymentStrategy { ... }
class RazorpayPayment implements PaymentStrategy { ... }

// Modern: just use functions
type PaymentFn = (amount: Money) => Promise<Receipt>;

const payWithStripe: PaymentFn = async (amount) => {
  const charge = await stripe.charges.create({ amount: amount.toCents() });
  return Receipt.from(charge);
};

const payWithRazorpay: PaymentFn = async (amount) => {
  const order = await razorpay.orders.create({ amount: amount.toPaise() });
  return Receipt.from(order);
};

// Usage: pass the function, not a class instance
async function checkout(cart: Cart, pay: PaymentFn): Promise<Order> {
  const total = cart.calculateTotal();
  const receipt = await pay(total);
  return Order.confirm(cart, receipt);
}

2. Modern Creational Patterns

Builder Pattern — For Complex Configuration

The builder pattern shines when constructing objects with many optional parameters. Instead of a constructor with 15 arguments, chain method calls.

// Query builder — fluent API
const users = await db
  .select('users')
  .where({ role: 'admin', active: true })
  .orderBy('created_at', 'desc')
  .limit(20)
  .offset(page * 20)
  .execute();

// HTTP request builder
const response = await HttpRequest
  .post('https://api.example.com/orders')
  .header('Authorization', `Bearer ${token}`)
  .header('Idempotency-Key', orderId)
  .body({ items, shippingAddress })
  .timeout(5000)
  .retry(3)
  .send();

Factory Function — Replacing Class Factories

// Factory function with validation — cleaner than constructor overloads
function createUser(input: CreateUserInput): Result<User, ValidationError> {
  const email = Email.parse(input.email);
  if (!email.ok) return Err(new ValidationError('Invalid email'));

  const name = UserName.parse(input.name);
  if (!name.ok) return Err(new ValidationError('Name too short'));

  return Ok(new User({
    id: UserId.generate(),
    email: email.value,
    name: name.value,
    role: input.role ?? 'member',
    createdAt: new Date()
  }));
}

3. Behavioral Patterns for Async Systems

Middleware / Chain of Responsibility

Express middleware, Koa middleware, Redux middleware — they're all the Chain of Responsibility pattern. Each handler processes the request and decides whether to pass it to the next handler.

// Generic middleware pipeline
type Middleware<T> = (ctx: T, next: () => Promise<void>) => Promise<void>;

function compose<T>(...middlewares: Middleware<T>[]): (ctx: T) => Promise<void> {
  return async (ctx: T) => {
    let index = -1;
    async function dispatch(i: number): Promise<void> {
      if (i <= index) throw new Error('next() called multiple times');
      index = i;
      if (i >= middlewares.length) return;
      await middlewares[i](ctx, () => dispatch(i + 1));
    }
    await dispatch(0);
  };
}

// Usage: logging → auth → rate-limit → handler
const pipeline = compose(logging, authenticate, rateLimit, handleRequest);

Publisher/Subscriber (Event Bus)

// Type-safe event bus
type EventMap = {
  'order.placed': { orderId: string; total: number };
  'user.registered': { userId: string; email: string };
  'payment.failed': { orderId: string; reason: string };
};

class EventBus {
  private handlers = new Map<string, Function[]>();

  on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void) {
    const list = this.handlers.get(event) || [];
    list.push(handler);
    this.handlers.set(event, list);
  }

  emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
    (this.handlers.get(event) || []).forEach(h => h(data));
  }
}

// Type-safe: TypeScript catches wrong event names or payloads
bus.on('order.placed', (data) => sendConfirmationEmail(data.orderId));
bus.on('payment.failed', (data) => notifySupport(data.orderId, data.reason));

4. Cloud-Native Patterns

Pattern Problem It Solves Implementation
SidecarAdd cross-cutting concerns without modifying the appEnvoy proxy for mTLS, logging agent for log collection
AmbassadorOffload network concerns from the appLocal proxy that handles retries, circuit breaking for legacy apps
Strangler FigIncrementally replace a legacy systemRoute new features to new service, old features to legacy. Gradually migrate.
Backend for FrontendDifferent clients need different API shapesMobile BFF returns slim payloads, web BFF returns rich data
OutboxReliable event publishing with database consistencyWrite event to DB table in same transaction, poll and publish to broker
SagaDistributed transactions across servicesEach service does local transaction + event. Compensating actions on failure.

5. Resilience Patterns

These patterns keep your system running when things go wrong — and in distributed systems, things always go wrong.

Pattern What It Does When to Use
Circuit BreakerStops calling a failing service after N failuresEvery external service call. Non-negotiable.
Retry with BackoffRetries failed calls with increasing delaysTransient failures (network blips, 503s). NOT for 400s.
BulkheadIsolates failures to prevent cascadingSeparate connection pools per downstream service
TimeoutFails fast instead of waiting foreverEvery network call. Every database query. No exceptions.
FallbackReturns cached/default data when primary source failsProduct catalog (show cached), recommendations (show popular)
// Retry with exponential backoff + jitter
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 200
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      if (!isRetryable(error)) throw error;

      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt);
      const jitter = delay * (0.5 + Math.random() * 0.5);
      await sleep(jitter);
    }
  }
  throw new Error('Unreachable');
}

function isRetryable(error: any): boolean {
  return error.status >= 500 || error.code === 'ECONNRESET';
}

6. Data Access Patterns

Pattern What It Does When We Use It
RepositoryAbstracts data access behind a domain-oriented interfaceDomain-driven design projects. One per aggregate root.
Unit of WorkTracks changes and commits them in a single transactionComplex use cases modifying multiple aggregates atomically
CQRSSeparate models for reading and writingRead-heavy systems, complex queries, denormalized read stores
Data MapperMaps between domain objects and database rowsWhen domain model differs significantly from DB schema

7. Anti-Patterns to Avoid

Anti-Pattern Symptom Fix
God ObjectOne class does everything (UserManager with 3000 lines)Split by responsibility. UserAuth, UserProfile, UserBilling.
Premature AbstractionInterfaces and factories with only one implementationWait for the second use case. Three similar functions is better than one wrong abstraction.
Distributed MonolithMicroservices that must deploy togetherLoose coupling via events, not synchronous chains of API calls.
Callback HellNested callbacks 6 levels deepasync/await, Promise.all for parallel operations.
Primitive ObsessionUsing strings for emails, numbers for moneyValue objects: Email, Money, UserId. Validate at construction.
Our biggest lesson with patterns: The most dangerous patterns are the ones you apply without a problem to solve. Every pattern adds complexity. If you can't explain what problem a pattern solves in your specific codebase, you don't need it yet. We've removed more unnecessary patterns from codebases than we've added.

8. Frequently Asked Questions

Should I learn the original GoF design patterns?

Learn the concepts, not the specific class hierarchies. Strategy, Observer, Factory, Adapter, and Decorator translate directly to modern code. Singleton, Abstract Factory, and Visitor are rarely useful in dynamic languages. Focus on understanding the problem each pattern solves rather than memorizing the UML diagrams. The "Refactoring to Patterns" mindset is more valuable than applying patterns upfront.

How do I decide which pattern to use?

Start with the problem, not the pattern. "I need to support multiple payment providers" leads to Strategy. "I need to notify 5 services when an order is placed" leads to Observer/Event Bus. "I need to add logging without changing every function" leads to Decorator/Middleware. If you're searching for a place to use a pattern, you're doing it wrong. Patterns should emerge from real problems.

Are design patterns language-specific?

The concepts are universal, but the implementation varies dramatically. Python and JavaScript have first-class functions, so many GoF patterns (Strategy, Command, Template Method) simplify to passing functions. Go uses interfaces implicitly, making Adapter trivially easy. Rust's ownership model eliminates some patterns entirely. Learn patterns in the language you use — a Java implementation of Observer looks nothing like a JavaScript EventEmitter.

What's the most important pattern for modern backend development?

Dependency Injection — not as a framework, but as a practice. Constructor injection makes code testable, swappable, and explicit about its dependencies. Combined with the Repository pattern for data access and the Strategy pattern for business rules, you get a codebase that's easy to test and maintain. We consider these three patterns non-negotiable for any project beyond a prototype.

PI
Pillai Infotech Engineering Team

We use design patterns daily — but pragmatically. Our code reviews focus on "does this pattern solve a real problem here?" rather than "which pattern should we apply?" This guide reflects patterns we've validated across production systems in multiple languages and frameworks.

Related Articles

Clean Architecture: Building Maintainable Software → Domain-Driven Design (DDD): A Practical Guide → Technical Debt Management: Strategies That Actually Work →