The 'Rocky River'
How to architect your Django monolith
David Seddon
slides.com/davidseddon/rocky-river
This talk
- What is the Rocky River?
- Real world example
Rivers
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
Rocks (#1)
Rocks are easier to handle when they're broken apart
So are apps.
App
App
App
App
App
App
App
Rocks (#2)
Rock is layered
Open layers
Closed layers
Typical layers in a Django app
Routing
Controller
Input validation
Business logic & storage
urls.py
views.py
forms.py
models.py
Splitting up storage and business logic
Routing
Controller
Input validation
Storage
urls.py
views.py
forms.py
models.py
Business logic
domain.py
Use layers to manage complexity
The Rocky River
- No circular dependencies
- Small apps
- Layers within apps
The Rocky River in practice
The classic way to do it
Businesses
Accounting
Business
urls.py
views.py
forms.py
models.py
client.py
Designing a 'rocky river':
- Break things apart into manageable chunks.
- Decide on a dependency flow.
- Use layers to manage complexity.
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
2. Decide on a dependency flow
But how?
Business
upstream
downstream
2. Decide on a dependency flow
Dependency inversion principle
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
Into the code...
- Abstracting the problem
- Plumbing
- Presentation
- Implementing the solution
1. Abstracting the problem
# 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)
2. Plumbing
# 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
3. Presentation
# 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})
4. Implementing the solution
# 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
The Rocky River
- No circular dependencies
- Small apps
- Layers within apps
David Seddon
@seddonym
david@seddonym.me
The Rocky River
By David Seddon
The Rocky River
- 2,687