ArchitectureLearning

Architecture vs. Design Patterns: The Scale

TT
TopicTrick Team
Architecture vs. Design Patterns: The Scale

Architecture vs. Design Patterns: Understanding the Scale

Software engineers use "pattern" and "architecture" interchangeably, but they operate at completely different levels of abstraction. Confusing them leads to applying the wrong tool: using a GoF design pattern where an architectural decision is needed, or re-architecting an entire system to solve a problem that a three-line Strategy pattern would fix.

This guide clarifies the distinction with concrete examples at each level, explains the hierarchy from architectural style down to individual code patterns, and gives you a framework for deciding which level of thinking a given problem requires.


The Core Distinction

ArchitectureDesign Patterns
ScopeWhole system or subsystemSingle class or component
Change costMonths to yearsHours to days
Decision typeTechnology, topology, contractsCode structure, object relationships
Who decidesArchitect, senior leadershipIndividual developer, tech lead
ExamplesMicroservices, Event-Driven, ServerlessStrategy, Observer, Factory, Singleton
Documented inArchitecture Decision RecordsCode comments, design docs

Architecture answers: "How are the major components of the system organised and how do they communicate?"

Design patterns answer: "How should the objects and classes within a component be structured to solve this recurring problem?"


The Three Levels of Structure

Think of software structure as a hierarchy with three distinct levels:

text
Level 1 — Architectural Style
  Examples: Microservices, Event-Driven, Serverless, Layered, Hexagonal
  Scope: The entire system
  Defines: Component boundaries, communication protocols, deployment units

Level 2 — Architectural Pattern (within a component)
  Examples: MVC, CQRS, Repository, Clean Architecture layers
  Scope: A single service or application
  Defines: Internal module structure, data flow within the service

Level 3 — Design Pattern (within a class or module)
  Examples: Strategy, Observer, Factory, Decorator, Singleton
  Scope: One or a few classes
  Defines: Object relationships, algorithm encapsulation, object creation

A change at Level 1 (moving from monolith to microservices) reshapes the entire engineering organisation. A change at Level 3 (replacing a switch statement with a Strategy pattern) affects one file.


Level 1: Architectural Styles

An architectural style defines the macro-structure of the system — how independent pieces are divided, deployed, and how they communicate.

Microservices

text
User Service ──REST──► Order Service ──REST──► Payment Service
     │                      │                       │
  users-db              orders-db               payments-db

Decision criteria: team size, independent scaling, separate deployment lifecycles.

Event-Driven

text
Order Service ──publish──► [order.placed] ──► Inventory Service
                                          ──► Notification Service
                                          ──► Analytics Service

Decision criteria: team autonomy, temporal decoupling, fan-out processing.

Hexagonal (Ports and Adapters)

text
        HTTP Adapter ──►
        CLI Adapter  ──► Application Core ──► Database Adapter
        SQS Adapter  ──►                  ──► Email Adapter

Decision criteria: testability, infrastructure independence, domain purity.

These choices are architectural because reversing them is expensive: migrating from a monolith to microservices takes months. Migrating from a REST-based system to event-driven requires redesigning inter-service contracts.


Level 2: Architectural Patterns Within a Service

Inside a service, architectural patterns define how code is organised into layers, modules, or flows.

MVC (Model-View-Controller)

text
HTTP Request
     │
     â–¼
Controller ──reads/writes──► Model (Business Logic + Data)
     │
     â–¼
   View (Response)
typescript
// Controller layer — handles HTTP
class OrdersController {
  constructor(private ordersService: OrdersService) {}

  async createOrder(req: Request, res: Response) {
    const order = await this.ordersService.createOrder(req.body);
    res.status(201).json(order);
  }
}

// Service layer (Model) — handles business logic
class OrdersService {
  constructor(private ordersRepo: OrdersRepository) {}

  async createOrder(dto: CreateOrderDto): Promise<Order> {
    if (dto.items.length === 0) throw new Error('Order must have items');
    return this.ordersRepo.save(new Order(dto));
  }
}

CQRS (Command Query Responsibility Segregation)

text
Commands (writes) ──► Command Handlers ──► Write Store (normalised)
Queries (reads)   ──► Query Handlers  ──► Read Store (denormalised projections)

CQRS is an architectural pattern within a service. It is not a full system architecture — it describes how a service internally separates its read and write models.


Level 3: Design Patterns (Gang of Four)

Design patterns solve recurring object-level problems. The 23 GoF patterns fall into three categories:

Creational Patterns (how objects are created)

Factory Method — decouple object creation from the code that uses the object:

typescript
// Without Factory: business logic knows about concrete classes
const notifier = new EmailNotifier();  // Tightly coupled

// With Factory: business logic uses an abstraction
interface Notifier {
  send(message: string, recipient: string): Promise<void>;
}

class NotifierFactory {
  static create(type: 'email' | 'sms' | 'slack'): Notifier {
    switch (type) {
      case 'email': return new EmailNotifier();
      case 'sms': return new SMSNotifier();
      case 'slack': return new SlackNotifier();
    }
  }
}

const notifier = NotifierFactory.create(config.notificationType);
await notifier.send('Order confirmed', user.contact);
// Business logic doesn't know or care which notifier is used

Structural Patterns (how objects are composed)

Decorator — add behaviour to an object without modifying it:

typescript
interface Logger {
  log(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(message);
  }
}

// Adds timestamps without touching ConsoleLogger
class TimestampLogger implements Logger {
  constructor(private inner: Logger) {}

