Hone logo
Hone
Problems

Implementing CQRS with Event Sourcing in Python

This challenge involves implementing the Command Query Responsibility Segregation (CQRS) pattern in Python, combined with event sourcing. You will build a system for managing a simple product inventory where updates are handled via commands and queries are executed against a read-optimized model. This exercise will help you understand how to separate write and read concerns, and how to leverage events to maintain consistency.

Problem Description

You are tasked with building a simplified product inventory management system using Python. The system should adhere to the Command Query Responsibility Segregation (CQRS) pattern. This means that the operations for changing the state of the system (commands) will be distinct from the operations for reading the state (queries).

Furthermore, the system will employ an event sourcing approach for its write model. Every change to the product inventory will be recorded as an immutable event. The read model will be built and maintained by processing these events.

Key Requirements:

  1. Command Handling:

    • Implement a mechanism to receive and process commands.
    • Commands should represent actions like CreateProduct, UpdateProductPrice, DeactivateProduct.
    • Each command should result in one or more domain events being generated.
  2. Event Sourcing (Write Model):

    • Define a set of domain events, such as ProductCreatedEvent, ProductPriceUpdatedEvent, ProductDeactivatedEvent.
    • An EventStore should persist these events. For this challenge, a simple in-memory list or dictionary can serve as the EventStore.
    • A Product aggregate root should be capable of applying events to reconstruct its state and generating new events.
  3. Query Handling:

    • Implement a mechanism to handle queries.
    • Queries should represent requests for product information, such as GetProductById, GetAllProducts.
    • Queries will read from a separate, read-optimized data structure (e.g., a dictionary or list representing the current state of products).
  4. Event Bus/Dispatcher:

    • An EventBus will be responsible for dispatching domain events to event handlers.
  5. Event Handlers (Read Model Projection):

    • Implement event handlers that subscribe to domain events.
    • These handlers will update the read model (projection) based on the events they receive. For example, a ProductCreatedEvent handler would add a new product to the read model.

Expected Behavior:

  • When a CreateProduct command is issued, a ProductCreatedEvent is generated, stored in the event store, and then processed by an event handler to update the read model.
  • When an UpdateProductPrice command is issued, a ProductPriceUpdatedEvent is generated, stored, and its corresponding event handler updates the price in the read model.
  • When a GetProductById query is executed, it should retrieve the current state of the product from the read model.

Edge Cases:

  • Querying for a product that has not been created.
  • Applying an event to a product that doesn't exist (this should ideally be prevented by command validation or handled gracefully).

Examples

Example 1: Product Creation and Retrieval

# Commands
create_product_command = CreateProductCommand(product_id="P123", name="Laptop", initial_price=1200.00)

# Process Command
# Assume command bus and event handlers are set up
# Event: ProductCreatedEvent(product_id="P123", name="Laptop", price=1200.00) is generated and stored.
# Read model is updated.

# Query
get_product_query = GetProductByIdQuery(product_id="P123")
# Assume query bus/handler retrieves from read model

Output:
{
    "product_id": "P123",
    "name": "Laptop",
    "price": 1200.00,
    "is_active": True
}
Explanation: The `CreateProductCommand` results in a `ProductCreatedEvent`. This event is processed by an event handler, which updates the read model. The `GetProductByIdQuery` then retrieves the product's current state from this read model.

Example 2: Product Price Update and Retrieval

# Existing state from Example 1:
# Read Model: {"P123": {"product_id": "P123", "name": "Laptop", "price": 1200.00, "is_active": True}}

# Command
update_price_command = UpdateProductPriceCommand(product_id="P123", new_price=1150.00)

# Process Command
# Event: ProductPriceUpdatedEvent(product_id="P123", new_price=1150.00) is generated and stored.
# Read model is updated.

# Query
get_product_query = GetProductByIdQuery(product_id="P123")

Output:
{
    "product_id": "P123",
    "name": "Laptop",
    "price": 1150.00,
    "is_active": True
}
Explanation: The `UpdateProductPriceCommand` generates a `ProductPriceUpdatedEvent`. The event handler updates the price in the read model. The subsequent query reflects this updated price.

Example 3: Product Deactivation and Retrieval

# Existing state from Example 2:
# Read Model: {"P123": {"product_id": "P123", "name": "Laptop", "price": 1150.00, "is_active": True}}

# Command
deactivate_command = DeactivateProductCommand(product_id="P123")

# Process Command
# Event: ProductDeactivatedEvent(product_id="P123") is generated and stored.
# Read model is updated.

# Query
get_product_query = GetProductByIdQuery(product_id="P123")

Output:
{
    "product_id": "P123",
    "name": "Laptop",
    "price": 1150.00,
    "is_active": False
}
Explanation: The `DeactivateProductCommand` leads to a `ProductDeactivatedEvent`. The event handler sets `is_active` to `False` in the read model. A query will now return the deactivated product.

# Query for a non-existent product
get_nonexistent_query = GetProductByIdQuery(product_id="P456")

Output:
None
Explanation: Querying for a product that has never been created or was deleted (if delete functionality was added) returns `None`.

Constraints

  • The EventStore can be an in-memory data structure (e.g., a list of events or a dictionary mapping aggregate IDs to lists of events).
  • The read model can also be an in-memory data structure.
  • Focus on the core CQRS and event sourcing logic rather than advanced persistence or concurrency control.
  • Commands and Events should be clearly defined classes or dataclasses.
  • The solution should be easily extensible to add new commands, events, and queries.

Notes

  • Consider using a simple pattern for dispatching commands to handlers and events to handlers (e.g., a registry or a simple dispatcher class).
  • The Product aggregate root is where the business logic for state changes resides. It should be responsible for validating commands and emitting events.
  • The event handlers are responsible for projecting events onto the read model. They should be idempotent.
  • Think about how to reconstruct the state of an aggregate from its history of events.
  • The primary goal is to demonstrate the architectural separation between command processing and query processing, and how event sourcing drives the read model.
Loading editor...
python