Package: ddd-domain-events
Inspired byUdi Dahan’s fabulous Domain Eventsimplementation in particular, and byDomain Driven Designbest practices in general.The DDD Domain Events package makes it easy to:Register toDomain Eventsfrom theApplication LayerRaiseDomain Eventsfrom theDomain Layerso they can be handled in theApplication LayerTheDomain Eventsare local to the execution thread
(via Python’sthreading.local)
and hence are thread specific.Installing ddd-domain-eventspipinstallddd-domain-eventsWhy should you consider using Domain Events?Domain Events are based on theObserver Design Pattern, which is a
convenient way to decouple yourDomain Layer Business Logicfrom theApplication Layercode - especially as the
Domain code should have noInfrastructure Layerdependencies (such as calling Repositories or Providers that call
external services).In other words, yourDomain Entitiesshould have no access to theApplication Layer Services- and
hence, yourEntitiesmight useDomain Eventsto indirectly communicate withApplication Services.For example, say you have a Gaming Domain with a User entity that hasadd_points(number_of_points)behavior.
Wheneveradd_pointsis executed, the obvious expected behavior is to add the provided number of points to the
specific user. However, in our sample game, whenever a user reaches 1,000 points then she should receive the “Master”
badge, and whenever the user reaches 1,000,000 points then she should receive the “Champion” badge.Your Application Layer pseudo python code might look like this:user.add_points(10,000)ifuser.has_reached_master_level:# TODO: Send the user a congratulation email...elifuser.has_reached_champion_level:# TODO: Send the user both a congratulation email and a check...The above code is perfectly o.k. - yet, as there will be more actions / options that
are the consequence ofadd_points, then the code will become more and more cumbersome.Using the Domain Events alternative, will allow you to write code that looks more like this pseudo code:domain_events.register_event(has_reached_master_level_callback)domain_events.register_event(has_reached_champion_level_callback)user.add_points(10,000)This code is cleaner and arguably easier to extend, as shown bellow:domain_events.register_event(has_reached_master_level_callback)domain_events.register_event(has_reached_champion_level_callback)# introduce new level...domain_events.register_event(has_reached_intermediate_level_callback)user.add_points(50)Last but not least, Domain Events allow you to keep your Entity’s code cleaner - with less methods
(such ashas_reached_master_levelandhas_reached_champion_levelwhich can be omitted) that might be
relatively complex - as they might require you to use additional state in the entity (so that you know if
the user has reached the current level following the recent call toadd_points).Using DDD Domain EventsUsing Domain Events in an existing Application Service can easily be achieved by using Python’swith-statement.For example:Somewhere in the Application Layer…withDomainEvents()asdomain_events:# create a callback to the notify_top_management Application Layer functionhigh_price_volume_callback=DomainEventCallable(OrderEvent.HIGH_VOLUME_PRICE,notify_top_management),# register callback - so it can be triggered from the Domain Layerdomain_events.register_event(high_price_volume_callback)# create Domain Entityorder=Order()# execute a Domain method that might raise the relevant Domain Eventorder.add_order_items(order_items)Somewhere in the Domain Layer…# Domain entity raises a Domain Event - allowing the Application Layer# to take a relevant action.DomainEvents.raise_event(OrderEvent.HIGH_VOLUME_PRICE,order=self)How it worksBellow is a simplified example that should help you understand how and when you might choose to useDomain Events.Step 1: Define aDomain Event Typein yourDomain Layerfromddd_domain_eventsimportDomainEvents,DomainEventCallableclassOrderEvent(Enum):"""Domain Event raised for special order use cases"""HIGH_QUANTITY='HIGH_QUANTITY'HIGH_VOLUME_PRICE='HIGH_VOLUME_PRICE'Step 2: Define aDomain Entitythat raises Domain EventsclassOrderItem:"""OrderItem value object that contains order details for a single item"""def__init__(self,product_id:str,price:float,quantity:int):self.product_id=product_idself.price=priceself.quantity=quantityclassOrder:"""Order entity that contains order items"""HIGH_VOLUME_PRICE=1_000_000HIGH_QUANTITY=10_000def__init__(self):self._order_items=[]@propertydeforder_items(self):fororder_iteminself._order_items:yieldorder_itemdefadd_order_items(self,order_items:List[OrderItem])->None:total_price=0total_quantity=0fororder_iteminorder_items:total_price+=(order_item.price*order_item.quantity)total_quantity+=order_item.quantity# Process the actual business logic related to this method,# which is add OrderItem value objects to this Order Entityself._order_items.append(order_item)# Notify whoever might be interested about high price volume ordersiftotal_price>=self.HIGH_VOLUME_PRICE:DomainEvents.raise_event(OrderEvent.HIGH_VOLUME_PRICE,order=self)# Notify whoever might be interested about high quantity volume ordersiftotal_quantity>=self.HIGH_QUANTITY:DomainEvents.raise_event(OrderEvent.HIGH_QUANTITY,order=self)Step 3: Define anApplication Servicethat registers toDomain EventsclassOrderService:"""Application Service for handling Order related operations"""@classmethoddefcreate_order(cls,order_items:List[OrderItem])->Order:withDomainEvents()asdomain_events:# Create callbacks for 'side effects' that are related to domain logic,# and which should be handled by the Application Layercallbacks=[DomainEventCallable(OrderEvent.HIGH_VOLUME_PRICE,cls.notify_top_management),DomainEventCallable(OrderEvent.HIGH_VOLUME_PRICE,cls.notify_sales_team),DomainEventCallable(OrderEvent.HIGH_QUANTITY,cls.notify_inventory_team)]# Register for these domain eventsforcallbackincallbacks:domain_events.register_event(callback)order=Order()order.add_order_items(order_items)returnorder@staticmethoddefnotify_sales_team(order:Order)->None:"""A callback for notifying the sales team about the important order"""@staticmethoddefnotify_top_management(order:Order)->None:"""A callback for notifying the top management about the important order"""@staticmethoddefnotify_inventory_team(order:Order)->None:"""A callback for notifying the inventory team required quantities"""