A backend service demonstrating production-grade system design: clean architecture, domain-driven design, RBAC, event-driven notifications, and explicit workflow state machines — patterns drawn from real-world SaaS products.
The system is organized around a strict layered architecture. Each layer has a single responsibility and no layer reaches past its immediate neighbor.
flowchart LR
Client[Client / Swagger UI]
API[FastAPI\nAPI Layer]
Domain[Domain Services\n& Policies]
DB[(PostgreSQL)]
Redis[(Redis Queue)]
Worker[Notification\nWorker]
Client --> API
API --> Domain
Domain --> DB
Domain --> Redis
Redis --> Worker
- API Layer — HTTP routing, request validation, authentication. Routes are intentionally thin and delegate all logic downward.
- Domain Services — All business logic and workflow transitions live here, independent of FastAPI.
- Policies — Authorization rules are extracted into a dedicated
TaskPolicyclass, keeping them testable and separate from business logic. - Infrastructure — Database sessions, JWT auth, and Redis publishing are isolated behind the infrastructure boundary.
- Worker — A standalone async process consuming the Redis notification queue, decoupled from the main request lifecycle.
Authorization logic lives in TaskPolicy rather than being scattered across routes or services. This keeps:
- permissions easy to test in isolation
- business logic focused on workflow, not access control
- future role expansion contained to a single, predictable place
Task status changes are controlled through an explicit state machine rather than allowing arbitrary status updates. Invalid transitions are rejected at the domain level. This mirrors how production approval workflows enforce data integrity — preventing states like jumping from draft directly to approved.
Redis-backed queue publishing was intentionally chosen as a lightweight first step:
- simpler infrastructure footprint
- sufficient to demonstrate async, event-driven architecture
- a clear and honest upgrade path to Celery for more complex job scheduling
Routes are thin by design. Business rules in the domain layer can be reused, tested independently, and remain decoupled from FastAPI's request/response model. This also makes future transport changes (e.g., adding a CLI or gRPC interface) straightforward.
Tasks move through a controlled, validated state machine:
Draft → In Progress → Pending Approval → Approved / Rejected → Completed
Valid transitions:
| From | To |
|---|---|
draft |
in_progress |
in_progress |
pending_approval |
pending_approval |
approved |
pending_approval |
rejected |
approved |
completed |
All other transitions are rejected.
| Role | Permissions |
|---|---|
member |
Create, start, and submit their own tasks |
manager |
Approve or reject tasks assigned to them |
admin |
All actions |
Every workflow action creates an immutable audit log entry capturing: entity type, entity ID, action, performer, metadata, and timestamp.
Logged events: task_started, task_submitted_for_approval, task_approved, task_rejected, task_completed
Workflow actions publish events to a Redis queue. A background worker consumes and processes them asynchronously, decoupled from the main API request cycle.
| Layer | Technology |
|---|---|
| API Framework | FastAPI |
| ORM | SQLAlchemy 2.0 |
| Database | PostgreSQL 17 |
| Migrations | Alembic |
| Async Queue | Redis 7 |
| Schema Validation | Pydantic |
| Testing | Pytest |
| Runtime | Python 3.13 |
| Containers | Docker / Docker Compose |
- User — authenticated actor with an assigned role
- Team — group of users tasks belong to
- Task — core entity with lifecycle state
- Approval — approval request linking a task to an approver
- AuditLog — immutable record of all domain events
- Docker & Docker Compose
- Python 3.13+
git clone https://github.com/mohanadtoaima/team-operations-service.git
cd team-operations-servicecp .env.example .envEdit .env as needed. Defaults work out of the box with Docker Compose.
docker compose up -dThis starts PostgreSQL, Redis, and the API. The API will be available at http://localhost:8000.
alembic upgrade headpython -m scripts.seedSeeded accounts:
| Role | Password | |
|---|---|---|
| Admin | [email protected] | admin123 |
| Manager | [email protected] | manager123 |
| Member | [email protected] | member123 |
Swagger UI: http://localhost:8000/docs
To authenticate in Swagger: use OAuth2 password flow with email as username. Leave client_id and client_secret empty.
python -m app.workers.notification_workerThe worker listens on notifications:queue and logs consumed events:
[NOTIFICATION WORKER] type=task_approved recipient=3 task=5 message=Manager User approved task 'Prepare weekly report'.
1. Login as member → POST /auth/login
2. Create a task → POST /tasks
3. Start the task → POST /tasks/{id}/start
4. Submit for approval → POST /tasks/{id}/submit-for-approval { "approver_id": 2 }
5. Login as manager → POST /auth/login
6. Approve or reject → POST /tasks/{id}/approve | POST /tasks/{id}/reject
7. Login as member → POST /auth/login
8. Complete the task → POST /tasks/{id}/complete
9. Inspect audit logs → GET /audit-logs
POST /tasks
{
"title": "Prepare weekly report",
"description": "Compile weekly project status",
"team_id": 1,
"assigned_to": 3,
"due_date": null
}POST /tasks/{task_id}/submit-for-approval
{
"approver_id": 2
}POST /tasks/{task_id}/reject
{
"comment": "Needs more details in section 2"
}pytestThe test suite covers:
- Domain service logic
- Policy enforcement
- Authentication
- End-to-end task workflow (integration tests)
app/
├── api/
│ ├── dependencies/
│ └── routes/
├── domain/
│ ├── exceptions/
│ ├── models/
│ ├── policies/
│ └── services/
├── infrastructure/
│ ├── auth/
│ ├── db/
│ └── notifications/
├── schemas/
├── workers/
└── main.py
scripts/
├── seed.py
├── seed_users.py
└── seed_teams.py
tests/
├── domain/
└── integration/
- Multi-step approval chains (multiple approvers per task)
- WebSocket or email notifications via a real notification provider
- Pagination and filtering on task listing endpoints
This project is for portfolio and learning purposes.