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 link

Seriously, since this awesome-* came out, it made my link pages single linked!!!

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

  • 5,046
Loading comments...

More from Mafinar Khan