Architecture decisions in Landbot
An overview of the architecture decisions we’ve made in Landbot over the years to improve the scalability and maintainability of our codebase
When you start building an MVP to validate an idea, sometimes there are design decisions that are questionable from the point of view of software architecture. You need to validate a hypothesis with a small user base. The scalability is not a problem (yet).
But brace yourselves, because you will need to replace your quick-and-dirty solutions with solid and well-proved implementations in the unlikely event that the product matures into the foundation of a profitable business.
We’re kind in the process of refurbishing the house, by applying the cool concepts of world-famous Clean Architecture. Let us share a few highlights about how we’re doing it.
In the beginning, everything was Django and Django Rest Framework…
Don’t misunderstand us. Django and Django Rest Framework are amazing pieces of software. Great for the purpose they were created: building REST APIs based on the MVC pattern. A common way to build MVPs and make your site live in a quick and reasonably clean way.
The Clean Architecture shows up
Once you get that, if your product grows, you will probably need to think deeper about the best way to manage the (also growing) flow of data and new business needs.
At this point, common industry-standard concepts come to the table: scalability, maintainability, growth, sharding, ports and adapters, clean architectures, quitting your job, and starting a new life in the mountains… And probably, the adoption of clean architectures is one of the smarter things you can think of (quitting your job aside… Just kidding. Don’t)
Those architectures normally give your codebase these 2 features:
- Better testability
- Independency (from frameworks, from UI, from databases, from thirds parties…)
But it’s also common to enter into software architecture Holy Wars: which one is better and why?
Well. It depends on your specific business case. In our case, we took:
- A few technical patterns of DDD (entities, value objects, repositories, domain events)
- The module distinction of hexagonal architecture (application, infrastructure, domain)
- One of the main properties of clean architectures: the dependency rule, or quoting Uncle Bob: source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle
And used them in our code base
Let’s see some actual code to better understand how we work with these concepts. For example, this is the summarized code structure of our campaigns
app
Let’s dig deeper. First, the inner layer: the domain
Inside the domain, we have our main entity, Campaign
. We use a little bit of typing and pydantic library for type validation
As you probably noticed, some of the types are basic Python primitives, like int
, bool
, or str
, and other ones are complex types. Specifically, value objects, which define the properties of our domain (AudienceUUID
, BotUUID
, etc).
And, in concept, they can be as simple as this
Apart from that, we also define our campaign-specific exceptions inside of our domain module. Because we believe they also belong there.
Finally, we define the interface with the ports also here. But just the interface, not the implementation.
The implementation lives in an outer layer: the infrastructure layer. But will go with that later.
Now, let’s go with the next layer: the application one. Where use cases live
The structure of one use case couldn’t be simpler. A Python class with 3 important concepts to understand:
- The parameter validation (using pydantic)
- The dependency injection, using a Python dependency injection framework (Clean Architecture as its best)
- The business logic
You can see them all in the screenshot (a simplified use case)
Last but not least, there is the infrastructure
package, which would represent the Adapters
layer in the Clean Architecture schema
Remember the domain layer above? It contained the interface with the ports. Now, here you have the (Django-specific) implementation
You’re probably missing the more external layer. The web one. In our case, this is just Django Rest Framework viewsets, exposing a REST API to our frontend apps. With a little bit of tunning, but in the end, the viewsets just call use cases.
For example, here is our endpoint to create a new channel
, as part of a Django Rest Framework ModelViewSet subclass (don't worry about the meaning of channel, just abstract from it)
In conclusion, it may sound obvious, but we believe that, in software architecture, there’s no silver bullet. It all depends on the specific needs of your business. This cherry-picking of well-tested industry standards suits us, so far.
And for you? Let us know in the comments!