Strategy Design Pattern

SOFTWARE DEVELOPMENT
STARTUPS

Designing a code architecture that can adapt to constant changes can be challenging. The Strategy Design Pattern can help you to switch service providers on the fly while keeping your core code untouched.

Featured image
April 23rd, 2024
4 min read

In the wild environment for startups where you need to build the rocket as you fly, you need to get the most valuable possible out of every dollar spent. Sometimes this means changing from one service provider to another. Whether it's a messaging queue like RabbitMQ to AmazonSQS or an email delivery service like SendGrid to AmazonSES, transitioning between providers can be a time-consuming task and therefore, expensive; especially if your code is not well structured.

This is where the Strategy Design Pattern comes in as a valuable tool. The Strategy Design Pattern allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. In simpler terms, it lets you swap out the specific service implementation without affecting the core functionality of your application.

So how does the Strategy Design Pattern help startups?

  • Reduced code changes: By isolating the service logic into separate strategies, switching providers only requires modifying the specific strategy implementation. The core application code remains untouched, minimizing the risk of bugs and regressions.
  • Improved maintainability: A well-designed strategy pattern promotes cleaner code by separating concerns. This makes your codebase easier to understand, maintain, and test, especially as your team grows.
  • Flexibility for future growth: The Strategy Design Pattern allows you to easily introduce new service providers in the future. This keeps your options open as your needs evolve.

Ok so this sounds awesome, but how can I actually implement it?

Here’s an example of how to implement Strategy Design Pattern using NodeJs and TypeScript, with a simple example. Imagine an application that sends notifications using a messaging queue. We want to be able to switch between RabbitMQ and Amazon SQS seamlessly.

1 2 3 interface MessagingStrategy { sendMessage(queue: string, message: string): Promise<void>; }

This interface defines the contract for our messaging strategies. Both RabbitMQ and SQS implementations will need to provide a sendMessage method that takes a queue name as string, a message string and returns a promise.

RabbitMQ Strategy

1 2 3 4 5 6 class RabbitMQStrategy implements MessagingStrategy { async sendMessage(queue: string, message: string): Promise<void> { // Code to connect to RabbitMQ and send message console.log(`Sending message to RabbitMQ: queue - ${queue} message - ${message}`); } }

This concrete strategy implements the MessagingStrategy interface and provides the specific logic for sending messages to RabbitMQ.

AmazonSQS Strategy

1 2 3 4 5 6 class SQSStrategy implements MessagingStrategy { async sendMessage(queue: string, message: string): Promise<void> { // Code to connect to Amazon SQS and send message console.log(`Sending message to SQS: queue - ${queue} message - ${message}`); } }

Similarly, this concrete strategy handles sending messages through Amazon SQS.

Using the Strategy

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class NotificationService { private messagingStrategy: MessagingStrategy; constructor(strategy: string) { switch (strategy) { case 'rabbitmq': this.messagingStrategy = new RabbitMQStrategy(); case 'sqs': default: this.messagingStrategy = new SQSStrategy(); } } async sendNotification(queue: string, message: string): Promise<void> { await this.messagingStrategy.sendMessage(queue, message); } } const message = "This is a test notification"; const queue = "etl"; // Choose the desired strategy const strategy = "rabbitmq"; // Or use sqs const notificationService = new NotificationService(strategy); notificationService.sendNotification(queue, message);

This is the output 

1 "Sending message to RabbitMQ: queue - etl message - This is a test notification" 

Things to bear in mind

  • Open for extension, closed for modifications: By following the O-principle from SOLID principles, you ensure the seamless integration of a new service provider or strategy. You can achieve this by keeping the signature of the methods generic. 
  • Testing: With multiple strategies, thorough testing becomes even more important. Ensure each strategy functions as expected and integrates seamlessly with the core application. Consider using dependency injection or mocking frameworks to facilitate easier testing.
  • Over-Engineering: Don't overcomplicate your application with the Strategy Design Pattern. Use it strategically when switching providers offers significant value and future flexibility. For simpler scenarios, a more direct approach might be preferable.
  • Documentation: Proper documentation is crucial for understanding how the Strategy Pattern is implemented within your code. This helps new developers and future maintainers understand the decision points and logic behind choosing specific strategies.

Featured image
By Jonatan Juarez
Co-Founder