The factory pattern is one of the foundational “Gang Of Four” design patterns. It’s so fundamental that you probably use it, perhaps without doing so intentionally. At its core, the factory pattern abstracts away the act of creating an object from an object’s constructor. This benefit of this separation is often emphasized in object-oriented terms, especially with respect to a factory’s ability to create subclasses of an object without the caller’s involvement.

At Globality, we’ve found that the benefit of the factory pattern has much more to do with formalizing the right way to create an object in terms of microservice configuration and dependencies. In a system with tens – or even hundreds – of microservices, a great deal of developer productivity comes from the consistency and simplicity enabled by the factory pattern, especially for developers working in microservices they’ve never seen before.

The Factory Pattern, In Python

Our preferred way to implement the factory pattern in Python uses @classmethod:

class MyClient:

    def __init__(self, url):
        self.url = url

    def load(self):
        """
        This function does something interesting.

        """
        return open(self.url)

    @classmethod
    def from_dict(cls, dct):
        """
        Our factory constructs the class from a dictionary.

        """
        return cls(url=dct["url"])

This style works great and has its place. It’s plainly better than putting this business logic into the constructor:

def __init__(self, dct):
    self.url = dct["url"]

because the constructor is a singleton and baking in the factory logic precludes any sort of alternative usage, e.g. you cannot also do:

my_client = MyClient("http://example.com")

Reusing Factories

Once you scale from one code base to many – as with microservices – it’s desirable to reuse components and their factories. For example, in our product, we wish to have consistent initialization of:

  • Python logging, including logging levels and connections to a log aggregation system
  • SQLAlchemy engines, including connection URLs and security configurations
  • OpenAPI clients, including service URLs and authentication credentials

Working backwards, microservice code that uses these components wants as little to do with this initialization as possible, leading to something along the lines of Inversion of Control:

class MyComponent:

    def __init__(self, my_client):
        self.my_client = my_client

    def do_something(self):
        """
        This function does something interesting using an instance of `MyClient`

        """
        print(self.my_client.load())

    @classmethod
    def create_my_component(cls, framework):
        """
        Our factory constructs the class from an inversion of control framework.

        """
        return cls(my_client=framework.my_client)

Of course, this design requires that the factory for my_client be registered a priori with the framework:

@framework.register
def create_my_client(url):
    return MyClient(url=url)

And by symmetry, the factory for MyComponent ought to work exactly the same way since other parts of the service are just as likely to want to reference it within their own factories:

@framework.register
def create_my_component(framework):
    return MyComponent(my_client=framework.my_client)

Allowing further references via:

framework.my_component.do_something()

Factory Signatures

The final challenge is to put these factories on the same footing by having consistent signatures. In this case, the function argument needs to provide access to several things:

  • External configuration (e.g. url)
  • Other components (e.g. framework.my_client)
  • Service metadata (e.g. the name of the service)

We can model these needs as:

@framework.register
def create_my_client(config, framework, metadata):
    return MyClient(url=config.my_client.url)

@framework.register
def create_my_component(config, framework, metadata):
    return MyComponent(my_client=framework.my_client)

Alternatively, to reduce argument verbosity:

@framework.register
def create_my_client(framework):
    return MyClient(url=framework.config.my_client.url)

@framework.register
def create_my_component(framework):
    return MyComponent(my_client=framework.my_client)

The Microcosm Framework

The preceding examples motivate the open-source microcosm library, which provides a framework along the lines of what’s described above, including:

  • Loading configuration data from various sources.
  • Registering factories via decorators and entry points.
  • Lazy-initialization and caching of factory-generated components.

With microcosm, the resulting code looks much the same:

from microcosm.api import binding

@binding("my_client")
def create_my_client(graph):
    return MyClient(url=graph.config.my_client.url)

@binding("my_component")
def create_my_component(graph):
    return MyComponent(my_client=graph.my_client)

Our Experience

Globality uses microcosm across more than fifty Python services. Along the way, we’ve hooked up configuration loading both to environment variables passed into our containers and to AWS Secrets Manager for sensitive values. We’ve written core integrations as reusable, mostly open-source, libraries with factory functions declared as entry points. By taking advantage of decoupling via inversion of control and by factoring out reusable code into upgradeable libraries, we’ve seen better code quality and improved maintainability.

Interested? Take a look.