How a request flows
POST /api/v1/tournaments/{id}/start, end to end.
Step by step
-
Routing and parsing - FastAPI matches the path and validates the request body (
CreateTournamentRequestfor the POST that creates one; here there is no body). Pydantic checks shape, not business rules. -
Resolving the use case - the handler declares
use_case: Annotated[StartTournament, Depends(deps.start_tournament)]. That factory inhttp/dependencies.pyasks for two ports: aTournamentRepositoryand anEventPublisher. Both are placeholder dependencies thatbootstrapoverrode with real providers when it built the app. This is the only "magic" in the template and it is FastAPI's own, documented mechanism. -
Executing -
StartTournament.execute()loads the aggregate through the port, callstournament.start()on it, persists the returned aggregate, publishes the returned events. Four lines. No framework in sight. -
Domain logic -
Tournament.start()delegates toProgress.start(phases), which raisesTournamentAlreadyStartedif needed, and returns a newTournamentplus aTournamentStartedevent inside aDomainResult. Nothing is mutated. -
Persisting -
SqlAlchemyTournamentRepository.save()maps the aggregate to the row model and flushes. It never commits: the session it received belongs to the request. -
Events -
publish()only records the events in the request'sCollectedEvents. Nothing runs yet: handlers need committed state. -
Presenting - the router converts the domain object to
TournamentResponseand FastAPI serialises it. The domain object never reaches the wire directly. -
Transaction boundary -
TransactionMiddleware(inbootstrap/transaction.py) opened the session before routing. When the handler returns a2xx/3xxit commits before the response leaves the process; on4xx/5xxor an exception it rolls back; if the commit itself fails the client gets500 TransactionFailedinstead of a false success. See ADR-004. -
Event dispatch - only after a successful commit,
EventDispatchMiddlewarehands the collected events toInProcessEventBus, which runs the subscribers in order (in the example, one that logs "Tournament is live"). A failing subscriber is logged with the request id and does not change the response; a rolled-back request dispatches nothing. See ADR-005.
What happens on errors
| Raised where | Exception | Becomes |
|---|---|---|
| Pydantic (shape) | RequestValidationError | 422 with FastAPI's detail list |
| domain | DomainError subclass | 422 {"error": "TournamentAlreadyStarted", "message": ...} |
| application | NotFoundError subclass | 404 {"error": "TournamentNotFound", ...} |
| application | ForbiddenError | 403 (only the organizer or an admin runs a tournament) |
| repository | ConflictError | 409 (someone else saved a newer version first; reload and retry) |
| http adapter | HTTPException(401) | 401 from shared/http/auth.py when API_KEYS is set |
| application | other ApplicationError | 409 |
| framework | unknown route / wrong method | 404 / 405 in the envelope (405 keeps Allow) |
| anywhere | anything else | 500 in the envelope, logged with traceback and request id |
See Errors for the reasoning.