  log(message: string) {
    this.inner.log(`[${new Date().toISOString()}] ${message}`);
  }
}

// Adds log levels without touching either previous class
class LevelLogger implements Logger {
  constructor(private inner: Logger, private level: string) {}

  log(message: string) {
    this.inner.log(`[${this.level}] ${message}`);
  }
}

// Compose decorators
const logger = new LevelLogger(new TimestampLogger(new ConsoleLogger()), 'INFO');
logger.log('Order created');
// Output: [INFO] [2026-04-18T10:30:00Z] Order created

Behavioural Patterns (how objects communicate)

Strategy — encapsulate interchangeable algorithms:

typescript
interface PaymentStrategy {
  charge(amount: number, customerId: string): Promise<string>; // returns transactionId
}

class StripeStrategy implements PaymentStrategy {
  async charge(amount: number, customerId: string) {
    const charge = await stripe.charges.create({ amount, customer: customerId });
    return charge.id;
  }
}

class PayPalStrategy implements PaymentStrategy {
  async charge(amount: number, customerId: string) {
    const payment = await paypal.createPayment({ amount, payer: customerId });
    return payment.id;
  }
}

class PaymentService {
  constructor(private strategy: PaymentStrategy) {}

  async processPayment(amount: number, customerId: string) {
    return this.strategy.charge(amount, customerId);
  }
}

// Switch strategies without changing PaymentService
const service = new PaymentService(new StripeStrategy());
const txId = await service.processPayment(9900, 'cus_123');

Observer — notify multiple subscribers when state changes:

typescript
type EventMap = {
  'order:created': { orderId: string; userId: string; amount: number };
  'order:shipped': { orderId: string; trackingNumber: string };
};

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

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

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

const bus = new EventBus();

// Subscribers register independently — publisher does not know about them
bus.on('order:created', ({ orderId }) => sendConfirmationEmail(orderId));
bus.on('order:created', ({ orderId }) => reserveInventory(orderId));
bus.on('order:created', ({ userId, amount }) => recordAnalytics(userId, amount));

// Publisher fires the event — does not know who handles it
bus.emit('order:created', { orderId: '123', userId: 'u456', amount: 9900 });

When to Apply Each Level

Signals that you need an architectural decision

  • "This component needs to be independently deployed"
  • "This part of the system needs to scale separately"
  • "Two teams need to work on this without coordinating every release"
  • "This service needs to access a database the other services must not touch"
  • "Changing this now will require a 3-month migration"

Signals that you need a design pattern

  • "I have a switch statement with 8 cases and adding a new case is risky" → Strategy
  • "Multiple parts of the code need to react when this object changes" → Observer
  • "I need to add logging/caching/auth to these 15 methods without modifying each one" → Decorator
  • "I'm new-ing concrete classes all over and it's hard to swap implementations" → Factory

The golden rule

Reach for the simplest solution first. Three if-statements that handle three payment types are simpler than the Strategy pattern with three classes and an interface. Apply the pattern when the if-statement version is genuinely more painful than the pattern version — because the problem has grown, not because the pattern exists.


Frequently Asked Questions

Q: Is Clean Architecture a design pattern or an architectural style?

Clean Architecture (Robert C. Martin) is an architectural style — it defines the internal organisation of a service into concentric layers (Entities, Use Cases, Interface Adapters, Frameworks & Drivers) with strict dependency rules. It operates at Level 2 (architectural pattern within a service), not at the design pattern level. Clean Architecture often uses design patterns internally (Repository pattern for data access, Factory for object creation) but is itself a structural framework for a whole application.

Q: When should I use the Singleton pattern?

The Singleton pattern restricts a class to one instance. It is appropriate for: logger instances, configuration managers, and connection pools where exactly one instance is required for correct behaviour. It is overused when applied to services that are "effectively singletons" in your IoC container — your dependency injection framework already manages this without you needing the Singleton pattern. In modern TypeScript/Node.js, module-level exports achieve the same result without the static state: export const redis = createClient(...).

Q: Which GoF patterns are most important to learn first?

In order of practical frequency in modern server-side TypeScript/Java/Go code: Repository (data access abstraction), Strategy (algorithm variants), Factory (object creation), Observer/Event (decoupled notifications), and Decorator (cross-cutting concerns). Learn these five well before studying the full GoF catalog. The patterns you will rarely need (Flyweight, Memento, Mediator) are worth knowing conceptually but are not daily-use tools.

Q: Can a microservices architecture use a monolithic design pattern?

Yes. Each microservice is a separate deployment unit, but internally each service might be a simple monolith with layered architecture using classical design patterns. The architectural choice (microservices) is orthogonal to the design choice (how code within each service is structured). A microservice can use MVC internally, apply the Strategy pattern for payment processing, and use the Repository pattern for data access — the pattern level operates entirely within the service boundary.


Key Takeaway

Architecture and design patterns solve different problems at different scales. Architectural decisions — microservices, event-driven, serverless — define how the system is divided and how major components communicate. These decisions are expensive to reverse. Design patterns — Strategy, Observer, Factory, Decorator — solve recurring object-level problems within a component. These are cheap to change. The skill is knowing which level a problem belongs to: if the problem is about team autonomy, independent scaling, or inter-service contracts, it is architectural. If the problem is about object relationships, algorithm variation, or object creation, it is a design pattern problem. Apply the right tool at the right scale.

Read next: Client-Server Architecture: The Foundation of the Web →


Part of the Software Architecture Hub — engineering the scale.