Doing Domain Driven Design with Django
Mafinar Khan ● Planswell ● @mafinar ● /code-shoily
In this session...
- Discuss DDD Concepts
- Reason about Architecture
- Walkthrough the code
- Interactive extension of the project
Our Project: Ride Sharing App
In the next 60 minutes we will be...
- Thinking in domains and contexts
- Using Django as the vessel to implement Domain Driven Design and a new way to structure Django projects
- Reasoning about the architecture of a project in a decoupled, pluggable and readable way
- Gaining interest towards learning more about DDD
- Getting introduced to some software engineering buzzwords to impress colleagues
background
- Pythonista for Life
- Django (Since 0.96)
- Had my share of Java
- PHP 4- Survivor
- Used Clojure in Production
- Dabbled with F# <3
- Used Go
Never to use again - In love with Elixir
- Suffered enough JavaScript
Let's make a ride sharing app and call it rideiculous
what is domain driven design?
what is a domain here?
Domain
- Subset of the Internet with addresses sharing a common suffix
- A specified sphere of activity or knowledge.
- The set of possible values of the independent variable or variables of a function.
domain Driven Design
- An approach to software development for complex needs by connecting the implementation to an evolving model.
- Domain dominates the design, code and communication during development, not model, class, function, or database
some definition
- Context - The setting in which a word or statement appears that determines its meaning
- Model - A system of abstractions that describes selected aspects of a domain and can be used to solve problems related to that domain
- Ubiquitous Language - A language structured around the domain model and used by all team members to connect all the activities of the team with the software.
some more Definitions
- Entity - An object that is not defined by its attributes, but rather by a thread of continuity and its identity.
- Value Object - An object that contains attributes but has no conceptual identity.
- Aggregate - A collection of objects that are bound together by a root entity
- Domain Event - A domain object that defines an event
- Service - When an operation does not conceptually belong to any object.
- Repository - Methods for retrieving domain objects should delegate to a specialized Repository objecy
Check out wikipedia for more definitions
now for the real fun
Let's look at some code
we arrange rides for you through our app- just set the time and place and our driver will pick you up.
Statement 1
#core #missionStatement #elevatorPitch
WE NEED TO HAVE A SYSTEM TO MANAGE DRIVERS AND VEHICLES
Statement 2
#backOffice #helper #internal
WE NEED A WAY FOR USERS TO REGISTER THEMSELVES WITH US
Statement 3
#registration #userFacing #external
USERS NEED TO be able REQUEST TRIPS AND drivers should be able to respond to it
Statement 4
#tripManagement #userFacing #driverFacing #initiation
WE NEED TO HAVE A MECHANISM TRACK IN/pre-TRIP LOCATIONS with Gis
Statement 5
#tripManagement #userFacing #driverFacing #GIS
WE NEED A MATCHING and controlling MECHANISM OF RIDERs, DRIVERs AND LOCATIONs
Statement 6
#tripManagement #userFacing #driverFacing #GIS
WE MUST PROVIDE SAFETY AND SECURITY OF EVERYONE
Statement 7
#legal #authr #user #driver #staff
User? I think we call it rider
Wtf is a GIS?
- Domain Expert
ubiquitous language time!!!
Let's make bounded contexts
but before that...
django
isn't
your app
Manage Drivers
Manage Vehicles
Register User
User Requests Trips
Driver Accepts Trips
Track Trips
Match Driver with Rider
Trip Status Control
Manage Location
Authenticate Rider
Authorize Trip
Big Ball of Mud
Manage Drivers
Manage Vehicles
Register Rider
User Requests Trips
Driver Accepts Trips
Track Trips
Match Driver with Rider
Trip Status Control
Manage Location
Authenticate Rider
Managing Rider
Bounded Contexts
User
Trip
Driver
Places
Manage Address
nouns = entity, value object, aggregates
verbs = domain events
SENTENCE GROUP = BOUNDED CONTEXT
Everything else = Services
All programming language, ever...
- Define Data Structure
- Compose Data Structure
- Apply Functions upon Data Structure
Define domain in python, stick to ubiquitous language
Entities
- Has Identity
- Represented a Noun within its context
- We use dataclass to represent one
- Please ensure the naming of fields and classes to satisfy ubiquitous language
- Example: Rider, Driver, Vehicle
from infrastructure.exceptions import DomainValidationError
from infrastructure.services import validate_email
from typing import List
from dataclasses import dataclass
from .value_objects import Vehicle, DriversLicense
from .services import validate_drivers_license
@dataclass
class Driver:
id: str
first_name: str
last_name: str
email: str
phone_number: str
mugshot: str
username: str
date_joined: str
last_login: str
drivers_license: DriversLicense
vehicles: List[Vehicle]
def __post_init__(self):
if not validate_email(self.email):
raise DomainValidationError("Invalid Email")
if not validate_drivers_license(self.drivers_license):
raise DomainValidationError("Invalid Drivers License")
value object
- A meaningful value of an entity
- Cannot change, needs no ID
- Can be of single value or composite
- Should only be used when it makes sense in the language of the domain
- Entity in one context can be value object in another
- Example: LicensePlate, RegistrationCode, DropOffAddress
from typing import List
from dataclasses import dataclass
from infrastructure.exceptions import DomainValidationError
from .services import create_default_geo_info, validate_latlng
@dataclass(frozen=True)
class GISInfo:
bounding_box: List[float]
projection: List[float]
@dataclass(frozen=True)
class Location:
lat: float
lng: float
geo: GISInfo
def __post_init__(self):
self.geo = create_default_geo_info()
if not validate_latlng(self.lat, self.lng, validate_latlng):
raise DomainValidationError("Invalid lat/lng")
aggregates
- Super Entity - containing other entity
- Forms collection in a meaningful way
- Often the representative data structure of a context or domain
- Example: Trip (Contains Driver, Rider as attributes)
import datetime
from typing import List
from dataclasses import dataclass
from infrastructure.exceptions import DomainValidationError
from .value_objects import (Driver, Rider, Address, Location, TripTime, TripStatus)
@dataclass
class Trip:
id: str
start_time: TripTime
end_time: TripTime
driver: Driver
rider: Rider
trip_status: TripStatus("Requested")
start_location: Address
end_location: Address
locations: List[Location]
responses: List[Driver]
def validate_time_range(self):
now = datetime.datetime.now()
if now > self.start_time or (self.end_time > self.start_time):
return False
return True
def __post_init__(self):
if not self.validate_time_range():
raise DomainValidationError("Time range error")
Services
- Functions that do not belong to any of the model object types
- Maybe public (Outside of Context) or private (Within Context)
- Example: compute_distance, can_view_vehicle etc`
from .entities import *
from .repository import create_trip, update_trip, find_trips_by_rider, find_trips_by_driver
def request_trip(rider_id: str) -> Trip:
return create_trip(rider_id)
def cancel_trip(canceller_id: str, cancelled_by: str, reason) -> None:
if cancelled_by == "driver":
return update_trip(driver=canceller_id, reason=reason)
else:
return update_trip(rider=canceller_id, reason=reason)
def trip_history_for_rider(id: str, start_time: datetime, end_time: datetime) -> Trip:
return find_trips_by_rider(rider=id, start_time=start_time, end_time=end_time)
def trip_history_for_driver(id: str, start_time: datetime, end_time: datetime) -> Trip:
return find_trips_by_rider(driver=id, start_time=start_time, end_time=end_time)
So entities map with database table, right?
WRONG
It's class function database domain driven design
isolate and push side-effect as far as possible
Database/Persistence are side effects
Abstract it away, single it out
Repository
- An interface to persist Aggregate Roots
- Move data in and out of Storage (Query + Write)
- Storage mechanism can change with minimal interference
- The only place where Django specific imports are made in a bounded context (Django ORM)
- Can easily swap it out for SQLAlchemy, or raw SQL without impacting anything else within the whole system
from typing import List
from django.contrib.auth.models import User
from infrastructure.models import Driver, Vehicle
from acl import convert_to_driver
def create_driver(driver_info,
user_info,
vehicles) -> Driver:
user = User.objects.create(**user_info)
driver = Driver.objects.create(user=user, **driver_info)
for vehicle in vehicles:
Vehicle.objects.create(driver=driver, **vehicle)
convert_to_driver(driver)
def search_driver(**criteria) -> List[Driver]:
matched_drivers = Driver.objects.filter(**criteria)
return [convert_to_driver(driver) for driver in matched_drivers]
Serialization and errors
- Entity, Value Objects and Aggregates should know how to (de-)serialize themselves
- Errors should be raised whenever a serialization fails at any level within the aggregate root
- Database level errors (i.e. Unique Constraint violation) should be incorporated during Repository reads/writes
how do i communicate between different bounded contexts?
Inter context communication
- Shared Kernel - Models are shared between contexts, good old fashioned way of not caring much
- Consumer Driven Contract - Calling context decides what data it needs from serving context
- Conformist Relationship - Serving Context decides what data to pass to calling context (Service functions)
- Anti Corruption Layer (ACL) - Converts one data type into another to ensure nothing leaks in or falls short
Domain Events
- Remember those verbs painted in red?
- Denotes something happening in the world
- In a pub/sub fashion, is broadcasted in the bloodstream
- Contexts that subscribe to it, handle it via services or model methods
- Can be implemented in many ways, Django signals are used here but also Celery and Redis can help
- Example: LocationSent, UserRegistered
pieces of the puzzle
Title | Filename | Description |
---|---|---|
Entities | entities.py | Entity Dataclass |
Value Objects | value_objects.py | Value object dataclasses |
Aggregates | entities.py | - |
Services | services.py | Service functions |
Repository | repository.py | Repository class |
Events | events.py | Signal handlers |
But but but django???
Project architecture
Folder/Module | Description |
---|---|
application | "View" side of things: web, api etc. May include typical Django apps (sans model) |
domain | Home of all the bounded contexts |
infrastructure | All the Django models and migrations (WHY?), along with any bash/python scripts, python libraries etc |
The application
- Contains all web stuff - views, forms, templates etc
- Can only call services from bounded contexts
- Make sure to NOT import anything from models
- Only import the "web" libraries of Django
- Can have graphql, rest, web as top layer modules
- Manages app security
- URL handling happens here
import json
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from domain.trips.services import find_trips_by_driver, get_current_trip
from domain.permissions.services import can_view_trips
@login_required
def get_driver_trips(request, driver_id):
trip = None
if can_view_trips(request.user, driver_id, driver=True):
start_time = request.GET.get("start_time")
end_time = request.GET.get("end_time")
trip_dict = find_trips_by_driver(driver_id, start_time, end_time, asdict=True)
return render(request, "trips/trip_report.html", {trips: trip_dict})
@login_required
def cancel_trip(request):
cancellation_status = None
if can_cancel_trip(trip, request.user):
cancellation_status = user_cancel_trip(trip)
return HttpResponse(json.dumps(cancellation_status), status=cancellation_status or 403)
infrastructure
- This is where the models reside
- With models come migrations
- All non-web, non-domain code must reside here
- Django models are used for migration and ORM purposes, not necessarily they belong in the web
Do we really need an example of django model?
some immediate benefits
- Focused on Domain and Application Case
- No one size fits all, solution space tailored to respect problem space
- Easy to test since side-effects are isolated
- Easier to reason about
- Easily pluck out non-domain components and insert new
- Database, ORM, even Web Framework changes are minimal
- Every member of bounded context only make use of language constructs, easy to shift in other language, easy to attract different mindsets
- Shared knowledge between all stakeholders, less chance to fail
what about cqrs, event sourcing etc?
NOT TODAY :(
A TO-do List
- More complete code for this
- Naming this pattern, I vote (VERESA)
- Follow up blog post or two (???)
- Create base classes to have common functionality for context members
- Make a generator to generate boilerplate and context automatically (to bring in some productivity!)
useful linkS
Seriously, since this awesome-* came out, it made my link pages single linked!!!
books that helped me learn to think like this
A Note From the future (aka 2021)
"Architecture Patterns in Python"
Didn't know this book was written while I was presenting this. It explains a lot of things I said here a lot more comprehensively. If you're interested in Python + DDD, I would recommend taking a look at the book.
questions? Thoughts? feelings? emotions? complains?
Thank you! You all rock!!!
Doing Domain Driven Design with Django
By Mafinar Khan
Doing Domain Driven Design with Django
- 10,141