Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

48 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Python Domain-Driven-Design(DDD) Example

Intro

I've adopted the DDD pattern for my recent FastAPI project. DDD makes it easier to implement complex domain problems. Improved readability and easy code fix have significantly improved productivity. As a result, stable but flexible project management has become possible. I'm very satisfied with it, so I'd like to share this experience and knowledge.

Why DDD?

Using DDD makes it easy to maintain collaboration with domain experts, not only engineers.

  • It is possible to prevent the mental model and the actual software from being dualized.
  • Business logic is easy to manage.
  • Infrastructure change is flexible.

Objective

  • Let's create a simple hotel reservation system and see how each component of DDD is implemented.
  • Don't go too deep into topics like event sourcing.
  • Considering the running curve, this project consists only of essential DDD components.

Implementation

ERD

NOTES: The diagram below represents only the database tables.

erd

Bounded Context

bounded-context

  • Display(Handling tasks related to the hotel room display)
    • List Rooms
  • Reception(Handling tasks related to the hotel room reservation)
    • Make a reservation
    • Change the reservation details
    • Cancel a reservation
    • Check-in & Check-out

Reservation and reception can also be isolated, but let's say that reception handles it altogether for now.

Project Structure

src
├── reception
│ ├── application
│ │ └── use_case
│ │ ├── query
│ │ └── command
│ ├── domain
│ │ ├── entity
│ │ ├── exception
│ │ ├── service
│ │ └── value_object
│ ├── infra
│ │ ├── repository
│ │ └── external_apis
│ └── presentation
│ ├── grpc
│ └── rest
│ ├── request
│ └── response
├── display
│ ├── application
│ ├── domain
│ ├── infra
│ └── presentation
└── shared_kernel
├── domain
└── infra
├── database
├── fastapi
└── log

DDD Components

1. Entity: Definition

fromdataclassesimportdataclass, fieldclassEntity:
id: int=field(init=False)
def__eq__(self, other: Any) ->bool:
ifisinstance(other, type(self)):
returnself.id==other.idreturnFalsedef__hash__(self):
returnhash(self.id)
classAggregateRoot(Entity):
pass@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
room: Roomreservation_number: ReservationNumberreservation_status: ReservationStatusdate_in: datetimedate_out: datetimeguest: Guest
  • Entity mix-in
    The entity is an object that have a distinct identity. I will implement __eq__() and __hash__(), to use it as a mix-in for dataclass.

  • AggregateRoot mix-in
    A DDD aggregate is a cluster of domain objects that can be treated as a single unit. An aggregate root is an entry point of an aggregate. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole. I will define an empty class called AggregateRoot and explicitly mark it.

  • Entity Implementation
    To use __eq__() from Entity mix-in, add eq=False. From Python 3.10, slots=True makes dataclass more memory-efficient.

  • Value Object
    With sqlalchemy, you can use value objects within entity when reading & saving data from a repository. I will introduce the details later.

2. Entity: Life Cycle

@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...@classmethoddefmake(
cls, room: Room, date_in: datetime, date_out: datetime, guest: Guest
) ->Reservation:
room.reserve()
returncls(
room=room,
date_in=date_in,
date_out=date_out,
guest=guest,
reservation_number=ReservationNumber.generate(),
reservation_status=ReservationStatus.IN_PROGRESS,
)
defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLEDdefcheck_in(self):
# ...defcheck_out(self):
# ...defchange_guest(self, guest: Guest):
# ...

By implementing the method according to the entity's life cycle, you can expect how it evolves when reading it.

  • Creation
    Declare a class method and use it when creating an entity.

  • Changes
    Declare an instance method and use it when changing an entity.

3. Entity: Table Mapping

NOTE: This is the most beautiful part of implementing DDD with sqlalchemy.

