Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 Eric Evans (2004) 11

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

4

Slide 6

Slide 6 text

5 Distilled problem-domain Low-ceremony solution-domain

Slide 7

Slide 7 text

6

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

8 http://dddcommunity.org

Slide 10

Slide 10 text

9

Slide 11

Slide 11 text

9

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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)

Slide 23

Slide 23 text

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)

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

21 Software Architecture Towards self-contained domain models

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

28 plain old python objects

Slide 40

Slide 40 text

29 Pure Python Domain Model

Slide 41

Slide 41 text

30 Event-Sourcing Incorruptible data for 6000 years

Slide 42

Slide 42 text

31

Slide 43

Slide 43 text

31 3000 B.C.

Slide 44

Slide 44 text

32

Slide 45

Slide 45 text

33

Slide 46

Slide 46 text

time events = ] [

Slide 47

Slide 47 text

time events = ] [

Slide 48

Slide 48 text

time events = ] [ append-only

Slide 49

Slide 49 text

time events = ] [ append-only

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

time events = ] [

Slide 54

Slide 54 text

time events = ] [ reconstitute domain model state

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

36 Concrete Advice for implementing Domain Driven Designs in Python

Slide 58

Slide 58 text

37 Concrete Advice for implementing Domain Driven Designs in Python

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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!

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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])

Slide 65

Slide 65 text

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)

Slide 66

Slide 66 text

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()) + ')'

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

45 Exercises Evolving a Domain Model to Event Sourced Persistence

Slide 70

Slide 70 text

46

Slide 71

Slide 71 text

47

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

49