Design Patterns Concepts

Last updated on
10 min read

Table of Contents

General Design Patterns

Creation Design Pattern (about class and object creation patterns)

Factory Pattern
  • It’s a creational design pattern.
  • It it used where we have a super class with multiple sub classes and based on the input we need to return one of the sub class. It makes it loosely coupled.
  • For example, consider notification service, it would have an interface or abstract class with method definitions. Then each type of notification would implement this super class (like sms, email etc). Then you would have a notificationFactory class which takes in type of notification as input and instantiates that specific sub class. Then another class that call this notification factory,
  • Examples, JDBC in Java or LINQ in C#. Web applications support connections to different type of databases. So, inorder to support multiple DB’s you can use factory design pattern. ORM tools like gorm also might be using similar logic to support multiple types of databases. Notification service which supports sending notification on email, sms etc.
Abstract Factory Method (yet to update)
Singleton Pattern
  • It’s a creation design pattern which allows you to create only a single instance of a class and provides only one global access to it. This class would have a a private variable which stores the instance of the object. A private constructor, so that it cannot be initiated and a public method which returns the instance of class.
  • It has three variations
    • Eager Instantiation (instance is created instantly)
    • Lazy Instantiation (instance is created when call is made for the first time)
    • Thread Safe Instantiation
  • Example, for printing, you would have a single instance of printer class. Database connection object. Single instance of apps running on desktop. One single instance of class to log all data from all over the application.
Prototype Method (yet to update)
Builder Pattern
  • A creational pattern that helps in building complex objects using simple objects in a step by step approach.
  • Example, PizzaBuilder construct Pizza using various toppings like crust, sauce, toppings.

Structural Design Pattern (about structure and composition of classes)

Adapter Pattern
  • It’s a structural design pattern.
  • It allows you to create a middle layer class that serves as a translator between your code and legacy class, 3rd party code etc
  • Example, laptop charger have different pins for Europe and US. An adapter to manage using XML and JSON service. It acts as an interface between two incompatible interfaces.
  • Match interfaces of different classes. Allows incompatible classes to interact with each other by converting the interface of one class into an interface expected by clients.
Bridge Method (yet to update)
Composite Method (yet to update)
Decorator Pattern
  • Structural Pattern
  • Allows you to add a new functionality to existing object without modifying existing structure.
  • Example, Higher order component in react like Connect function. Wrapper object around request handler which verify and authenticate the user requests
  • Composition (HAS-A)
Facade Pattern
  • Structural Pattern
  • Provides a simplified interface to a library or framework or any other complex set of libraries.
  • Video Converter, Video Encoding, Shopping on e-commerce sites.
Flyweight Method (yet to update)
Proxy Method (yet to update)

Behavioral Design Pattern (about communication between classes)

Chain of Responsibility Pattern
  • Behavioral Pattern
  • Chain Of Responsibility
  • Examples, Event capturing and bubbling. Pipes using in linux. Logging with different severity.
Command Method (yet to update)
Interpreter Method (yet to update)
Mediator Method (yet to update)
Memento Method (yet to update)
Observer Pattern
  • Behavioral Pattern
  • Defines a subscription mechanism to notify multiple objects about any events that happen to the object they are observing.
  • Example, Event handlers, react with redux where if store data is changes then components will re-render. Webhooks.
  • This pattern is like newspaper subscription. If you have subscribed you get updates whenever there is a new newspaper. If you unsubscribe you won’t receive it.
  • This pattern has subject and observers
  • Observers get the update when there is a new notification from the subject.
  • Observers need not continuously request subject for any notification. Whenever there are updates the subject sends notification to all its observers
  • All observers must implement a common observer interface. This will help the subject to send the notifications to all its observers.
State Method (yet to update)
Strategy Pattern
  • Behavioral Pattern
  • The class behavior or the algorithm can be changed at runtime.
  • Example, Collections.sort(). We can pass a comparator function which is an algorithm sent in runtime which allows you to define the behavior of the sorting.
Visitor Method (yet to update)
Iterator Pattern
  • Behavioral Pattern
  • Lets you traverse through a collection without exposing its underlying representation.
  • Example, Cursors in DB

Distributed Design Patterns

Saga Pattern

  • In distributed systems, a single business request often requires coordination across multiple microservices.
  • For example, for a single request like ordering a product, there are multiple steps involved. Such as payment processing, inventory updates, shipping etc. The distributed nature makes achieving ACID compliance difficult since all the individual services will have databases of their own and operate independently.
  • This design pattern helps in managing transaction updates across multiple services, by breaking it down into small local transactional updates called saga steps. Once a step is completed the next step in the sequence is triggered. If any of the steps in the sequence fail, compensating updates are done to undo the changes made by previous steps.
  • The pattern ensures eventual consistency rather than strict atomicity across services.
  • Types
    • Choreography
      • In this approach when a request is made, the first service in the sequence is triggered. Once it is completed it publishes an event. Other services listening for this event, perform their respective transaction and publishes an event. This chain of sequence continues until the saga is complete. If a service fails, a compensating event is published to trigger compensating transactions in previous services.
      • Example: Service A performs its task and emits an event. Service B listens to the event, does work, emits another event. Service C repeats the same thing. This repeats until all the steps in the sequence are done.
      • Pros - Good for simple workflows.
      • Cons - Hard to manage and debug issues. Tracing is difficult
    • Orchestration
      • In this approach an orchestrator coordinates the saga steps. The orchestrator tells each step when to execute its local transaction. If any local transaction fails, the orchestrator then invokes compensating transactions to roll back previous steps.
      • Example: Orchestrator sends a command to Service A. On successful completion, orchestrator sends a command to Service B. And this process continues until all steps are done. If any of the command fails, then a a compensating command is triggered to reverse the previous operations.
      • Pros - Easy to manage, monitor and trace.
      • Cons - Cons – Centralized control introduces a potential single point of failure.

