Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 10 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,20 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Cache uv dependencies
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install dependencies
run: uv sync --locked --all-extras --dev

- name: Run Ruff linting
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
args: check --output-format=github

- name: Run Ruff formatting check
uses: astral-sh/ruff-action@v3
uses: astral-sh/ruff-action@v4.0.0
with:
args: format --check

Expand All @@ -64,14 +59,9 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Cache uv dependencies
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install dependencies
run: uv sync --locked --all-extras --dev
Expand All @@ -94,14 +84,9 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Cache uv dependencies
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install dependencies
run: uv sync --locked --all-extras --dev
Expand All @@ -124,14 +109,9 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Cache uv dependencies
uses: actions/cache@v5
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
restore-keys: |
uv-${{ runner.os }}-
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install dependencies
run: uv sync --locked --all-extras --dev
Expand Down
131 changes: 124 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,80 @@
# delivery
Domain Driven Design and Clean Architecture course
# Delivery Service

<img width="1655" height="784" alt="image" src="https://github.com/user-attachments/assets/4590d5eb-08b0-458e-ae62-7cbd86e3e4ba" />
A microservice responsible for delivery management — assigning orders to couriers, tracking courier movement, and coordinating delivery lifecycle.

Built with **Domain-Driven Design (DDD)** tactical patterns and **Clean Architecture** to keep business logic isolated, testable, and framework-agnostic.

Command to run models generation based on the OpenAPI specification:
---

## Context

The Delivery service is part of a larger system and communicates with:

- **Geo Service** — provides geographic data (coordinates by address) via gRPC
- **Basket Service** — publishes events when a new basket/order is created (consumed via Kafka)
- **Other consumers** — Delivery publishes domain events (order assigned, order completed) to Kafka

The main goal of this project is applying DDD tactical patterns:

- **Aggregates** — `Courier`, `Order` with enforced invariants
- **Entities** — with identity and lifecycle
- **Value Objects** — `Location` (immutable, equality by value)
- **Domain Events** — `OrderAssigned`, `OrderCompleted`
- **Domain Services** — logic that spans multiple aggregates
- **Repository pattern** — persistence abstraction via ports
- **Unit of Work** — transactional consistency
---

## Architecture

The project follows layered Clean Architecture with strict dependency direction (outer → inner):

```
┌─────────────────────────────────────┐
│ Adapters (HTTP, Kafka, gRPC, DB) │
└──────────────────┬──────────────────┘
│ depends on
┌─────────────────────────────────────┐
│ Application (Use Cases, Commands) │
└──────────────────┬──────────────────┘
│ depends on
┌─────────────────────────────────────┐
│ Domain (Entities, Value Objects, │
│ Aggregates, Domain Events) │
└─────────────────────────────────────┘
```
---

## Technology Stack

| Layer | Technology |
|-------|--------------------------------------------------------------|
| Language | Python ≥ 3.13 (strict type hints) |
| Web Framework | [Litestar](https://litestar.dev/) |
| DI Container | [Dishka](https://github.com/reagento/dishka) |
| Message Broker | Kafka via [FastStream](https://github.com/airtai/faststream) |
| Database | PostgreSQL (async, psycopg) |
| ORM | [SQLAlchemy 2.0](https://www.sqlalchemy.org) |
| Migrations | [Alembic](https://alembic.sqlalchemy.org/en/latest/) |
| Scheduling | [APScheduler](https://github.com/agronholm/apscheduler) |
| RPC | gRPC (geo service communication) |
| Serialization | Pydantic v2, Protobuf |
| Lint/Format | [Ruff](https://docs.astral.sh/ruff/) |
| Package Manager | [uv](https://docs.astral.sh/uv/) |
| Testing | [Polyfactory](https://github.com/litestar-org/polyfactory) (test data factories) |
| Infrastructure | Docker, Docker Compose |

---

## Service Contracts

We treat **contracts as borders** between services. Code is generated from formal specifications — never hand-written:

### OpenAPI (HTTP)

REST API schemas are generated from `contract.yaml` using `datamodel-codegen`:

```bash
datamodel-codegen \
Expand All @@ -25,18 +95,65 @@ datamodel-codegen \
--formatters ruff-check ruff-format
```

### gRPC (Geo Service)

Command to generate grpc client based on the protobuf definition:
gRPC client stubs are generated from `.proto` definitions using `grpc_tools`:

```bash
cd src/delivery_app/adapters/output/grpc/geo
```

```bash
python -m grpc_tools.protoc \
--python_out=./pb \
--grpc_python_out=./pb \
--pyi_out=./pb \
--proto_path=./protos \
./protos/*
```

---

## Project Structure

```
src/
├── delivery_app/
│ ├── core/
│ │ ├── domain/ # Entities, Value Objects, Aggregates, Domain Events, Errors
│ │ ├── application/ # Use Cases (Commands/Queries), Application Services
│ │ └── ports/ # Interfaces (Repositories, UoW, External Services)
│ ├── adapters/
│ │ ├── input/
│ │ │ ├── http/ # Litestar controllers, OpenAPI contract & schemas
│ │ │ └── kafka/ # Kafka consumers (basket events)
│ │ └── output/
│ │ ├── postgres/ # SQLAlchemy repositories, UoW, Alembic migrations
│ │ ├── kafka/ # Kafka producers (integration events)
│ │ ├── grpc/ # gRPC client (geo service)
│ │ └── scheduler/ # APScheduler jobs (assign orders, move couriers, outbox)
│ └── di/ # Dishka DI container providers
├── config/ # App settings (12-factor, env-based)
└── main.py # Application entrypoint
```

---

## Running

```bash
docker compose up -d # Start PostgreSQL
uv run alembic upgrade head # Apply migrations
uv run python src/main.py # Start the service
```

---

## Testing

```bash
uv run pytest
```

Tests are organized by layer:
- `tests/unit/core/domain/` — pure domain logic
- `tests/unit/core/application/` — use cases with fakes/mocks
- `tests/integration/` — adapters with real infrastructure
3 changes: 1 addition & 2 deletions src/delivery_app/adapters/input/kafka/basket/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import uuid

from dishka import FromDishka
from dishka.integrations.faststream import inject
from dishka_faststream import FromDishka, inject

from src.config import settings
from src.delivery_app.adapters.kafka_broker import kafka_broker
Expand Down
2 changes: 2 additions & 0 deletions src/delivery_app/default_domain_event_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@


class DefaultDomainEventPublisher:
"""Default implementation of a domain event publisher that uses Litestar's event system to publish domain events."""

def __init__(self, publisher: Litestar):
self._publisher = publisher

Expand Down