A few practical examples of software design patterns we implemented

Jorge Arévalo
Landbot Engineering
6 min readSep 19, 2022

Here at Landbot, we love software design patterns. We love (building) software. We love design. And you can bet we love patterns.

Imagen de rawpixel.com en Freepik

We’d like to share a few software patterns we successfully implemented in our codebase. They may also fit in yours. Or, like us, you may just feel the satisfaction nice software solutions generate 😊.

Let’s start with a basic: separate the way we construct components from the way we use them, to decrease coupling.

Yes, we’re talking about the well-known design pattern Dependency Injection

Dependency injection

In several parts of our code, we reuse instances of useful components, like repositories (we will go with that later) or clients to connect with 3rd parties. And we wanted to split the abovementioned responsibilities:

  • Create the components
  • Use them where needed

And instead of coding this by ourselves, we reused the already existent and battle-tested Inject Python package.

There are 3 critical things we had to consider to work with Inject:

Configure injector

We use Django as our base framework. So, we think the right way to configure our injector is at Django app config time:

Configure Inject in the Django app

Let’s zoom in on configure_injector

Calling to configure_once

The configure_once function receives a callable with an argument of type Binder. And this is how it’s used:

Binding components to specific implementations

There are some subtleties with this binding step. You may need to create a new instance each time the component is needed, instead of using a single shared instance. That’s .bind_to_provider is for. But we don’t want to go deeper into these details now.

So, you need a Redis client, cool. Here, you define the binding between the component (Redis client) and its implementation (a single shared instance). This is the instance that will be injected where needed. And where is it needed?

Injecting autoparams

The component may be needed in a class implementation, to be available for all class members. In this situation, we use inject.autoparams decorator

Injecting autoparams

Now you can instantiate the class like this

_filter = ChannelUUIDFilter()

and the 2 required components, channel_repository and template_repository, will be automatically injected by the framework. Which also created them by following the defined strategy (.bind vs .bind_to_provider)

Injecting instances

The component may also be needed in a specific function or method. In those situations, we use inject.instance like this:

Injecting specific instances

We don’t care about who created TrackingSender, and how they did it. We can just use it here now.

Repository pattern

A repository is a component that encapsulates the logic to access a data source. Here in Landbot, we mainly use them to encapsulate ORM calls, as part of our general architecture strategy for layers separation (we don’t want to expose Django-specific calls).

We just make sure a repository implements a defined contract interface, which we define as an abstract Python class like this one:

Contract interface for a repository

And we implement it using Django-specific ORM queries, like these:

Repository implementation

We implement the same pattern to access external data sources. In that situation, we may need to inject other components into our repository implementation. We do it in the constructor.

Injecting dependencies to a repository implementation

Use Cases

If you’re worried about how to encapsulate your functional requirements, separating them from pure technical ones, you know the use case concept for sure.

A use case allows a role (an external actor) to interact with our system to achieve a goal, by encapsulating a list of actions.

In Landbot, external actors (like our bot builder) interact with our system via REST API. We respond to that interaction by implementing a Django Rest Framework view that follows these 3 steps:

  • Deserialize the input data
  • Pass the deserialized data (plus other optionally needed data) to a use case
  • Send the use case’s response back to the actor

Here we can see the 3 steps. The operation is to create a channel, but just forget about it and see the structure:

DRF view calling a use case

And the internal structure of a use case for us may be as simple as this one:

As you can see, we inject the needed components at build creation time, using inject.autoparams, as explained above. And the use case just follows a list of actions (fetch data from a repository, create the requested object, publish some events and return the object created).

Oh, yes, there's that other part about… param validation?

Let’s go with this now.

Domain Specific Data Validation

We’re implicitly validating data via Django Rest Framework serializer, as we could see in the view code above when calling _serializer.is_valid.

But this is not a domain validation. It’s just data validation.

In our Django Rest Framework view, we ensure the input data is valid from a strictly typing-oriented point of view: we receive valid strings, valid numbers, valid lists, etc.

In our use case, we need to make sure the input data is valid from our domain point of view: we receive valid channel IDs we receive valid brand IDs, etc.

You don’t know what a channel or a brand is? Well, that’s the whole point :-) Just the components working inside of our domain should know that. Use cases are that kinds of components. Django Rest Framework views are not.

To implement this domain-specific validation, we use the Pydantic library.

So, going back to our previous example, this is what the ApiChatChannelCreator use case expects as input data:

Pydantic BaseModel for validation

A brand, a channel name, and a list of URLs.

As we saw in a prior post on our blog, these domain-specific types (domain objects, in our architecture’s terms) may be as simple as a string:

Simple domain object

Or a more complex object, like a URL that must be declared as valid for our domain (a specific type of str, in plain Python terms):

Domain-specific URL validation

You can be as specific as desired when talking about domain validation. For example, one of our use cases requires a very specific type of dictionary (Mapping type, really), containing date and time-related elements. So, we define it in Pydantic like this:

Specific Mapping for our domain

Of course, DateStr, DateRange, TimeStr, and TimeRange are also domain-defined Pydantic types. You get the idea, right?

What do you think about these software patterns/abstractions we use here at Landbot? Do let us know in the comments!

--

--

Jorge Arévalo
Landbot Engineering

Software developer and trainer. My main focus are Python language and Django framework