Ronan's Tech Blog

Feb 16, 2019

Django ports & adapters series, laying out the problem

Start from the beginning...

This is my introduction piece to this blog series and lays out how I came to be tackling this problem.

Django is a great system for getting started with a web project but it starts to strain as the project grows. Their ethos is to make the simple things easy and the complex things possible. I'm using Django heavily at the moment and I'm feeling some of these strains. This series is about addressing those issues in a maintainable way using the Ports and Adapters architecture.

It's an opinionated framework in the sense that it makes some choices for you. This is a necessary part of making simple things easy. One of the choices that is made for you is to use the Active record pattern.

Escaping Active Record

Active record is familiar to most Django programmers, by experience if not by name. The models.Model base class is Django's implementation of it. Like any software design choice there are strengths and weaknesses. In accordance with Django's ethos, this choice is aimed at making the simple things easy.

Active record is the practise of building your domain model, domain logic and database model into one object. This is a shortcut that can be taken when the two objects overlap. It's really good when the database table layout and the in-memory domain model are the same - of course you'd use the same object there. Conversely, if the two models are very different then you don't want them to be the same object.

Usually a system will start out simple and grow. It will slowly transition from being well suited to ActiveRecord into something that's really not. This has happened to me.

I've got a professional project where there are dozens of models with hundreds of views. Refactoring the database schema has become hard because there are too many pieces of code dependent on it. Too much complexity is brought in by depending on a detail instead of an abstraction. The views and controllers all depend on the domain model and reference its API. This is good and correct. The problem we have with active record is that the views also depend on the persistence model.

This is tight coupling from one IO mechanism to another and our domain logic is caught in the middle. This puts us in the position of needing to trade off between performance, maintainability and domain model complexity. The performance is necessary and the domain model is out of our control so maintainability suffers if the system continues being developed without addressing this issue.

Have you ever heard a colleague calling user requirements "stupid"? Then some part of the business insists that it's built anyway and everyone gets more tense? This is my litmus test. A code smell without code. The forced complexity from the domain model has nowhere to go when the domain logic depends on other complex details like persistence.

Ports and adapters

Ports and adapters is an architecture pattern very well suited to this situation. It puts domain models and business logic at the core of your program and makes everything else depend on them. These are the details of the real world that don't change nearly as often as database technologies, messaging frameworks, the number of customers your product needs to support or usage patterns of users.

The pattern says that everything outside of the domain model should depend on the domain model. This allows us to contain complexity in the domain model where it is abundant and keep all other concerns separate. In Django's case that starts with the database but will be extended to signals, background task queues and anything else that occurs along the journey.

The dependency of our domain logic on the database has to be inverted so that we can freely change either without worrying about breaking the other. The active record stuff is very useful but has to be abstracted.

It's neatly explained on Ward Cunningham's wiki and informs Bob Martin's "clean architecture".

So, where to?

The next post will show how I started with a domain model built outside Django, built persistence services around it, then brought it in to a Django project whilst retaining the useful properties of the architecture, adding Django models as 'just' another option for persistence.

I'll go on to explore inserting this architecture into an existing system and refactoring the interfaces involved.

Click to read and post comments