Domain Services Enhancements
subhashb opened this issue · 0 comments
Abstract
This RFC proposes a redesign of domain services in Protean. Currently, domain services are simple classes that lack structured invariants and guidance on their usage. This change aims to elevate domain services to full-fledged domain layer elements, providing clear and structured mechanisms for business logic validation and invariant enforcement.
Background
In the current framework, domain services are implemented as simple classes with class methods invoked from Application Services, Command Handlers, and Event Handlers. This structure does not explicitly mark invariants or guide the usage of a Domain Service in an application. We can ensure more robust and maintainable code by redesigning domain services to be callable classes with explicit invariant handling.
Proposal
-
Callable Classes for Domain Services: Each domain service will be implemented as a callable class, functioning similarly to a function. These classes'
__init__
method will accept the necessary aggregates for executing the business logic, ensuring that all required data is encapsulated within the service. -
Invariant Methods: Domain services will include explicit methods identified with the
@invariant
decorator that define the invariants that must be maintained throughout the business process. -
Automatic Invariant Validation: All invariant methods will be invoked automatically after executing the
__call__
method. This ensures that all invariants are checked and remain satisfied, providing additional validation and consistency within the business logic.
Justification
The proposed redesign addresses several issues in our current implementation:
- Structured Invariants: By defining invariants explicitly within domain services, we provide a clear and structured approach to maintaining business rules.
- Guidance and Consistency: Callable domain services offer better guidance on how they should be used, reducing the likelihood of misuse or incorrect implementation.
- Robustness: Automatic invariant validation ensures that business logic remains consistent and correct, enhancing the robustness of our codebase.
- Maintainability: The proposed changes lead to more maintainable code, as invariants and business logic are clearly defined and encapsulated within domain services.
Implementation Considerations
-
Decorator Implementation: The existing
@invariant
decorator, used to mark invariant methods in aggregates and entities, will be repurposed to identify invariant methods within domain services. The Domain Service factory method will collect these methods and invoke them in the order they are defined after the__call__
method execution. -
Dependency Injection: The domain services
__init__
method will accept one or more aggregates as arguments, making all necessary data available for business logic execution and invariant validation later. -
Error Handling: A validation error will be raised, with a collection of all errors raised during invariant validation.
Sample Code
Sample place_order
Domain Service
@domain.domain_service
class place_order:
def __init__(self, order: Order, inventories: list[Inventory]):
self.order = order
self.inventories = inventories
@invariant
def validate_that_product_is_in_stock(self):
for item in self.order.items:
inventory = next(
(i for i in self.inventories if i.product_id == item.product_id), None
)
if inventory is None or inventory.quantity < item.quantity:
raise Exception("Product is out of stock")
inventory.reserve_stock(item.quantity)
def __call__(self):
self.order.confirm()
Usage Example
@domain.command_handler
class OrderCommandHandler:
@handle(PlaceOrderCommand)
def handle_place_order(self, command: PlaceOrderCommand):
order = current_domain.repository_for(Order).get(command.order_id)
inventories = []
for id in command.stock_ids:
inventories.append(current_domain.repository_for(Inventory).get(id))
place_order(order=order, inventories=inventories)()