Domain Driven Design Toolkit

Artem Malyshev

@proofit404

BIO

Complexity

Accidental complexity refers to challenges that developers unintentionally make for themselves as a result of trying to solve a problem.

Essential complexity is just the nature of the beast you're trying to tame.

Accidental complexity

  • AsyncIO vs. Gevent
  • PostgreSQL vs. MongoDB
  • Python vs. Go
  • Emacs vs. Vim
  • Tabs vs. Spaces

What is the domain-driven design?

Focus on the core complexity and opportunity in the domain

Explore models in a collaboration of domain experts and software experts

Write software that expresses those models explicitly

Speak ubiquitous language within a bounded context

Not about technology

You have to master the tool first then you can focus on DDD.

In most domain models, most design patterns are technical noise.

What is a model?

HINT: Not a UML diagram

A set of services, entities and value objects expressed in classes, methods, and refs.

dry-python

A set of libraries for pluggable business logic components.

Define a user story in the business transaction DSL.

Separate state, implementation and specification.

Layout

project
├── admin
├── forms
├── migrations
├── models
├── services
├── templates
└── views

Specification DSL

from stories import story, arguments

class Subscription:
    @story
    @arguments("invoice_id", "user")
    def buy(I):
        I.find_order
        I.find_price
        I.find_invoice
        I.check_balance
        I.persist_payment
        I.persist_subscription
        I.send_subscription_notification

State Contract

from pydantic import BaseModel

class Subscription:
    @story
    def buy(I):
        ...

@Subscription.buy.contract
class Context(BaseModel):
    user: User
    invoice_id: int
    invoice: Optional[Invoice]

Steps implementation

from stories import Failure, Success

class Subscription:

    def find_invoice(self, ctx: Context):
        invoice = Invoice.objects.get(pk=ctx.invoice_id)
        return Success(invoice=invoice)

    def check_balance(self, ctx: Context):
        if ctx.user.can_pay(ctx.invoice):
            return Success()
        else:
            return Failure()

Story Execution

>>> Subscription().buy(category_id=2)
Subscription.buy:
  find_category
  check_price
  check_purchase (PromoCode.validate)
    find_code (skipped)
  check_balance
    find_profile

Context:
  category_id = 1318  # Story argument
  user = <User: 3292> # Story argument
  category = <Category: 1318>
    # Set by Subscription.find_category

DEBUG TOOLBAR











py.test

Sentry

ELK

Layout

project
├── admin
├── aggregates
├── forms
├── migrations
├── models
├── services
├── templates
└── views

dataclasses

from dataclasses import dataclass
from typing import List, NewType

OrderId = NewType("OrderId", int)

@dataclass
class LineItem:
    product_id: ProductId

@dataclass
class Order:
    primary_key: OrderId
    items: List[LineItem]

Declarative mappers from ORM models to domain entities. And back again!

Layout

project
├── admin
├── aggregates
├── forms
├── migrations
├── models
├── repositories
├── services
├── templates
└── views

Django ORM

from mappers import Mapper
from app.aggregates import Order, OrderId, User
from app.models import OrderModel, UserModel

mapper = Mapper(Order, OrderModel, {"primary_key": "pk"})

@mapper.reader
def load_order(id: OrderId, user: User) -> Order:
    friends = UserModel.objects.filter(
        purchases=OuterRef("pk"), friends=user.primary_key)
    return OrderModel.objects.filter(pk=id).annotate(
        purchased_by_friends=Exists(friends)).get()

Swagger definitions

from mappers import Mapper
from bravado import swagger_model
from app.aggregates import Price

spec = swagger_model.load_file("price_service.yml")
mapper = Mapper(Price, spec.definitions["Price"])

@mapper.reader
def load_price(id: PriceId) -> Price:
    return requests.get(f"http://172.16.1.7/get/{id}")

GraphQL queries

from mappers import Mapper
from gql import gql, Client, build_schema
from app.models import Invoice

schema = build_schema("invoice_service.graphql")
mapper = Mapper(Invoice, schema.get_type_map()["Invoice"])

@mapper.reader
def load_invoice(id: InvoiceId) -> Invoice:
    return Client(schema=schema).execute(gql("""
      {
        loadInvoice(id: %(id)d)
      }
    """, {"id": id}))

Tests & Mocks

def test_before(monkeypatch):
    monkeypatch.setattr(pusher, "Pusher", Mock())
    # ...
    pusher.Pusher.trigger.assert_called_once_with(
        "private-user-1"
    )

def test_after(emitter):
    # ...
    emitter.trigger.assert_called_once_with(
        UserStream(User(primary_key=1))
    )

Provide composition instead of inheritance.

Solves top-down approach problem.

Dependency Injection

from dependencies import Injector, Package

app = Package("project")

class BuySubscription(Injector):

    buy_subscription = app.services.Subscription.buy
    load_order = app.repositories.load_order
    load_price = app.repositories.load_price
    load_invoice = app.repositories.load_invoice

BuySubscription.buy_subscription(category_id=1, price_id=1)

Refactoring roadmap

  • No DDD at all
  • Stories without contracts
  • Contracts and aggregates
  • Mappers
  • Dependency injection

Questions?