How to architect your Django monolith
David Seddon
slides.com/davidseddon/rocky-river
A river system
django
django-treebeard
django-cms
bcrypt
django-formtools
openid
django-allauth
myproject.app3
myproject.app2
myproject.app1
myproject.app4
settings
You can think of your dependencies like a river system
Rivers only flow in one direction.
So should your dependencies.
upstream
downstream
django
django-treebeard
django-cms
bcrypt
django-formtools
openid
django-allauth
myproject.app2
myproject.app1
myproject.core
myproject.app3
myproject.main
settings
App
App
App
App
App
App
App
Open layers
Closed layers
Routing
Controller
Input validation
Business logic & storage
urls.py
views.py
forms.py
models.py
Routing
Controller
Input validation
Storage
urls.py
views.py
forms.py
models.py
Business logic
domain.py
The classic way to do it
Businesses
Accounting
Business
urls.py
views.py
forms.py
models.py
client.py
Quickbooks
Sage
Xero
1. Break things apart into manageable chunks
Business
Cloudconnect
Quickbooks
Sage
Xero
Platform
1. Break things apart into manageable chunks
Business
Cloudconnect
Quickbooks
Sage
Xero
Platform
ConnectionService
1. Break things apart into manageable chunks
Business
Cloudconnect
Quickbooks
Sage
Xero
Platform
ConnectionService
Cloudconnect
Accounting
Xero
Quickbooks
Sage
1. Break things apart into manageable chunks
Business
Cloudconnect
But how?
Business
upstream
downstream
2. Decide on a dependency flow
Abstractions should not depend on details.
Details should depend on abstractions.
Abstractions
Implementation details
upstream
downstream
From the SOLID principles:
Business
Platform
ConnectionService
Accounting
Abstractions
upstream
downstream
2. Decide on a dependency flow
Business
Quickbooks
Sage
Xero
Platform
ConnectionService
Cloudconnect
Cloudconnect
Accounting
Xero
Quickbooks
Sage
Abstractions
Implementation
details
upstream
downstream
2. Decide on a dependency flow
3. Use layers to manage complexity
Closed layer
Plumbing
Low level
urls.py
views.py
models.py client.py
Domain
platform.py
connection_service.py
3. Use layers to manage complexity
Plumbing
Low level
Domain
Low level
Dependency injection
platform.py
views.py
urls.py
connection_service.py
accounting
platform.py
views.py
urls.py
connection_service.py
platforms.py
quickbooks
platforms.py
sage
platforms.py
xero
platform.py
urls.py
connection_service.py
cloudconnect
client.py
models.py
connection_service.py
platform.py
urls.py
connection_service.py
templates/accounting
connection_page.py
includes/base_connect_platform_block.html
templatetags/accounting_tags.py
includes/connect_xero_block.html
includes/connect_quickbooks_block.html
includes/connect_sage_block.html
Domain
}
Plumbing
}
Presentation
Low level
Low level
# accounting/platform.html
class Platform:
def __init__(self, name, verbose_name):
self.name = name
self.verbose_name = verbose_name
def __str__(self):
return self.verbose_name
class PlatformRegistry:
def register(self, platform):
...
def get_by_name(self, platform_name):
...
def all(self):
...
# accounting/__init__.py
from django.utils.module_loading import autodiscover_modules
class AppConfig(BaseAppConfig):
...
def ready(self):
# Autodiscover any app modules named accounting_platforms.py
autodiscover_modules('accounting_platforms')
...
# accounting/connection.html
from django.conf import settings
from django.utils.module_loading import import_string
class AbstractConnectionService:
@classmethod
def get_connected_platform(cls, business):
"""Given a business, returns the platform that the business
is currently connected to. Otherwise returns None.
"""
raise NotImplementedError
@classmethod
def get_connection_url(cls, business, platform):
"""Get the URL that the business should visit to connect
to the supplied platform.
"""
raise NotImplementedError
@classmethod
def store_successful_connection(cls, business, params):
"""Stores the connection following a successful connection
using the connection service.
"""
raise NotImplementedError
def get_connection_service():
return import_string(settings.ACCOUNTING_CONNECTION_SERVICE)
# accounting/views.py
from . import platforms
from .connection_service import get_connection_service
class ConnectionsPage(TemplateView):
"""View for the user to view their connected platform,
or select one to connect to.
"""
template_name = 'accounting/connections.html'
def get_context_data(self, *args, **kwargs):
context_data = super().get_context_data(*args, **kwargs)
connection_service = get_connection_service()
# If the business is connected to a platform,
# include that in the context
context_data['connected_platform'] = connection_service\
.get_connected_platform(self.business)
# Otherwise, include all available platforms
if not context_data['connected_platform']:
context_data['available_platforms'] = platforms.all()
return context_data
# templates/accounting/connections.html
{% load accounting_tags %}
...
{% if connected_platform %}
{% borrower_connected_platform_block connected_platform %}
{% else %}
{% for platform in available_platforms %}
{% borrower_connect_platform_block platform %}
{% endfor %}
{% endif %}
...
# accounting/templatetags/accounting_tags.html
...
@register.simple_tag
def borrower_connect_platform_block(platform):
"""Display the block to allow the user to connect to a platform.
"""
template_name = 'accounting/includes/connect_{}_block.html'/
.format(platform.name)
template = loader.get_template(template_name)
return template.render({'platform': platform})
# xero/accounting_platforms.py
from accounting import platforms, Platform
xero = Platform(name='xero', verbose_name='Xero')
platforms.register(xero)
# quickbooks/accounting_platforms.py
from accounting import platforms, Platform
quickbooks = Platform(name='quickbooks', verbose_name='Quickbooks')
platforms.register(quickbooks)
# sage/accounting_platforms.py
from accounting import platforms, Platform
sage = Platform(name='sage', verbose_name='Sage')
platforms.register(sage)
# cloudconnect/connection_service.py
from accounting.connection import AbstractConnectionService
from accounting import platforms
from .models import CloudconnectBusinessConnection
class Cloudconnect(AbstractConnectionService):
@classmethod
def get_connected_platform(cls, business):
try:
connection = CloudconnectBusinessConnection.objects.filter(
business=business).exclude(platform_name='').get()
except CloudconnectBusinessConnection.DoesNotExist:
return None
else:
return platforms.get_by_name(connection.platform_name)
...
# settings.py
ACCOUNTING_CONNECTION_SERVICE = \
'cloudconnect.connection_service.Cloudconnect'
platform.py
views.py
urls.py
connection_service.py
accounting
platform.py
views.py
urls.py
connection_service.py
platforms.py
quickbooks
platforms.py
sage
platforms.py
xero
platform.py
urls.py
connection_service.py
cloudconnect
client.py
models.py
connection_service.py
platform.py
urls.py
connection_service.py
templates/accounting
connection_page.py
includes/base_connect_platform_block.html
templatetags/accounting_tags.py
includes/connect_xero_block.html
includes/connect_quickbooks_block.html
includes/connect_sage_block.html
Domain
}
Plumbing
}
Presentation
Low level
Low level
David Seddon
@seddonym
david@seddonym.me