CQRS (Command Query Responsibility Segregation) Pattern

  • It separates the read and write operations of a system into different models:
    • Command: Operations that change state (create, update, delete).
    • Query: Operations that read data (fetch, list).
  • The write model (command side) is responsible for handling requests that modify data, while the read model (query side) is optimized for retrieving data.
  • Benefits:
    • Allows independent scaling of read and write workloads.
    • Enables use of different data stores or schemas for reads and writes. For example, the write side may use a normalized relational database optimized for transactional integrity, while the read side can use a denormalized NoSQL database or cache for fast query performance. This flexibility allows each side to be tailored for its specific workload, improving scalability and responsiveness.
    • Facilitates event sourcing, where state changes are stored as a sequence of events.
  • Drawbacks:
    • Increased complexity due to maintaining separate models and synchronization.
    • Eventual consistency between read and write sides.
  • Typical use cases:
    • Systems with high read/write load imbalance.
    • Applications requiring audit trails or event sourcing.
    • Complex domains where read and write logic differ significantly.
  • Example:
    • In an e-commerce system, order creation (command) and order history retrieval (query) can be handled by separate models and databases.

Event Sourcing Pattern

  • Event Sourcing is a pattern where the state of an application is determined by a sequence of events rather than just the current state.

  • Instead of storing just the current state, all changes to application state are stored as a sequence of events in the order they occurred.

  • The current state can be recreated by replaying these events from the beginning.

  • Core concepts:

    • Events: Immutable records of something that happened in the system.
    • Event Store: Database that stores the complete sequence of events.
    • Event Stream: Sequence of events for a particular entity or aggregate.
    • Replay: Process of reconstructing state by processing historical events.
  • Benefits:

    • Complete audit trail and history of all changes.
    • Ability to reconstruct past states of the system.
    • Easy to implement temporal queries (what was the state at time X?).
    • Natural fit for event-driven architectures.
    • Helps in debugging by providing detailed history.
  • Challenges:

    • Event schema evolution (handling changes to event structure over time).
    • Performance considerations for systems with long event histories.
    • Learning curve and complexity in implementation.
    • Eventually consistent read models.
  • Common Use Cases:

    • Financial systems requiring audit trails.
    • Systems needing point-in-time reconstruction.
    • Applications with complex domain logic where history is important.
  • Example:

    • Banking system storing all transactions as events rather than just account balances. Each deposit, withdrawal, or transfer is stored as an event. Current balance is calculated by replaying all events.

Circuit Breaker

  • Software systems make remote calls to other systems to get data. If there’s a supplier of data that is not responding or down, then the client will continuously make call to the supplier requesting for the data. The basic idea is simple, you wrap protected function in circuit breaker object which monitors for failures. Once circuit breaker reaches a certain threshold, it trips and all further calls will return with an error. Also you want some alert to be generated for monitoring and observability to alert the concerned teams that the circuit breaker had tripped. If it trips you can have fallback logic or return default values to build better fault tolerant systems.

  • It has three states

    • Closed - normal condition
    • Open - error condition
    • Half open - Only limited requests are allowed and if things are good, then it is updated to closed normal condition

Bulkhead Pattern (yet to update)

Aggregator (yet to update)

  • Combine data from multiple services and display it in the application. It’s based on DRY principle

API Gateway (yet to update)

  • It acts as a entry point to forward the client requests to appropriate micro services. See the below block diagram.

Chained or Chain of Responsibility (yet to update)

  • Output of one service forms an input to another service. It’s slow, it’s not not recommended for the end user API’s. Kind of like synchronous messaging.

Asynchronous Messaging (yet to update)

  • All services communicate with each other, but they do not have to communicate sequentially.

Decomposition (yet to update)

  • Decompose the application into multiple services based on sub-domains of application, business capability

Two Phase Commit (yet to update)

Sidecar pattern (yet to update)

Leader Election (yet to update)

Retry pattern (yet to update)

Others

Dependency Injection

  • Dependency injection (DI) is a software design pattern widely used in object-oriented programming to improve code modularity and testability. It allows a class to receive its dependencies from an external source rather than creating them internally, thereby promoting loose coupling between classes.
// Without dependency injection
import { Engine } from './Engine';

class Car {
    private engine: Engine;

    public constructor () {
        this.engine = new Engine();
    }

    public startEngine(): void {
        this.engine.fireCylinders();
    }
}
// With Dependency Injection
import { Engine } from './Engine';

class Car {
    private engine: Engine;

    public constructor (engine: Engine) {
        this.engine = engine;
    }

    public startEngine(): void {
        this.engine.fireCylinders();
    }
}

So, what is the difference between the two. Instead of you instantiating the Engine class, you are passing the dependency i.e. Engine instance to the Car. This is dependency injection

References