5 Must-Know Design Patterns for Building Scalable FastAPI Applications

The Primadonna
10 min readMar 10, 2023

--

Photo by Obed Hernández on Unsplash

Design patterns are reusable solutions to commonly occurring problems in software development. By using these patterns, developers can save time and effort by not having to solve the same problems repeatedly.

Design Patterns for FastAPI

we will cover several design patterns that can be applied to FastAPI. These patterns include:

  • Singleton Pattern: ensures that only one instance of a class exists and provides a global point of access to it.
  • Factory Pattern: provides a way to create objects without specifying the exact class of object that will be created.
  • Observer Pattern: defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
  • Strategy Pattern: allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
  • Decorator Pattern: allows you to add behavior to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.

Singleton Pattern

The Singleton Pattern is a creational design pattern that ensures that only one instance of a class exists and provides a global point of access to it. This pattern is useful when you need to ensure that there is only one instance of a class in your application, such as a database connection or a logging system.

To implement the Singleton Pattern:

class Singleton:
_instance = None

def __new__(cls):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance

you can define a private constructor and a static method that creates and returns the instance of the class. The static method should check if the instance has already been created, and if so, return it. If not, it should create a new instance and return it. The instance should be stored as a private static variable within the class

Here’s an example that might be used in the FastAPI

from fastapi import FastAPI, Depends
from pymongo import MongoClient
from typing import Optional

class Database:
instance = None

def __new__(cls):
if cls.instance is None:
cls.instance = super().__new__(cls)
cls.instance.client = MongoClient("mongodb://localhost:27017/")
cls.instance.db = cls.instance.client["mydatabase"]
return cls.instance

def get_db() -> Optional[Database]:
return Database()

app = FastAPI()

@app.get("/")
def read_root(db: Optional[Database] = Depends(get_db)):
if db:
result = db.db.my_collection.find_one()
return {"message": result}
else:
return {"message": "Failed to connect to database."}

In this example, we define a Database class that creates a database connection using the PyMongo library. The Database class uses the Singleton pattern to ensure that only one instance of the class is created during the lifetime of the application.

We then define a get_db function that returns an optional instance of the Database class. This function is used as a dependency in our FastAPI route handler by passing it as a parameter with the Depends function.

When the read_root function is called, the get_db dependency is resolved and an instance of the Database class is created if it doesn't already exist. The function then retrieves a single document from the my_collection collection in the database and returns its contents.

By using the Singleton pattern in this way, we can ensure that we only create a single database connection throughout the lifetime of our FastAPI application, even when using dependency injection. This can help to reduce resource usage and improve performance, particularly for applications that require frequent database access.

Note that while this example uses the PyMongo library and get_db function, you can use the Singleton pattern with any database library or dependency function in FastAPI.

Factory Pattern

The Factory Pattern is a creational design pattern that provides a way to create objects without specifying the exact class of object that will be created. This pattern is useful when you have a class hierarchy and you want to create objects of different classes based on some input, such as a user’s selection or a configuration parameter.

class ShapeFactory:
@staticmethod
def create_shape(shape_type):
if shape_type == "circle":
return Circle()
elif shape_type == "square":
return Square()
elif shape_type == "triangle":
return Triangle()
else:
raise ValueError("Invalid shape type")

To implement the Factory Pattern, you can define a factory method that creates and returns an instance of the appropriate class based on some input. This method should be static and defined in a separate factory class or in the base class of the class hierarchy.

Here’s an example of how you can use the Factory pattern in FastAPI to dynamically create objects based on user input

from fastapi import FastAPI, HTTPException

class PaymentProcessor:
def process_payment(self, amount: float):
pass

class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float):
return f"Processing credit card payment for {amount} dollars."

class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount: float):
return f"Processing PayPal payment for {amount} dollars."

class PaymentProcessorFactory:
@staticmethod
def create_processor(processor_type: str) -> PaymentProcessor:
if processor_type == "credit_card":
return CreditCardProcessor()
elif processor_type == "paypal":
return PayPalProcessor()
else:
raise HTTPException(status_code=400, detail="Invalid payment processor type.")

app = FastAPI()

@app.post("/process_payment/{processor_type}")
def process_payment(processor_type: str, amount: float):
processor = PaymentProcessorFactory.create_processor(processor_type)
return processor.process_payment(amount)

