Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Event-Sourced Domain Models in Python

Event-Sourced Domain Models in Python

The introductory slides for a workshop on applying some classic patterns from Domain-Driven Design (DDD) in Python-based systems, which storage in a simple event store.

Robert Smallshire

September 20, 2015
Tweet

More Decks by Robert Smallshire

Other Decks in Programming

Transcript

  1. @sixty_north
    Event-Sourced Domain Models in Python
    Principles and Practice
    1
    Robert Smallshire
    @robsmallshire

    View Slide

  2. 2
    Engaging training from seasoned professionals
    DDD Patterns in Python
    Is this the course for you?
    Domain-Driven Design (DDD) is an approach to
    software development that emphasizes high-
    fidelity modeling of the problem domain, and
    which uses a software implementation of the
    domain model as a foundation for system design.
    This approach helps organize and minimize the
    essential complexity of your software. Python is
    a highly productive, easy-to-learn, lightweight
    programming language that minimizes accidental
    complexity in the solution domain. This two day
    course will teach you the fundamentals of classical
    and modern DDD patterns in the context of
    Python.
    • We start by introducing the philosophy and
    principles of DDD.
    • We move quickly into hands-on domain
    modeling.
    • We implement a stand-alone domain model in
    pure Python code.
    • Taught on Windows, Linux or Mac OS X
    • Knowledge level of our Python for Programmers
    course is assumed
    Key Topics
    • Domain discovery
    • Bounded contexts and subdomains
    • Entity and value types
    • Aggregates
    • Domain events
    • Architecture
    • Persistence
    • Repositories
    • Projections
    • Domain services
    sixty-north.com

    View Slide

  3. 3
    Eric Evans (2004)
    11

    View Slide

  4. 4

    View Slide

  5. 4

    View Slide

  6. 5
    Distilled problem-domain
    Low-ceremony
    solution-domain

    View Slide

  7. 6

    View Slide

  8. a route to expressive and valuable software models
    Domain Driven Design is...
    ‣ a philosophy of bringing software
    closer to the problem domain
    • involving domain experts
    ‣ a systematic approach intertwining
    design and development
    • strategic design practices
    • fundamental principles
    • guidelines, rules and patterns
    7

    View Slide

  9. 8
    http://dddcommunity.org

    View Slide

  10. 9

    View Slide

  11. 9

    View Slide

  12. 10
    encapsulate with
    MODEL-DRIVEN
    DESIGN
    express model with
    isolate domain with
    encapsulate with
    ENTITIES
    VALUE
    OBJECTS
    LAYERED
    ARCHITECTURE
    AGGREGATES
    REPOSITORIES
    act as root of
    FACTORIES
    encapsulate with
    express model with
    encapsulate with
    access with
    encapsulate with
    access with
    DOMAIN EVENTS
    express model with
    SERVICES
    push state change with
    CONTEXT MAP
    overlap allied contexts through
    minimize translation
    support multiple
    clients through
    formalize as
    CONTINUOUS
    INTEGRATION
    CUSTOMER
    /SUPPLIER
    CONFORMIST
    OPEN HOST
    SERVICE
    SEPARATE
    WAYS
    PUBLISHED
    LANGUAGE
    SHARED
    KERNEL
    relate allied contexts as
    free teams to go
    ANTI-
    CORRUPTION
    LAYER
    translate and insulate
    unilaterally with
    BOUNDED CONTEXT
    keep model unified by
    assess/overview
    relationships with
    UBIQUITOUS LANGUAGE
    names
    enter
    BIG BALL OF MUD
    segregate the
    conceptual messes
    define model within
    model gives structure to
    CORE DOMAIN
    work in
    autonomous, clean
    GENERIC
    SUBDOMAINS
    avoid overinvesting in
    cultivate rich
    model with
    express model with
    by Eric Evans
    http://domainlanguage.com/ddd/patterns/
    Eric Evans

    View Slide

  13. 11
    ‣ Plain Old Python
    Objects
    • POPOs
    • regular references
    • object graph

    View Slide

  14. 12
    ‣ Entities
    • identifiable
    • probably mutable
    • composite
    ‣ Values
    • measure/describe
    • equivalent
    • replaceable
    • self-contained
    • preferably immutable

    View Slide

  15. 12
    entity
    value
    ‣ Entities
    • identifiable
    • probably mutable
    • composite
    ‣ Values
    • measure/describe
    • equivalent
    • replaceable
    • self-contained
    • preferably immutable

    View Slide

  16. 13
    entity
    value
    ‣ Aggregates
    • consistency
    boundaries
    • transaction / unit-of-
    work modifies only
    one aggregate
    • not just object
    clusters
    • may only have one
    entity

    View Slide

  17. 13
    entity
    value
    aggregate
    ‣ Aggregates
    • consistency
    boundaries
    • transaction / unit-of-
    work modifies only
    one aggregate
    • not just object
    clusters
    • may only have one
    entity

    View Slide

  18. 14
    entity
    value
    aggregate
    ‣ Aggregate Root Entity
    • maintains aggregate
    consistency
    • hosts commands
    (methods) which modify
    the aggregate
    • target for inbound
    aggregate references
    • all inter-aggregate
    references are by root
    entity ID

    View Slide

  19. 14
    entity
    value
    aggregate
    root
    ‣ Aggregate Root Entity
    • maintains aggregate
    consistency
    • hosts commands
    (methods) which modify
    the aggregate
    • target for inbound
    aggregate references
    • all inter-aggregate
    references are by root
    entity ID

    View Slide

  20. 15
    entity
    value
    aggregate
    root
    ‣ Domain Events
    • Something which
    happens which domain
    experts care about
    • Significant state
    transitions
    • Publish-subscribe rather
    than subject-observer

    View Slide

  21. 15
    entity
    value
    aggregate
    root
    event
    ‣ Domain Events
    • Something which
    happens which domain
    experts care about
    • Significant state
    transitions
    • Publish-subscribe rather
    than subject-observer

    View Slide

  22. 16
    entity
    value
    aggregate
    root
    event
    ‣ Factories
    • Facilitate entity (and
    thereby aggregate)
    construction
    • Allow us to express the
    ubiquitous language -
    verbs rather than nouns
    (constructors)
    • Hide construction
    details (e.g. entity ID
    generation)

    View Slide

  23. 16
    entity
    value
    aggregate
    root
    factory
    event
    ‣ Factories
    • Facilitate entity (and
    thereby aggregate)
    construction
    • Allow us to express the
    ubiquitous language -
    verbs rather than nouns
    (constructors)
    • Hide construction
    details (e.g. entity ID
    generation)

    View Slide

  24. 17
    entity
    value
    aggregate
    root
    factory
    event
    ‣ Repositories
    • Store aggregates.
    • Usually one repo. per
    aggregate type.
    • Abstraction over
    persistence mechanism.
    • Architecturally
    significant!

    View Slide

  25. 17
    entity
    value
    aggregate
    root
    factory
    repository
    event
    ‣ Repositories
    • Store aggregates.
    • Usually one repo. per
    aggregate type.
    • Abstraction over
    persistence mechanism.
    • Architecturally
    significant!

    View Slide

  26. 18
    entity
    value
    aggregate
    root
    factory
    repository
    event
    ‣ Bounded Contexts
    • Scope of a
    ubiquitous language.
    • Segregated models
    • Independent,
    autonomous
    implementations
    • Align with technical
    components
    • Loosely coupled

    View Slide

  27. 18
    entity
    value
    aggregate
    bounded
    context
    root
    factory
    repository
    event
    ‣ Bounded Contexts
    • Scope of a
    ubiquitous language.
    • Segregated models
    • Independent,
    autonomous
    implementations
    • Align with technical
    components
    • Loosely coupled

    View Slide

  28. 19
    entity
    value
    aggregate
    root
    factory
    repository
    event
    bounded
    context
    ‣ Context maps
    • Integrations between
    bounded contexts
    • Logical mapping
    (concepts)
    • Physical mapping
    (actual software!)
    • Many integration
    patterns e.g. anti-
    corruption layer or
    open host service

    View Slide

  29. 19
    entity
    value
    aggregate
    context map
    root
    factory
    repository
    event
    bounded
    context
    ‣ Context maps
    • Integrations between
    bounded contexts
    • Logical mapping
    (concepts)
    • Physical mapping
    (actual software!)
    • Many integration
    patterns e.g. anti-
    corruption layer or
    open host service

    View Slide

  30. 20
    encapsulate with
    MODEL-DRIVEN
    DESIGN
    express model with
    isolate domain with
    encapsulate with
    ENTITIES
    VALUE
    OBJECTS
    LAYERED
    ARCHITECTURE
    AGGREGATES
    REPOSITORIES
    act as root of
    FACTORIES
    encapsulate with
    express model with
    encapsulate with
    access with
    encapsulate with
    access with
    DOMAIN EVENTS
    express model with
    SERVICES
    push state change with
    CONTEXT MAP
    overlap allied contexts through
    minimize translation
    support multiple
    clients through
    formalize as
    CONTINUOUS
    INTEGRATION
    CUSTOMER
    /SUPPLIER
    CONFORMIST
    OPEN HOST
    SERVICE
    SEPARATE
    WAYS
    PUBLISHED
    LANGUAGE
    SHARED
    KERNEL
    relate allied contexts as
    free teams to go
    ANTI-
    CORRUPTION
    LAYER
    translate and insulate
    unilaterally with
    BOUNDED CONTEXT
    keep model unified by
    assess/overview
    relationships with
    UBIQUITOUS LANGUAGE
    names
    enter
    BIG BALL OF MUD
    segregate the
    conceptual messes
    define model within
    model gives structure to
    CORE DOMAIN
    work in
    autonomous, clean
    GENERIC
    SUBDOMAINS
    avoid overinvesting in
    cultivate rich
    model with
    express model with
    by Eric Evans
    http://domainlanguage.com/ddd/patterns/
    Eric Evans

    View Slide

  31. 21
    Software Architecture
    Towards self-contained domain models

    View Slide

  32. Foo
    Nothing we have
    seen requires:
    ‣ complicated
    frameworks
    ‣ proprietary
    infrastructure
    ‣ avante-garde
    architectures
    ‣ "enterprise"
    anything
    22

    View Slide

  33. 23
    We dynamic language
    developers are still
    (largely) building models
    in terms of infrastructure
    and frameworks
    Eric Evans (2004)
    but the world has moved
    over the last decade

    View Slide

  34. 23
    We dynamic language
    developers are still
    (largely) building models
    in terms of infrastructure
    and frameworks
    Eric Evans (2004)
    in

    View Slide

  35. 24
    “The Object Relational Mapper takes
    two brilliant ideas and incapacitates
    them both.” Eric Evans, Domain Language Inc.
    at Skills Matter DDD eXchange 2013

    View Slide

  36. 25
    http://blog.8thlight.com/
    uncle-bob/2012/08/13/the-
    clean-architecture.html
    http://alistair.cockburn.us/
    Hexagonal+architecture
    The Clean Architecture The Hexagonal Architecture
    Uncle Bob Martin Alistair Cockburn

    View Slide

  37. 26
    http://jeffreypalermo.com/blog/
    the-onion-architecture-part-1/
    These architectures
    place the model at
    the centre and
    externalise
    infrastructure
    The Onion Architecture
    Jeffrey Palermo

    View Slide

  38. 27
    your model
    DDD layers from 2004
    Eric Evans (2004)
    Pyramid web-app Django web-app

    View Slide

  39. 28
    plain old
    python
    objects

    View Slide

  40. 29
    Pure Python
    Domain Model

    View Slide

  41. 30
    Event-Sourcing
    Incorruptible data for 6000 years

    View Slide

  42. 31

    View Slide

  43. 31
    3000 B.C.

    View Slide

  44. 32

    View Slide

  45. 33

    View Slide

  46. time
    events = ]
    [

    View Slide

  47. time
    events = ]
    [

    View Slide

  48. time
    events = ]
    [
    append-only

    View Slide

  49. time
    events = ]
    [
    append-only

    View Slide

  50. time
    “Current state is the left-fold over previous behaviours” Greg Young
    events = ]
    [
    append-only

    View Slide

  51. time
    “Current state is the left-fold over previous behaviours” Greg Young
    functools.reduce(
    )
    events = ]
    [
    append-only

    View Slide

  52. time
    “Current state is the left-fold over previous behaviours” Greg Young
    functools.reduce(
    )
    events = ]
    [
    apply_event,
    events,
    initial_model_state
    append-only

    View Slide

  53. time
    events = ]
    [

    View Slide

  54. time
    events = ]
    [
    reconstitute
    domain model
    state

    View Slide

  55. time
    events = ]
    [
    reconstitute
    domain model
    state
    at any time

    View Slide

  56. time
    events = ]
    [
    reconstitute
    domain model
    state
    at any time
    “projections”
    (time dependent queries)
    of the event stream

    View Slide

  57. 36
    Concrete Advice
    for implementing Domain Driven Designs in Python

    View Slide

  58. 37
    Concrete Advice
    for implementing Domain Driven Designs in Python

    View Slide

  59. 38
    class Entity:
    """The base class of all entities.
    Attributes:
    id: A unique identifier.
    version: An integer version.
    discarded: True if this entity should no longer be used, otherwise False.
    """
    def __init__(self, id, version):
    self._id = id
    self._version = version
    self._discarded = False
    def _increment_version(self):
    self._version += 1
    @property
    def id(self):
    """A string unique identifier for the entity."""
    self._check_not_discarded()
    return self._id
    @property
    def version(self):
    """An integer version for the entity."""
    self._check_not_discarded()
    return self._version
    @property
    def discarded(self):
    """True if this entity is marked as discarded, otherwise False."""
    return self._discarded
    def _check_not_discarded(self):
    if self._discarded:
    raise DiscardedEntityError("Attempt to use {}".format(repr(self)))
    class DiscardedEntityError(Exception):
    """Raised when an attempt is made to use a discarded Entity."""
    pass
    pass id as argument (don't create a new id here)
    maintain a discarded flag
    consider maintaining a version

    View Slide

  60. 38
    class Entity:
    """The base class of all entities.
    Attributes:
    id: A unique identifier.
    version: An integer version.
    discarded: True if this entity should no longer be used, otherwise False.
    """
    def __init__(self, id, version):
    self._id = id
    self._version = version
    self._discarded = False
    def _increment_version(self):
    self._version += 1
    @property
    def id(self):
    """A string unique identifier for the entity."""
    self._check_not_discarded()
    return self._id
    @property
    def version(self):
    """An integer version for the entity."""
    self._check_not_discarded()
    return self._version
    @property
    def discarded(self):
    """True if this entity is marked as discarded, otherwise False."""
    return self._discarded
    def _check_not_discarded(self):
    if self._discarded:
    raise DiscardedEntityError("Attempt to use {}".format(repr(self)))
    class DiscardedEntityError(Exception):
    """Raised when an attempt is made to use a discarded Entity."""
    pass
    pass id as argument (don't create a new id here)
    maintain a discarded flag
    consider maintaining a version

    View Slide

  61. 39
    !"" application
    # !"" __init__.py
    # $"" __main__.py Application services.
    !"" bounded_context One package per bounded context
    # !"" __init__.py
    # $"" domain
    # !"" __init__.py
    # !"" model
    # # !"" __init__.py
    # # !"" aggregate_large Large complex aggregates should be their own package
    # # # !"" __init__.py This will expose the API of the aggregate at package level
    # # # !"" root_entity.py The root entity - responsible for consistency of the aggregate
    # # # !"" other_entity.py Another entity which forms part of this aggregate
    # # # !"" factories.py Factory functions for creating aggregates
    # # # $"" repository.py Aggregate persistence (Abstract if following hexagonal architecture)
    # # !"" aggregate_small.py Small, simple aggregates (most of them) can be a single file module
    # # $"" events.py Singleton publish-subscribe hub for domain events. Event base class.
    # $"" services
    # !"" __init__.py
    # !"" calculation_service.py Implement significant computations which involve multiple aggregates
    # $"" manipulation_service.py Perform significant manipulations involving for than one aggregate (care!)
    $"" infrastructure
    !"" __init__.py
    $"" repositories
    !"" __init__.py
    !"" large_aggregate_repo.py A concrete repository - in terms of the persistence infrastructure
    $"" small_aggregate_repo.py for each abstract repository type on the domain model
    Remember to use the
    ubiquitous language!

    View Slide

  62. 40
    from bounded_context.domain.model.entity import Entity
    class User(Entity):
    def __init__(self, user_id, user_version, name):
    """DO NOT CALL DIRECTLY.
    """
    super().__init__(user_id, user_version)
    self._name = name
    def __repr__(self):
    return "{d}User(id={id!r}, name={name!r})".format(
    d="*Discarded* " if self._discarded else "",
    id=self.id,
    name=self._name)
    @property
    def name(self):
    self._check_not_discarded()
    return self._name
    @name.setter
    def name(self, value):
    self._check_not_discarded()
    if len(value) < 1:
    raise ValueError("User name cannot be empty")
    self._name =value
    self._increment_version()
    def register_customer(name):
    user = User(user_id=uuid.uuid4().hex,
    user_version=0,
    name=name)
    return work_item
    factory creates id
    ubiquitous language
    mutators increment version
    queries and mutators
    check liveness
    __repr__() makes song and
    dance about liveness

    View Slide

  63. 40
    from bounded_context.domain.model.entity import Entity
    class User(Entity):
    def __init__(self, user_id, user_version, name):
    """DO NOT CALL DIRECTLY.
    """
    super().__init__(user_id, user_version)
    self._name = name
    def __repr__(self):
    return "{d}User(id={id!r}, name={name!r})".format(
    d="*Discarded* " if self._discarded else "",
    id=self.id,
    name=self._name)
    @property
    def name(self):
    self._check_not_discarded()
    return self._name
    @name.setter
    def name(self, value):
    self._check_not_discarded()
    if len(value) < 1:
    raise ValueError("User name cannot be empty")
    self._name =value
    self._increment_version()
    def register_customer(name):
    user = User(user_id=uuid.uuid4().hex,
    user_version=0,
    name=name)
    return work_item
    factory creates id
    ubiquitous language
    mutators increment version
    queries and mutators
    check liveness
    __repr__() makes song and
    dance about liveness

    View Slide

  64. 41
    ‣ Lean heavily on built-in
    immutable types tuple,
    frozenset, str, etc
    ‣ Consider preventing attribute
    assignment by overriding
    __setattr__()
    ‣ Implement value equality/
    inequality __eq__() and
    __ne__()
    ‣ Hashing by overriding
    __hash__()
    ‣ Side-effect free functions to
    produce new versions
    class Email:
    """An e-mail address
    """
    @classmethod
    def from_text(cls, address):
    if '@' not in address:
    raise ValueError("Email address must contain '@'")
    local_part, _, domain_part = address.partition('@')
    return cls(local_part, domain_part)
    def __init__(self, local_part, domain_part):
    if len(local_part) + len(domain_part) > 255:
    raise ValueError("Email addresses too long")
    self._parts = (local_part, domain_part)
    def __str__(self):
    return '@'.join(self._parts)
    def __repr__(self):
    return "Email(local_part={!r}, domain_part={!r})".format(*self._parts)
    def __eq__(self, rhs):
    if not isinstance(rhs, Email):
    return NotImplemented
    return self._parts == rhs._parts
    def __ne__(self, rhs):
    return not (self == rhs)
    def __hash__(self):
    return hash(self._parts)
    @property
    def local(self):
    return self._parts[0]
    @property
    def domain(self):
    return self._parts[1]
    def replace(self, local=None, domain=None):
    return Email(local_part=local or self._parts[0],
    domain_part=domain or self._parts[1])

    View Slide

  65. 42
    _event_handlers = {}
    def subscribe(event_predicate, subscriber):
    """Subscribe to events.
    Args:
    event_predicate: A callable predicate which is used to identify the events to which to subscribe.
    subscriber: A unary callable function which handles the passed event.
    """
    if event_predicate not in _event_handlers:
    _event_handlers[event_predicate] = set()
    _event_handlers[event_predicate].add(subscriber)
    def unsubscribe(event_predicate, subscriber):
    """Unsubscribe from events.
    Args:
    event_predicate: The callable predicate which was used to identify the events to which to subscribe.
    subscriber: The subscriber to disconnect.
    """
    if event_predicate in _event_handlers:
    _event_handlers[event_predicate].discard(subscriber)
    def publish(event):
    """Send an event to all subscribers.
    Each subscriber will receive each event only once, even if it has been subscribed multiple
    times, possibly with different predicates.
    Args:
    event: The object to be tested against by all registered predicate functions and sent to
    all matching subscribers.
    """
    matching_handlers = set()
    for event_predicate, handlers in _event_handlers.items():
    if event_predicate(event):
    matching_handlers.update(handlers)
    for handler in matching_handlers:
    handler(event)

    View Slide

  66. 43
    class DomainEvent:
    """A base class for all events in this domain.
    DomainEvents are value objects and all attributes are specified as keyword
    arguments at construction time. There is always a timestamp attribute which
    gives the event creation time in UTC, unless specified. Events are
    equality comparable.
    """
    def __init__(self, timestamp=_now, **kwargs):
    self.__dict__['timestamp'] = utc_now() if timestamp is _now else timestamp
    self.__dict__.update(kwargs)
    def __setattr__(self, key, value):
    raise AttributeError("DomainEvent attributes are read-only")
    def __eq__(self, rhs):
    return self.__dict__ == rhs.__dict__
    def __ne__(self, rhs):
    return self.__dict__ != rhs.__dict__
    def __hash__(self):
    return hash(tuple(self.__dict__.items()))
    def __repr__(self):
    return self.__class__.__qualname__ + "(" + ', '.join(
    "{0}={1!r}".format(*item) for item in self.__dict__.items()) + ')'

    View Slide

  67. 44
    class Repository:
    __metaclass__ = ABCMeta
    def __init__(self, **kwargs):
    super().__init__(**kwargs)
    def all_users(self, user_ids=None):
    return self.users_where(lambda user: True, user_ids)
    def users_with_name(self, name, user_ids=None):
    return self.users_where(lambda user: user.name == name,
    user_ids)
    def users_with_id(self, user_id):
    try:
    return exactly_one(self.all_users((user_id,)))
    except ValueError as e:
    raise ValueError("No WorkItem with id {}".format(user_id)) from e
    @abstractmethod
    def users_where(self, predicate, user_ids=None):
    raise NotImplementedError
    convenience queries for retrieving users
    implemented in terms of a generic query
    Repository will be subclassed with an infrastructure-specific
    implementation, which can specialise all queries.
    subclass implementation must override at least this

    View Slide

  68. 44
    class Repository:
    __metaclass__ = ABCMeta
    def __init__(self, **kwargs):
    super().__init__(**kwargs)
    def all_users(self, user_ids=None):
    return self.users_where(lambda user: True, user_ids)
    def users_with_name(self, name, user_ids=None):
    return self.users_where(lambda user: user.name == name,
    user_ids)
    def users_with_id(self, user_id):
    try:
    return exactly_one(self.all_users((user_id,)))
    except ValueError as e:
    raise ValueError("No WorkItem with id {}".format(user_id)) from e
    @abstractmethod
    def users_where(self, predicate, user_ids=None):
    raise NotImplementedError
    convenience queries for retrieving users
    implemented in terms of a generic query
    Repository will be subclassed with an infrastructure-specific
    implementation, which can specialise all queries.
    subclass implementation must override at least this

    View Slide

  69. 45
    Exercises
    Evolving a Domain Model to Event Sourced Persistence

    View Slide

  70. 46

    View Slide

  71. 47

    View Slide

  72. To do Doing Done
    Work
    Item A
    Work
    Item B
    Work
    Item C
    Work
    Item D
    Work
    Item E
    register
    schedule
    advance
    advance
    retire
    abandon

    View Slide

  73. 49

    View Slide