June 11, 2026
python api integration

Design Patterns I use in External API Integration in Python

When we are building systems that interact with external services like payment gateways, cloud storages, or notification systems, I think it’s essential to support different environments like dev, test, and prod environments. I believe that Every external resource integration must have a dummy driver in the project. Because it gives us the flexibility to swap between a real implementation for production and a dummy one for development and test environment. This way we can make sure we don’t have to expose external service credentials to every developer on the team to work with it, which is not reasonable and secure. This not only protects us from unexpected costs (especially when using paid services), but also shields our development from rate limits or downtime issues. Also, considering that test environments should always be fully isolated and free from dependencies on live external systems, this approach helps enforce that principle.

In this post, I’ll explain how I implement drivers for external services to create a clean, flexible, and testable integration layer. The goal is to isolate third-party dependencies from the core application logic, support multiple environments (like development, testing, and production), and make it easy to switch between real and mock implementations without changing any business code. Here are the three key design patterns I use to bring this all together:

  • Strategy Pattern
  • Factory Pattern
  • Singleton Pattern
  • Retry with Backoff Pattern

Assume we need to talks to a third-party weather API in our project to get weather information. Here’s what we want:

  • In prod, fetch real weather from an external API.
  • In dev or test, return fake (but valid) weather data.
  • Ensure the weather client is initialized once and reused across the system.
  • Keep the code loosely coupled and easy to extend.

Diagram 1 illustrates the final architecture of the weather API integration we want to develop. In this design, the Weather Client creates a driver during its initialization using the Weather driver factory, based on the provided environment variable WEATHER_PROVIDER. It then delegates the actual weather retrieval to the driver’s get_weather() method, which is called within client’s get_weather() method.

Diagram 1. architecture of the weather API integration

Let’s dive into implementation.

1. Strategy Pattern For Swappable Drivers

The Strategy Pattern is all about defining a family of interchangeable algorithms/drivers/components and making them swappable at runtime. However, in our case, the strategy (i.e., the weather driver) is determined by an environment variable and cannot be altered once the application is running. This gives us a environment-specific behavior.

Let’s write down our strategies. In our case, each “strategy” is a different driver for the weather service:

from abc import ABC, abstractmethod

class WeatherDriver(ABC):
    @abstractmethod
    def get_weather(self):
        pass

class RealWeatherDriver(WeatherDriver):
    def get_weather(self):
        return "Real Weather from API"

class DummyWeatherDriver(WeatherDriver):
    def get_weather(self):
        return "Fake Weather Data"

With this, our client (which we will implement in section 3) doesn’t care how the weather is fetched. It just uses a WeatherDriver. That’s the power of strategy.

2. Factory Pattern For Centralized Creation Logic

To create a driver instance in the weather client we need a factory. The Factory Pattern helps us centralize the creation of objects, especially when that creation involves some logic, or we have more than one type of object.

Here, we decide which driver to create based on the given provider :

class WeatherDriverFactory:
    @staticmethod
    def create_driver(provider: str) -> WeatherDriver:
        if provider == "real":
            return RealWeatherDriver()
        elif provider == "dummy":
            return DummyWeatherDriver()
        else:
            raise ValueError(f"Unknown provider: {provider}")

This factory prevents redundant decision-making across rest of our app. You want a weather driver? Ask the factory.

3. Singleton Pattern for a Single, Shared Client Instance

This is our Config class which reads the strategy from the .env file. If the value is not found in the .env file , it will use the default value:

from decouple import config

class AppConfig:
    # The config reads the value from the .env file.
    # If the value is not found in the .env file, it will use the default value.
    WEATHER_PROVIDER = config('WEATHER_PROVIDER', default='dummy')

With all the pieces ready, it’s time to build the WeatherClient , a clean abstraction that handles retrieving weather data without worrying about which provider/driver is being used behind the scenes. I think external service integration is among the use cases where we should only have one instance of the client class in whole system. I used a lazy singleton with a metaclass. It’s only created when needed, and will be reused in the system after that:

from config import AppConfig

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class WeatherClient(metaclass=SingletonMeta):
    def __init__(self):
        provider = AppConfig.WEATHER_PROVIDER
        self.driver = WeatherDriverFactory.create_driver(provider)

    def get_weather(self):
        print(self.driver.get_weather())

Usage:


client = WeatherClient()
client.get_weather()

The client is ready to use. No matter how many times you call WeatherClient(), it will always return the same instance. Note that the SingletonMeta metaclass can be reused to create any other singleton class across the project. This approach keeps our code base clean and DRY, eliminating the need to repeat boilerplate logic for every singleton implementation.

4. Retry with Backoff Pattern

One other important pattern that increases the resilience and reliability of the system is the retry pattern with backoff strategy, which helps manage retries after failures. Instead of retrying requests immediately and repeatedly, which could overwhelm the system or external services, the backoff pattern introduces a delay between retries. This delay can be fixed, incremental, or exponential, allowing the system to recover gracefully while avoiding unnecessary load. By applying this pattern, systems become more fault-tolerant, reduce the risk of cascading failures, and improve overall stability under stress.

Here’s a simple example showing how to use exponential backoff using backoff package:

import backoff

class WeatherClient(metaclass=SingletonMeta):
    def __init__(self):
        provider = AppConfig.WEATHER_PROVIDER
        self.driver = WeatherDriverFactory.create_driver(provider)

    @backoff.on_exception(
         backoff.expo,                               # exponential backoff
         requests.exceptions.RequestException,       # retry on these exceptions
         max_time=30,                                # give up after 30 seconds total
         max_tries=5                                 # or stop after 5 attempts
     )
    def get_weather(self):
        print(self.driver.get_weather())

💡 You can find the complete source code on GitHub: python_design_patterns

Final Word

Working with external services is part of almost every modern application. But if we don’t design it with best practices, we end up locked into brittle systems that are hard to test and harder to scale.

Using this combo of patterns Strategy for behavior, Factory for creation, and Singleton for reuse makes my projects more maintainable, developer-freindly and test-friendly. In addition, if in some point of time in the future we decide to use different provider, it won’t be difficult to switch.

I’d love to hear your thoughts on this approach. Feel free to share any feedback or alternative patterns you prefer to use in your projects.

One thought on “Design Patterns I use in External API Integration in Python

  1. Hey there! Love your take on using design patterns for external API integration. It’s super helpful for keeping things organized. I’m curious though, are there situations where you think using these patterns might be overkill? Sometimes it feels like they can complicate things for simpler projects. Have you ever run into that? Would love to hear your thoughts! 😊

Leave a Reply

Your email address will not be published. Required fields are marked *