From ae828e7907ca71b82319efc12342e3b0129e5092 Mon Sep 17 00:00:00 2001 From: Subhash Bhushan Date: Mon, 10 Jun 2024 16:02:21 -0700 Subject: [PATCH] Add documentation on raising events --- .../domain-behavior/aggregate-mutation.md | 29 -------- docs/guides/domain-behavior/raising-events.md | 69 ++++++++++++++++++- docs_src/guides/domain-behavior/001.py | 2 +- docs_src/guides/domain-behavior/009.py | 53 ++++++++++++++ mkdocs.yml | 1 + 5 files changed, 123 insertions(+), 31 deletions(-) create mode 100644 docs_src/guides/domain-behavior/009.py diff --git a/docs/guides/domain-behavior/aggregate-mutation.md b/docs/guides/domain-behavior/aggregate-mutation.md index ef237393..792f0e1f 100644 --- a/docs/guides/domain-behavior/aggregate-mutation.md +++ b/docs/guides/domain-behavior/aggregate-mutation.md @@ -70,32 +70,3 @@ InsufficientFundsException Traceback (most recent call last) ... InsufficientFundsException: Balance cannot be below overdraft limit ``` - -## Raising Events - -The aggregate also (preferably) raises one or more events to recort the state -change and to propagate the change within and beyond the bounded context. - -```python hl_lines="15" ---8<-- "guides/domain-behavior/002.py:16:33" -``` - -The events are visible as part of the mutated aggregate. When we review the -Application Layer, we will also talk about how these events are dispatched -automatically. - -```shell hl_lines="8 12-13" -In [1]: account = Account(account_number="1234", balance=1000.0, overdraft_limit=50.0) - -In [2]: account.withdraw(500.0) - -In [3]: account.to_dict() -Out[3]: -{'account_number': '1234', - 'balance': 500.0, - 'overdraft_limit': 50.0, - 'id': '37fc8d10-1209-41d2-a6fa-4f7312912212'} - -In [4]: account._events -Out[4]: [] -``` \ No newline at end of file diff --git a/docs/guides/domain-behavior/raising-events.md b/docs/guides/domain-behavior/raising-events.md index 228b28ec..aa74a869 100644 --- a/docs/guides/domain-behavior/raising-events.md +++ b/docs/guides/domain-behavior/raising-events.md @@ -1 +1,68 @@ -# Raising Events \ No newline at end of file +# Propagating State + +An aggregate rarely exists in isolation - it's state changes often mean +that other parts of the system of the system have to sync up. In DDD, the +mechanism to accomplish this is through Domain Events. + +## Raising Events + +When an aggregate mutates, it also (preferably) raises one or more events +to record the state change in time, as well as propagate it within and beyond +the bounded context. + +```python hl_lines="15-19" +--8<-- "guides/domain-behavior/002.py:16:35" +``` + +The generated events are collected in the mutated aggregate: + +```shell hl_lines="8 12-15" +In [1]: account = Account(account_number="1234", balance=1000.0, overdraft_limit=50.0) + +In [2]: account.withdraw(500.0) + +In [3]: account.to_dict() +Out[3]: +{'account_number': '1234', + 'balance': 500.0, + 'overdraft_limit': 50.0, + 'id': '37fc8d10-1209-41d2-a6fa-4f7312912212'} + +In [4]: account._events +Out[4]: [] +``` + +Any entity in the aggregate cluster can raise events. But the events are +collected in the aggregate alone. As we will see in the future, aggregates +are also responisible for consuming events and performing state changes on +underlying entities. + +## Dispatching Events + +These events are dispatched automatically to registered brokers when the +aggregate is persisted. We will explore this when we discuss repositories, but +you can also manually publish the events to the rest of the system with +`domain.publish()`. + + + +```shell hl_lines="11 16" +In [1]: order = Order( + ...: customer_id=1, premium_customer=True, + ...: items=[ + ...: OrderItem(product_id=1, quantity=2, price=10.0), + ...: OrderItem(product_id=2, quantity=1, price=20.0), + ...: ] + ...: ) + +In [2]: order.confirm() + +In [3]: order._events +Out[3]: +[, + ] + +In [4]: domain.publish(order._events) +``` \ No newline at end of file diff --git a/docs_src/guides/domain-behavior/001.py b/docs_src/guides/domain-behavior/001.py index 5221c51a..13002489 100644 --- a/docs_src/guides/domain-behavior/001.py +++ b/docs_src/guides/domain-behavior/001.py @@ -52,7 +52,7 @@ def customer_id_must_be_non_null_and_the_order_must_contain_at_least_one_item(se ) -@domain.entity +@domain.entity(part_of=Order) class OrderItem: product_id = Identifier() quantity = Integer() diff --git a/docs_src/guides/domain-behavior/009.py b/docs_src/guides/domain-behavior/009.py new file mode 100644 index 00000000..778b92c4 --- /dev/null +++ b/docs_src/guides/domain-behavior/009.py @@ -0,0 +1,53 @@ +from datetime import datetime, timezone + +from protean import Domain, invariant +from protean.exceptions import ValidationError +from protean import fields + +domain = Domain(__file__, load_toml=False) + + +@domain.event(part_of="Order") +class OrderConfirmed: + order_id = fields.Identifier(required=True) + confirmed_at = fields.DateTime(required=True) + + +@domain.event(part_of="Order") +class OrderDiscountApplied: + order_id = fields.Identifier(required=True) + customer_id = fields.Identifier(required=True) + + +@domain.aggregate +class Order: + customer_id = fields.Identifier(required=True) + premium_customer = fields.Boolean(default=False) + items = fields.HasMany("OrderItem") + status = fields.String( + choices=["PENDING", "CONFIRMED", "SHIPPED", "DELIVERED"], default="PENDING" + ) + payment_id = fields.Identifier() + + @invariant.post + def order_should_contain_items(self): + if not self.items or len(self.items) == 0: + raise ValidationError({"_entity": ["Order must contain at least one item"]}) + + def confirm(self): + self.status = "CONFIRMED" + self.raise_( + OrderConfirmed(order_id=self.id, confirmed_at=datetime.now(timezone.utc)) + ) + + if self.premium_customer: + self.raise_( + OrderDiscountApplied(order_id=self.id, customer_id=self.customer_id) + ) + + +@domain.entity(part_of=Order) +class OrderItem: + product_id = fields.Identifier(required=True) + quantity = fields.Integer() + price = fields.Float() diff --git a/mkdocs.yml b/mkdocs.yml index 5ae1126a..33102777 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -120,6 +120,7 @@ nav: - guides/domain-behavior/validations.md - guides/domain-behavior/invariants.md - guides/domain-behavior/aggregate-mutation.md + - guides/domain-behavior/raising-events.md - guides/domain-behavior/domain-services.md - Accessing the domain: - guides/access-domain/index.md