Save 37% off PRO during our Black Friday Sale! »

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.

4be361182fa13cf39c00ec69c1cb9e30?s=128

Robert Smallshire

September 20, 2015
Tweet

Transcript

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

    Robert Smallshire @robsmallshire
  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
  3. 3 Eric Evans (2004) 11

  4. 4

  5. 4

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

  7. 6

  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
  9. 8 http://dddcommunity.org

  10. 9

  11. 9

  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
  13. 11 ‣ Plain Old Python Objects • POPOs • regular

    references • object graph
  14. 12 ‣ Entities • identifiable • probably mutable • composite

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

    • composite ‣ Values • measure/describe • equivalent • replaceable • self-contained • preferably immutable
  16. 13 entity value ‣ Aggregates • consistency boundaries • transaction

    / unit-of- work modifies only one aggregate • not just object clusters • may only have one entity
  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
  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
  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
  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
  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
  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)
  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)
  24. 17 entity value aggregate root factory event ‣ Repositories •

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

    • Store aggregates. • Usually one repo. per aggregate type. • Abstraction over persistence mechanism. • Architecturally significant!
  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
  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
  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
  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
  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
  31. 21 Software Architecture Towards self-contained domain models

  32. Foo Nothing we have seen requires: ‣ complicated frameworks ‣

    proprietary infrastructure ‣ avante-garde architectures ‣ "enterprise" anything 22
  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
  34. 23 We dynamic language developers are still (largely) building models

    in terms of infrastructure and frameworks Eric Evans (2004) in
  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
  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
  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
  38. 27 your model DDD layers from 2004 Eric Evans (2004)

    Pyramid web-app Django web-app
  39. 28 plain old python objects

  40. 29 Pure Python Domain Model

  41. 30 Event-Sourcing Incorruptible data for 6000 years

  42. 31

  43. 31 3000 B.C.

  44. 32

  45. 33

  46. time events = ] [

  47. time events = ] [

  48. time events = ] [ append-only

  49. time events = ] [ append-only

  50. time “Current state is the left-fold over previous behaviours” Greg

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

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

    Young functools.reduce( ) events = ] [ apply_event, events, initial_model_state append-only
  53. time events = ] [

  54. time events = ] [ reconstitute domain model state

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

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

    any time “projections” (time dependent queries) of the event stream
  57. 36 Concrete Advice for implementing Domain Driven Designs in Python

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

  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
  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
  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!
  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
  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
  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])
  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)
  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()) + ')'
  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
  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
  69. 45 Exercises Evolving a Domain Model to Event Sourced Persistence

  70. 46

  71. 47

  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
  73. 49