Building Blocks¶
In this section we will define the basic building blocks that will be used by the core module of the application, namely the todos module.
Domain model¶
Since the todos module is responsible for adding and completing todos,
we need to define a TodoModel
that will encapsulate all logic related to a todo:
@dataclass
class TodoModel:
"""Model representing a todo"""
id: UUID
title: str
description: str = ""
due_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
@property
def is_completed(self) -> bool:
return self.completed_at is not None
def is_due(self, now: datetime) -> bool:
if self.due_at is None or self.is_completed is False:
return False
return self.due_at < now
def mark_as_completed(self, when: datetime) -> None:
self.completed_at = when
From this point we will refer to TodoModel instances as todo entities (or entities in short).
Read model¶
In addition to a TodoModel
, we will also create a corresponding TodoReadModel
:
@dataclass
class TodoReadModel:
"""Read model exposed to the external world"""
id: UUID
title: str
description: str
is_due: bool
is_completed: bool
Why do we need 2 models? TodoModel
serves as a domain model that encapsulates the business logic of a todo entity.
This model will be modified by command handlers defined within the module. In other words, only the todos module has the authority to change the
state of TodoModel
other modules are not permitted to access it.
On the other hand, TodoReadModel
is exposed outside the todos module. Other modules can query the todos module and
receive the read model. If other modules need to make changes, they should not directly access it.
Instead, they should request the modification by sending a message to the todos module.
Repository¶
Our design pattern of choice for storing and retrieving entities is the repository pattern.
Note
The repository pattern encapsulates the logic for accessing data within an application, providing a layer of abstraction between the data access code and the business logic. By abstracting away the details of data storage and retrieval, it promotes modularity, testability, and flexibility in software design.
In real applications, a repository is commonly associated with a fully-fledged database.
This includes managing DB connections, managing transactions with commit and rollback operations, etc.
However, for the sake of simplicity, we will implement the in-memory TodoRepository
as follows:
class TodoRepository:
"""A repository of todos"""
def __init__(self):
self.items: list[TodoModel] = []
def add(self, item: TodoModel) -> None:
self.items.append(item)
def get_by_id(self, todo_id: UUID) -> TodoModel:
for item in self.items:
if item.id == todo_id:
return item
raise ValueError(f"Todo with id {todo_id} does not exist")
def get_all(self) -> list[TodoModel]:
return self.items
def get_all_completed(self):
return [todo for todo in self.items if todo.is_completed]
def get_all_not_completed(self):
return [todo for todo in self.items if not todo.is_completed]
Having the basic building blocks in place, we are now ready to implement the todos module.