diff --git a/docs/adapters/database/redis.md b/docs/adapters/database/redis.md deleted file mode 100644 index 6a015730..00000000 --- a/docs/adapters/database/redis.md +++ /dev/null @@ -1,2 +0,0 @@ -# Redis - diff --git a/docs/core-concepts/building-blocks/command-handlers.md b/docs/core-concepts/building-blocks/command-handlers.md new file mode 100644 index 00000000..4659cfa9 --- /dev/null +++ b/docs/core-concepts/building-blocks/command-handlers.md @@ -0,0 +1,64 @@ +# Command Handlers + +Command handlers are responsible for processing commands. They encapsulate the +logic required to handle a command and ensure that the appropriate actions are +taken within the domain model. + +Command handlers typically reside in the application layer and serve as a +bridge between the application's API and the domain model. + +## Facts + +### Command handlers are connected to an aggregate. { data-toc-label="Connected to Aggregate" } +Command handlers are always connected to a single aggregate. One command handler +per aggregate is the norm, with all aggregate commands handled within it. + +### A Command handler contains multiple handlers. { data-toc-label="Handlers" } +Each method in a command handler is connected to a specific command to handle +and process. + +### Handlers are single-purpose. { data-toc-label="Single-Purpose" } +Each handler method is responsible for handling a single type of command. This +ensures that the command handling logic is focused and manageable. + +### Handlers should deal only with the associated aggregate. { data-toc-label="Single Aggregate" } +Methods in a command handler should only deal with managing the lifecycle of +the aggregate associated with it. Any state change beyong an aggregate's +boundary should be performed by eventual consistency mechanisms, like raising +an event and consuming it in the event handler of the other aggregate. + +### Command handlers invoke domain logic. { data-toc-label="Invoke Domain Logic" } +Command handlers do not contain business logic themselves. Instead, they invoke +methods on aggregates or domain services to perform the necessary actions. + +### Command handlers coordinate actions. { data-toc-label="Coordinate Actions" } +Command handlers coordinate multiple actions related to each command. Primarily, +this involves hydrating (fetching) the aggregate, invoking methods to perform +state changes and persisting changes through a repository. + +### Handler methdos are enclosed in Unit of Work context. { data-toc-label="Unit of Work"} +Each handler method is automatically enclosed within a Unit of Work context. +This means that all interactions with the infrastructure is packaged into a +single transaction. This makes it all the more important to not mix multiple +responsibilities or aggregates when handling a command. + +### Commands can be handled asynchronously. { data-toc-label="Asynchronous" } +While handling commands synchronously is the norm to preserve data integrity, +it is possible to configure the domain to handle commands asynchronously for +performance reasons. + +## Best Practices + +### Ensure idempotency. { data-toc-label="Idempotency" } +Command handling should be idempotent, meaning that handling the same command +multiple times should not produce unintended side effects. This can be achieved +by checking the current state before applying changes. + +### Handle exceptions gracefully. { data-toc-label="Exception Handling" } +Command handlers should handle exceptions gracefully, ensuring that any +necessary rollback actions are performed and that meaningful error messages are +returned to the caller. + +### Validate commands. { data-toc-label="Validation" } +Ensure that commands are validated before processing. This can be done in a +separate validation layer or within the command handler itself. diff --git a/docs/core-concepts/building-blocks/commands.md b/docs/core-concepts/building-blocks/commands.md new file mode 100644 index 00000000..e8f667b3 --- /dev/null +++ b/docs/core-concepts/building-blocks/commands.md @@ -0,0 +1,87 @@ +# Commands + +Commands are data transfer objects that express an intention to change the +state of the system. They represent actions that a user or system wants to +perform. In DDD, commands are an essential part of the application layer, +where they help to encapsulate and convey user intentions. + +Commands typically do not return values; instead, they result in events that +indicate changes in the system. + +## Facts + +### Commands express intentions. { data-toc-label="Intentions" } +Commands are not queries; they do not request data but rather express an +intention to perform an action that changes the state of the system. + +### Commands are essentially Data Transfer Objects (DTO). { data-toc-label="Data Transfer Objects" } +They can only hold simple fields and Value Objects. + +### Commands are immutable. { data-toc-label="Immutability" } +Commands should be designed to be immutable once created. This ensures that the intention they represent cannot be altered after they are sent. + +### Commands trigger domain logic. { data-toc-label="Domain Logic" } +Commands are handled by application services or command handlers, which then +interact with the domain model to execute the intended action. + +### Commands are named with verbs. { data-toc-label="Naming" } +Commands should be named clearly and concisely, typically using verbs to +indicate the action to be performed, such as `CreateOrder`, +`UpdateCustomerInfo`, or `CancelReservation`. These terms should match with +concepts in Ubiquitous Language. + +## Structure + +### Commands have **metadata**. { data-toc-label="Metadata" } +Headers and metadata such as timestamps, unique identifiers, and version +numbers are included in commands for precise tracking of origin and intent. + +### Commands are **versioned**. { data-toc-label="Versioning" } +Each command is assigned a version number, ensuring that commands can evolve +over time. Since commands are handled by a single aggregate through a command +handler, there is seldom a need to support multile versions of commands at +the same time. + +### Commands are **timestamped**. { data-toc-label="Timestamp" } +Each command carries a timestamp indicating when the command was initiated, +which is crucial for processing incoming commands chronologically. + +### Commands are written into streams. { data-toc-label="Command Streams" } +Commands are written to and read from streams. Review the section on +[Streams](../streams.md) for a deep-dive. + +### Command objects are always valid. { data-toc-label="Validation" } +Like other elements in Protean, commands are validated as soon as they are +initialized to ensure they contain all required information and that the data +is in the correct format. + +## Persistence + +### Commands do not persist data directly. { data-toc-label="Persistence" } +Commands themselves do not persist data; they trigger domain operations that +result in changes to the state of aggregates, which are then persisted by +repositories. + +### Commands can result in events. { data-toc-label="Events" } +Once a command has been successfully handled, it may result in domain events +being published. These events can then be used to notify other parts of the +system about the changes. + +## Best Practices + +### Keep commands simple. { data-toc-label="Simplicity" } +Commands should be simple and focused on a single responsibility. This makes +them easier to understand and maintain. + +### Use a consistent naming convention. { data-toc-label="Consistency" } +Maintain a consistent naming convention for commands to ensure clarity and +uniformity across the system. + +### Ensure idempotency. { data-toc-label="Idempotency" } +Command handling should be idempotent, meaning that handling the same command +multiple times should result in the same state without unintended side effects. + +### Secure sensitive data. { data-toc-label="Security" } +Be mindful of sensitive data within commands, especially when they are +transmitted over a network. Ensure that appropriate security measures are in +place to protect this data. diff --git a/docs/core-concepts/building-blocks/events.md b/docs/core-concepts/building-blocks/events.md index e8277ee2..5e3c9538 100644 --- a/docs/core-concepts/building-blocks/events.md +++ b/docs/core-concepts/building-blocks/events.md @@ -77,7 +77,7 @@ crucial for tracking and ordering events chronologically. Each event carries a structured unique identifier that indicates the origin of the event and the unique identity of the aggregate that generated the event. -### Event Streams +### Events are written into streams. { data-toc-label="Event Streams" } Events are written to and read from streams. Review the section on [Streams](../streams.md) for a deep-dive. diff --git a/docs/core-concepts/index.md b/docs/core-concepts/index.md deleted file mode 100644 index 1152a5ba..00000000 --- a/docs/core-concepts/index.md +++ /dev/null @@ -1 +0,0 @@ -# Core Concepts \ No newline at end of file diff --git a/docs/guides/compose-a-domain/initialize-domain.md b/docs/guides/compose-a-domain/initialize-domain.md index 3e945965..a4a255b8 100644 --- a/docs/guides/compose-a-domain/initialize-domain.md +++ b/docs/guides/compose-a-domain/initialize-domain.md @@ -31,7 +31,7 @@ exposes them in a registry. {! docs_src/guides/composing-a-domain/016.py !} ``` -## 3. Inject dependencies +## 3. Initialize dependencies Calling `domain.init()` establishes connectivity with the underlying infra, testing access, and making them available for use by the rest of the system. @@ -51,3 +51,27 @@ makes it available for domain elements for further use. Refer to [Configuration handling](../configuration.md) to understand the different ways to configure the domain. + +## 4. Validate Domain Model + +In the final part of domain initialization, Protean performs additional setup +tasks on domain elements and also conducts various checks to ensure the domain +model is specified correctly. + +Examples of checks include: + +1. Resolving references that were specified as Strings, like: +```python +@domain.entity(part_of="User") +class Account: + ... +``` + +2. Setting up Aggregate clusters and their shared settings. The object graph +constructed earlier is used to homogenize settings across all elements under +an aggregate, like the stream category and database provider. + +3. Constructing a map of command and event types to reference when processing +incoming messages later. + +4. Various checks and validations to ensure the domain structure is valid. diff --git a/docs/guides/domain-definition/events.md b/docs/guides/domain-definition/events.md index 8ace9c61..9d6608ca 100644 --- a/docs/guides/domain-definition/events.md +++ b/docs/guides/domain-definition/events.md @@ -51,6 +51,31 @@ An event's metadata provides additional context about the event. The unique identifier of the event. The event ID is a structured string, of the format **....**. +#### `type` + +Type of the event. Format is `..`. +For e.g. `Shipping.OrderShipped.v1`. + +#### `fqn` + +The fully qualified name of the event. This is used internally by Protean +to resconstruct objects from messages. + +#### `kind` + +Represents the kind of object enclosed in an event store message. Value is +`EVENT` for Events. `Metadata` class is shared between Events and Commands, so +possible values are `EVENT` and `COMMAND`. + +#### `stream` + +Name of the event stream. E.g. Stream `user-1234` encloses messages related to +`User` aggregate with identity `1234`. + +#### `origin_stream` + +Name of the stream that originated this event or command. + #### `timestamp` The timestamp of event generation. @@ -82,7 +107,7 @@ The payload is made available as the data in the event. If you want to extract just the payload, you can use the `payload` property of the event. -```shell hl_lines="17 19-20" +```shell hl_lines="22 24-25" In [1]: user = User(id="1", email="", name="") In [2]: user.login() @@ -90,15 +115,20 @@ In [2]: user.login() In [3]: event = user._events[0] In [4]: event -Out[4]: +Out[4]: In [5]: event.to_dict() Out[5]: -{'_metadata': {'id': '002.User.v1.1.0.1', - 'timestamp': '2024-06-30 19:20:53.587542+00:00', +{'_metadata': {'id': '002::user-1-0.1', + 'type': '002.UserLoggedIn.v1', + 'fqn': '002.UserLoggedIn', + 'kind': 'EVENT', + 'stream': '002::user-1', + 'origin_stream': None, + 'timestamp': '2024-07-18 22:02:32.522360+00:00', 'version': 'v1', 'sequence_id': '0.1', - 'payload_hash': 5473995227001335107}, + 'payload_hash': 2731902408806877088}, 'user_id': '1'} In [6]: event.payload @@ -123,10 +153,10 @@ class UserActivated: activated_at = DateTime(required=True) ``` -The configured version is reflected in `version` and `id` attributes of the -generated event: +The configured version is reflected in `version` and `type` attributes of the +generated event's metadata: -```python hl_lines="34 50 52 66 68" +```python hl_lines="32 49 55 70 76" {! docs_src/guides/domain-definition/events/002.py !} ``` diff --git a/docs/patterns/command-idempotency.md b/docs/patterns/command-idempotency.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/patterns/consuming-events-from-other-domains.md b/docs/patterns/consuming-events-from-other-domains.md new file mode 100644 index 00000000..293a5540 --- /dev/null +++ b/docs/patterns/consuming-events-from-other-domains.md @@ -0,0 +1,2 @@ +Transform an event into a event that our domain understands. +A subscriber will consume event from a broker, transform it, and persist it into event store. \ No newline at end of file diff --git a/docs_src/guides/domain-definition/events/001.py b/docs_src/guides/domain-definition/events/001.py index e2593600..b6e79311 100644 --- a/docs_src/guides/domain-definition/events/001.py +++ b/docs_src/guides/domain-definition/events/001.py @@ -3,7 +3,7 @@ from protean import Domain from protean.fields import Identifier, String -domain = Domain(__file__, load_toml=False) +domain = Domain(__file__, name="Authentication", load_toml=False) class UserStatus(Enum): diff --git a/docs_src/guides/domain-definition/events/002.py b/docs_src/guides/domain-definition/events/002.py index e207393c..26c5899c 100644 --- a/docs_src/guides/domain-definition/events/002.py +++ b/docs_src/guides/domain-definition/events/002.py @@ -4,7 +4,7 @@ from protean import BaseEvent, Domain from protean.fields import DateTime, Identifier, String -domain = Domain(__name__) +domain = Domain(__name__, name="Authentication", load_toml=False) @domain.aggregate @@ -45,11 +45,16 @@ class UserActivated: """ Output: { "_metadata": { - "id": "__main__.User.v1.1.0.1", - "timestamp": "2024-06-30 16:29:31.312727+00:00", + "id": "authentication::user-1-0.1", + "type": "Authentication.UserLoggedIn.v1", + "fqn": "__main__.UserLoggedIn", + "kind": "EVENT", + "stream": "authentication::user-1", + "origin_stream": null, + "timestamp": "2024-07-18 22:06:10.148226+00:00", "version": "v1", "sequence_id": "0.1", - "payload_hash": -7433283101704735063 + "payload_hash": 6154717103144054927 }, "user_id": "1" } @@ -61,15 +66,18 @@ class UserActivated: """ Output: { "_metadata": { - "id": "__main__.User.v2.1.0.2", - "timestamp": "2024-06-30 16:32:59.703965+00:00", + "id": "authentication::user-1-0.2", + "type": "Authentication.UserActivated.v2", + "fqn": "__main__.UserActivated", + "kind": "EVENT", + "stream": "authentication::user-1", + "origin_stream": null, + "timestamp": "2024-07-18 22:06:10.155603+00:00", "version": "v2", "sequence_id": "0.2", - "payload_hash": 7340170219237812824 + "payload_hash": -3600345200911557224 }, "user_id": "1", - "activated_at": "2024-06-30 16:32:59.704063+00:00" + "activated_at": "2024-07-18 22:06:10.155694+00:00" } """ - - print(json.dumps(user._events[1].payload, indent=4)) diff --git a/docs_src/guides/domain-definition/events/003.py b/docs_src/guides/domain-definition/events/003.py index 3e674367..8531b870 100644 --- a/docs_src/guides/domain-definition/events/003.py +++ b/docs_src/guides/domain-definition/events/003.py @@ -4,7 +4,7 @@ from protean.fields import HasOne, String from protean.utils.mixins import Message -domain = Domain(__file__, load_toml=False) +domain = Domain(__file__, name="Authentication", load_toml=False) @domain.aggregate(fact_events=True) @@ -28,7 +28,9 @@ class Account: # Persist the user domain.repository_for(User).add(user) - event_message = domain.event_store.store.read(f"user-fact-{user.id}")[0] + event_message = domain.event_store.store.read( + f"authentication::user-fact-{user.id}" + )[0] event = Message.to_object(event_message) print(json.dumps(event.to_dict(), indent=4)) @@ -36,21 +38,22 @@ class Account: """ Output: { "_metadata": { - "id": "user-fact-e97cef08-f11d-43eb-8a69-251a0828bbff-0.1", - "type": "User.UserFactEvent.v1", + "id": "authentication::user-fact-781c4363-5e7e-4c53-a599-2cb2dc428d96-0.1", + "type": "Authentication.UserFactEvent.v1", + "fqn": "protean.container.UserFactEvent", "kind": "EVENT", - "stream_name": "user-fact-e97cef08-f11d-43eb-8a69-251a0828bbff", - "origin_stream_name": null, - "timestamp": "2024-07-09 17:24:41.800475+00:00", + "stream": "authentication::user-fact-781c4363-5e7e-4c53-a599-2cb2dc428d96", + "origin_stream": null, + "timestamp": "2024-07-18 22:01:02.364078+00:00", "version": "v1", "sequence_id": "0.1", - "payload_hash": -1529271686230030119 + "payload_hash": 2754382941688457931 }, "_version": 0, "name": "John Doe", "email": "john.doe@example.com", "status": null, "account": null, - "id": "e97cef08-f11d-43eb-8a69-251a0828bbff" + "id": "781c4363-5e7e-4c53-a599-2cb2dc428d96" } """ diff --git a/mkdocs.yml b/mkdocs.yml index 2fb3fa62..4dffdc8e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,7 +67,6 @@ nav: - Protean: index.md - Start Here: start-here.md - Core Concepts: - - core-concepts/index.md - core-concepts/philosophy.md - core-concepts/whitepaper.md - core-concepts/analysis-model.md @@ -86,9 +85,9 @@ nav: - core-concepts/building-blocks/entities.md - core-concepts/building-blocks/value-objects.md - core-concepts/building-blocks/domain-services.md - # - core-concepts/building-blocks/commands.md - # - core-concepts/building-blocks/command-handlers.md - core-concepts/building-blocks/events.md + - core-concepts/building-blocks/commands.md + - core-concepts/building-blocks/command-handlers.md # - core-concepts/building-blocks/event-handlers.md # - core-concepts/building-blocks/repositories.md # - core-concepts/building-blocks/models.md @@ -219,7 +218,6 @@ nav: - adapters/database/index.md - adapters/database/postgresql.md - adapters/database/elasticsearch.md - - adapters/database/redis.md - Brokers: - adapters/broker/index.md - adapters/broker/redis.md