Architecting Django Software Projects | by Luccas Correa | May, 2022

Some lessons learned on how to organize and structure Django projects

The architecture of a software project is a defining point for its long-term success. If the architecture is not well defined, maintenance gets more and more difficult, and implementing new features becomes cumbersome and requires rewriting large portions of the app.

In this article, I’ll quickly talk about some lessons learned over the years to better organize Django projects. There are things here that would be applicable to any software project but here and there I’ll add some Django-specific advice.

This is nowhere near a definitive guide as some of these topics could easily be expanded into multiple articles (or perhaps even books!) but it should provide food for thought.

Django encourages you to separate bits and pieces of your code into separate apps. This is not by chance as it is absolutely essential to keeping your application lean and easy to work with.

When studying the requirements for the project you are working on, one of the first steps you should take is grouping together related functionality while also drawing clear boundaries between unrelated parts. These separate parts will make up the different Django apps in your application.

An important point is that the apps should be completely separate, without direct references between them. As a rule of thumb, an app should never need to import a model from another app directly. If ever an app needs to interact with another, it should do so by calling some helper function that is implemented in the other app (or even better, use signals for handling communication between apps).

This should keep the internals of each app shielded from the outside world, making it much easier for you to make changes without breaking unrelated parts of the application.

Django uses the model-view-template paradigm (MVT), which is a slight variation of the model-view-controller paradigm (MVC). This architectural pattern encourages you to keep your front-facing code separate from the internal logic of your application. However, it’s very easy to mix model and view logic together.

Overall, the view should be responsible for parsing any data that comes from your frontend application. That means converting strings to dates, validating parameters, and returning user-friendly error messages when necessary. On the other hand, it should not be responsible for manipulating model instances, modifying their properties, or running queries.

Your view code should be lean and simple with the sole purpose of preparing the parameters that should be sent to your backend logic. Your backend logic can be executed by custom manager methods or your own classes and functions which will then manipulate model instances and run queries.

This makes your backend logic shielded from the external world enabling you to reuse it no matter how it’s being accessed. For instance, you could have some logic that is triggered by a REST endpoint that uses Django Rest Framework and reuse that same logic somewhere else where you’re using a Django form for receiving input. At the same time that it gives you this flexibility, writing surgically precise tests becomes much easier too.

Whenever adding dependencies to a project, you should really think about it with care. Libraries add fragility to your application since you’ll depend on code that wasn’t written by you or your team which you may not be familiar with. A library that may look promising today could be left gathering dust by its authors and leave you with an unresolved bug.

This, however, does not mean you should be completely averse to adding dependencies to your projects. After all, building everything from scratch every time would not be the smartest decision. There’s a fine balance between quickly adding functionality to your app and making sure your app can evolve smoothly through the years.

In general, whenever I need to add a dependency to a project, I first study exactly what it is I need to solve and how the library solves it. This means going through the library’s documentation and its source code. If I see that the way the library solves the issue is simple enough, I’ll implement it myself. If not, I’ll check if the library has a large enough number of maintainers and if its source code is well organized.

Then, when you decide that a dependency indeed needs to be added to the project and there isn’t an easy way around it, there’s another tip you can use to make your life easier if you ever need to replace it. The trick is to completely isolate the library from the rest of your application.

For example, let’s say you need a library to send push notifications to mobile devices. Instead of importing the library directly whenever you need to send a notification, define a class or function of your own whose sole purpose is to send push notifications (ideally in its own app that’s responsible for handling push notification logic). Then, call that single function throughout the application.

If ever comes the day when that library becomes deprecated or a nasty unresolved bug creeps in, you can simply replace it with another one or even code your own implementation without having to modify any of your application logic. This is a lighter version of a concept preached by Clean Architecture (which may be overkill depending on the project but you should definitely look into it).

This strategy is nothing new, Django itself uses this all throughout its own source code for dealing with different database backends, email backends, caching, etc. It’s a sure way to keep your code protected from the outside world, allowing you to swap dependencies without hassle.

I wrote some other articles on how to test your applications and it’s never enough to emphasize how important tests are for the long-term survival of your application.

Even if you take all the precautions in this article with care, chances are at some point you’ll realize that to implement a certain feature, you’ll need to refactor some logic that has been working fine for a long while. That’s when having well-written tests will let you breathe much more calmly.

Tests help you ensure that your application will remain working in the future just as well as it does today. It will also give you a lot more confidence whenever you need to refactor large portions of the code.

I think software architecture is something that you never really stop learning. Reading articles like this can give you a direction and show you things that you can look deeper into if you are interested but things will really sink in when you start facing these problems in the real world.

I hope I was able to share some of the knowledge I’ve gathered (though I’m still constantly learning too). If you have any questions or some other tips of your own, I would be very interested to hear them 🙂.

Leave a Comment