fromsqlalchemyimportMetaData, Table, Column, Integer, String, Text, ForeignKey, DateTimefromsqlalchemy.ormimportregistrymetadata=MetaData()
mapper_registry=registry()
room_table=Table(
"hotel_room",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("image_url", String(200), nullable=False),
Column("description", Text, nullable=True),
UniqueConstraint("number", name="uix_hotel_room_number"),
)
reservation_table=Table(
"room_reservation",
metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("room_id", Integer, ForeignKey("hotel_room.id"), nullable=False),
Column("number", String(20), nullable=False),
Column("status", String(20), nullable=False),
Column("date_in", DateTime(timezone=True)),
Column("date_out", DateTime(timezone=True)),
Column("guest_mobile", String(20), nullable=False),
Column("guest_name", String(50), nullable=True),
)
definit_orm_mappers():
fromreception.domain.entity.roomimportRoomasReceptionRoomEntityfromreception.domain.entity.reservationimportReservationasReceptionReservationEntitymapper_registry.map_imperatively(
ReceptionRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
mapper_registry.map_imperatively(
ReceptionReservationEntity,
reservation_table,
properties={
"room": relationship(
Room, backref="reservations", order_by=reservation_table.c.id.desc, lazy="joined"
),
"reservation_number": composite(ReservationNumber.from_value, reservation_table.c.number),
"reservation_status": composite(ReservationStatus.from_value, reservation_table.c.status),
"guest": composite(Guest, reservation_table.c.guest_mobile, reservation_table.c.guest_name),
}
)
fromdisplay.domain.entity.roomimportRoomasDisplayRoomEntitymapper_registry.map_imperatively(
DisplayRoomEntity,
room_table,
properties={
"room_status": composite(RoomStatus.from_value, room_table.c.status),
}
)
# call this after app runninginit_orm_mappers()

Because entities do not need to know the implementation of the database table, let's use sqlalchemy's imperative mapping to separate entity definitions and table definitions.

Because name conflicts can occur when mapping tables and entities, the number is replaced like reservation_number.

If you want to keep using the number as it is, you can change the original number to _number first.

@dataclass(eq=False, slots=True)classRoom(Entity):
number: strroom_status: RoomStatus

Entities only need to use logically required data among the columns defined in the table. For example, in the reservation domain, you don't need to know the image of the room, so only name, status is defined in the room.

4. Value Object

frompydanticimportconstrmobile_type=constr(regex=r"\+[0-9]{2,3}-[0-9]{2}-[0-9]{4}-[0-9]{4}")
@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=None

A value object is an object that matter only as the combination of its attributes. Guest A's name and mobile should be treated as a single unit, so make it a value object.

Using sqlalchemy's composite column type, it allows you to implement value objects by changing columns to an object that fits your needs when you load data.

Let's define the mix-in as follows and inherit it when implementing a value object.

classValueObject:
def__composite_values__(self):
returnself.value,
@classmethoddeffrom_value(cls, value: Any) ->ValueObjectType|None:
ifisinstance(cls, EnumMeta):
foritemincls:
ifitem.value==value:
returnitemraiseValueObjectEnumErrorinstance=cls(value=value)
returninstance

If you define the __composite_values_() method, sqlalchemy separates the object and puts them in the columns when you save the data.

NOTE: The , in the return of __composite_value__() is not a typo.

classRoomStatus(ValueObject, str, Enum):
AVAILABLE="AVAILABLE"RESERVED="RESERVED"OCCUPIED="OCCUPIED"@dataclass(slots=True)classReservationNumber(ValueObject):
DATETIME_FORMAT: ClassVar[str] ="%y%m%d%H%M%S"RANDOM_STR_LENGTH: ClassVar[int] =7value: str@classmethoddefgenerate(cls) ->ReservationNumber:
time_part: str=datetime.utcnow().strftime(cls.DATETIME_FORMAT)
random_strings: str=''.join(
random.choice(string.ascii_uppercase+string.digits) for_inrange(cls.RANDOM_STR_LENGTH)
)
returncls(value=time_part+":"+random_strings)

ReservationNumber intentionally used the name value for a single attribute to leverage __composite_values__() in ValueObject class.

@dataclass(slots=True)classGuest(ValueObject):
mobile: mobile_typename: str|None=Nonedef__composite_values__(self):
returnself.mobile, self.name

If a value object consists of more than one column, you must override the __composite_values__() as shown above.

5. Exception

classReservationStatusException(BaseMsgException):
message="Invalid request for current reservation status."@dataclass(eq=False, slots=True)classReservation(AggregateRoot):
# ...defcancel(self):
ifnotself.reservation_status.in_progress:
raiseReservationStatusExceptionself.reservation_status=ReservationStatus.CANCELLED

By defining and using domain exceptions, the cohesion can be increased.

Dependency Injection

FastAPI's Depends makes it easy to implement Dependency Injection between layers. And you can achieve Inversion of control with Dependency Injector.

@router.get("/reservations/{reservation_number}")@injectdefget_reservation(
reservation_number: str,
reservation_query: ReservationQueryUseCase=Depends(
Provide[AppContainer.reception.reservation_query]
),
):
try:
reservation: Reservation=reservation_query.get_reservation(
reservation_number=reservation_number
)
exceptReservationNotFoundExceptionase:
raiseHTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=e.message,
)
returnReservationResponse(
detail="ok",
result=ReservationSchema.build(reservation=reservation),
)
classReservationQueryUseCase:
def__init__(
self,
reservation_repo: ReservationRDBRepository,
db_session: Callable[[], ContextManager[Session]],
):
self.reservation_repo=reservation_repoself.db_session=db_sessiondefget_reservation(self, reservation_number: str) ->Reservation:
reservation_number=ReservationNumber.from_value(value=reservation_number)
withself.db_session() assession:
reservation: Reservation|None= (
self.reservation_repo.get_reservation_by_reservation_number(
session=session, reservation_number=reservation_number
)
)
ifnotreservation:
raiseReservationNotFoundExceptionreturnreservation
classReservationRDBRepository(RDBRepository):
@staticmethoddefget_reservation_by_reservation_number(
session: Session, reservation_number: ReservationNumber
) ->Reservation|None:
returnsession.query(Reservation).filter_by(reservation_number=reservation_number).first()

Schema

Pydantic makes it easy to implement the request and response schema.

classCreateReservationRequest(BaseModel):
room_number: strdate_in: datetimedate_out: datetimeguest_mobile: mobile_typeguest_name: str|None=None
classReservationSchema(BaseModel):
room: RoomSchemareservation_number: strstatus: ReservationStatusdate_in: datetimedate_out: datetimeguest: GuestSchema@classmethoddefbuild(cls, reservation: Reservation) ->ReservationSchema:
returncls(
room=RoomSchema.from_entity(reservation.room),
reservation_number=reservation.reservation_number.value,
status=reservation.reservation_status,
date_in=reservation.date_in,
date_out=reservation.date_out,
guest=GuestSchema.from_entity(reservation.guest),
)
classReservationResponse(BaseResponse):
result: ReservationSchema

Run server

$ uvicorn shared_kernel.infra.fastapi.main:app --reload

Requirements

  • Python 3.10+
    • 3.10 and lower versions can also take the key concepts

About

Python Domain-Driven-Design(DDD) Example

Resources

Stars

455 stars

Watchers

11 watching

Forks

Releases

Packages

Used by

Contributors

Languages