In this example, we define a PaymentProcessor abstract class that represents a payment processor, as well as two concrete implementations of the class: CreditCardProcessor and PayPalProcessor. We then define a PaymentProcessorFactory class that uses the Factory pattern to dynamically create instances of the CreditCardProcessor and PayPalProcessor classes based on user input.

In our FastAPI application, we define a route handler for processing payments that takes a processor_type parameter and an amount parameter. The process_payment function uses the PaymentProcessorFactory to create an instance of the appropriate payment processor based on the processor_type parameter, and then calls the process_payment method on the created instance to process the payment.

By using the Factory pattern in this way, we can dynamically create objects based on user input without tightly coupling the code to the concrete implementations of the objects. This can help to make the code more modular and easier to maintain over time.

Observer Pattern

The Observer Pattern is a behavioral design pattern that defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. This pattern is useful when you have a set of objects that need to be updated when some other object changes, such as a model-view-controller architecture.

class Subject:
def __init__(self):
self._observers = []

def attach(self, observer):
self._observers.append(observer)

def detach(self, observer):
self._observers.remove(observer)

def notify(self):
for observer in self._observers:
observer.update()

class Observer:
def update(self):
pass

To implement the Observer Pattern, you can define a subject class that maintains a list of its dependents and provides methods for adding and removing dependents. The subject class should also provide a method for notifying its dependents of a change. The dependents should implement an observer interface that defines a method for receiving updates from the subject.

Here’s an example of how you can use the Observer pattern in FastAPI to notify multiple subscribers of changes to a resource

from fastapi import FastAPI
from typing import List

class Subject:
def __init__(self):
self.observers = []

def register_observer(self, observer):
self.observers.append(observer)

def remove_observer(self, observer):
self.observers.remove(observer)

def notify_observers(self, message):
for observer in self.observers:
observer.update(message)

class Resource(Subject):
def __init__(self):
super().__init__()
self.data = []

def add_data(self, new_data):
self.data.append(new_data)
self.notify_observers(new_data)

class Subscriber:
def __init__(self, name):
self.name = name

def update(self, message):
print(f"{self.name} received message: {message}")

app = FastAPI()
resource = Resource()

@app.post("/add_data/{new_data}")
def add_data(new_data: str):
resource.add_data(new_data)
return {"message": "Data added."}

@app.post("/subscribe/{subscriber_name}")
def subscribe(subscriber_name: str):
subscriber = Subscriber(subscriber_name)
resource.register_observer(subscriber)
return {"message": f"{subscriber_name} subscribed."}

@app.post("/unsubscribe/{subscriber_name}")
def unsubscribe(subscriber_name: str):
subscriber = Subscriber(subscriber_name)
resource.remove_observer(subscriber)
return {"message": f"{subscriber_name} unsubscribed."}

In this example, we define a Subject abstract class that represents a subject that can be observed, as well as a Resource class that extends Subject and represents a resource that can be modified. We also define a Subscriber class that represents an observer of the resource.

Our FastAPI application defines three routes: /add_data, /subscribe, and /unsubscribe. When a POST request is made to the /add_data endpoint with a new data string, the add_data function in our FastAPI application adds the new data to the Resource and calls the notify_observers method to notify all subscribers of the change.

When a POST request is made to the /subscribe endpoint with a subscriber name string, the subscribe function creates a new Subscriber object with the provided name and registers it as an observer of the Resource.

When a POST request is made to the /unsubscribe endpoint with a subscriber name string, the unsubscribe function creates a new Subscriber object with the provided name and removes it as an observer of the Resource.

By using the Observer pattern in this way, we can decouple the subject and observer objects and allow multiple subscribers to receive updates about changes to the resource. This can help to improve the modularity and maintainability of the code over time.

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. This pattern is useful when you have a set of algorithms that perform similar tasks but differ in their implementation, and you want to be able to switch between them easily.

class Sort: 
def sort(self, data):
pass

class QuickSort(Sort):
def sort(self, data):
# Implementation of quicksort algorithm pass

class MergeSort(Sort):
def sort(self, data):
# Implementation of mergesort algorithm pass

class Client:
def init(self, sort_strategy):
self.sort_strategy = sort_strategy

def do_sort(self, data):
self.sort_strategy.sort(data)

To implement the Strategy Pattern, you can define an interface or abstract class that defines a method for performing the task, and then define a set of concrete classes that implement the interface or extend the abstract class. The client class should maintain a reference to an instance of the interface or abstract class and use it to perform the task.

Here’s an example of how you can use the Strategy pattern in FastAPI to encapsulate different algorithms and make them interchangeable

from fastapi import FastAPI
from typing import List

class SortStrategy:
def sort(self, data: List[int]) -> List[int]:
pass

class BubbleSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
n = len(data)
for i in range(n):
for j in range(n-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
return data

class QuickSort(SortStrategy):
def sort(self, data: List[int]) -> List[int]:
if len(data) <= 1:
return data
pivot = data[len(data)//2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)

class Sorter:
def __init__(self, strategy: SortStrategy):
self.strategy = strategy

def set_strategy(self, strategy: SortStrategy):
self.strategy = strategy

def sort(self, data: List[int]) -> List[int]:
return self.strategy.sort(data)

app = FastAPI()
sorter = Sorter(BubbleSort())

@app.post("/sort/{strategy}")
def sort_data(strategy: str, data: List[int]):
if strategy == "bubble":
sorter.set_strategy(BubbleSort())
elif strategy == "quick":
sorter.set_strategy(QuickSort())
else:
return {"message": "Invalid sorting strategy."}
sorted_data = sorter.sort(data)
return {"sorted_data": sorted_data}

In this example, we define a SortStrategy abstract class that represents a sorting strategy, as well as two concrete implementations of the class: BubbleSort and QuickSort. We then define a Sorter class that encapsulates a SortStrategy and provides a sort method that delegates sorting to the encapsulated strategy.

Our FastAPI application defines a single route: /sort. When a POST request is made to the /sort endpoint with a sorting strategy string and a list of integers to sort, the sort_data function in our FastAPI application creates a new Sorter object with the appropriate sorting strategy based on the strategy string parameter, and then calls the sort method on the Sorter object to sort the data.

By using the Strategy pattern in this way, we can encapsulate different sorting algorithms and make them interchangeable, allowing us to easily switch between different algorithms without changing the code that uses them. This can help to improve the modularity and maintainability of the code over time.

Decorator Pattern

The Decorator Pattern is a structural design pattern that allows you to add behavior to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. This pattern is useful when you have a set of objects that perform similar tasks but differ in their behavior, and you want to be able to add or remove behavior from an object without affecting the behavior of other objects from the same class.

class Component:
def operation(self):
pass

class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"

class Decorator(Component):
def __init__(self, component):
self._component = component

def operation(self):
return self._component.operation()

class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({self._component.operation()})"

class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({self._component.operation()})"

To implement the Decorator Pattern, you can define a base class or interface that defines the methods and properties of the objects. You can then define a set of concrete classes that implement the base class or interface and add behavior to the objects. Finally, you can define a set of decorator classes that wrap the concrete classes and add or remove behavior from the objects.

Here’s an example of how you can use the Decorator pattern in FastAPI to add functionality to an existing endpoint

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"message": "Hello, world!"}

def add_greeting(original_function):
def new_function():
return {"greeting": "Welcome to my FastAPI application!"} | original_function()
return new_function

@app.get("/decorated")
@add_greeting
def read_decorated():
return {"data": "This is some data."}

In this example, we define a basic FastAPI application with a route handler for the root path (/) that returns a simple message.

We then define a add_greeting function that takes an existing function as a parameter and returns a new function that adds a greeting message to the output of the original function. The new function uses the | operator to merge the output of the original function with the new greeting message.

We then define a new endpoint (/decorated) that is decorated with the @add_greeting decorator. This decorator adds the greeting message to the output of the read_decorated function, which returns some sample data.

When a GET request is made to the /decorated endpoint, the add_greeting function is called to add the greeting message to the output of the read_decorated function, and the resulting output is returned as the response.

By using the Decorator pattern in this way, we can add functionality to an existing endpoint without modifying the original function or the code that calls it. This can help to improve the modularity and maintainability of the code over time.

Conclusion

In this series, we have covered several design patterns that can be applied to FastAPI. the Singleton, Factory, Observer, Strategy, and Decorator Patterns are all useful design patterns that can be applied to FastAPI. By understanding these patterns and when to use them, you can write more modular, reusable, and maintainable code.

We hope that this series has been helpful in your journey to becoming a better FastAPI developer. Thanks for reading!

If you liked the post, please follow and give me a round of applause.

--

--

The Primadonna
The Primadonna

Written by The Primadonna

Experienced software engineer in Golang and Python. Sharing knowledge on software development in this blog.