A geo-aware delivery orchestration backend β from order creation to doorstep, with automatic driver matching and self-healing reassignment built in.
Built as a self-directed project to explore how delivery-logistics platforms actually solve dispatch: nearest-driver matching, hot-path location data, timeout-driven reassignment, and the operational edge cases that come with all three.
An admin onboards drivers. A delivery request comes in. The system finds the closest available, capacity-eligible driver to the warehouse, assigns the order, and gives them a window to respond. If they don't β or if they reject it β the system finds the next-best driver automatically, with no manual intervention. The driver tracks the order through pickup and delivery via a small state machine on both the order and the assignment.
That loop β match β assign β timeout/reject β reassign β is the core of the project.
flowchart LR
Client["Client / Driver App"] -->|HTTPS + JWT| API["FastAPI"]
API -->|orders, drivers, assignments<br/>source of truth| PG[("PostgreSQL")]
API -->|live GPS pings| Redis[("Redis")]
API -->|distance lookups| Redis
Scheduler["APScheduler<br/>(expiry + rejection jobs)"] -->|poll every 5s| PG
Scheduler -->|reassign on timeout/rejection| PG
The split between Postgres and Redis is deliberate, not incidental:
- Postgres holds everything that needs to be durable and queryable β orders, drivers, assignments, their full history.
- Redis holds only the one thing that changes constantly and doesn't need history: a driver's current lat/lng. Pushing every GPS ping into Postgres would mean constant writes to a row something else is also trying to read and lock; Redis takes that churn off the relational store entirely.
Two background jobs poll Postgres every 5 seconds, independent of any incoming request, and handle the cases where a driver doesn't respond in time:
expired_assignmentsβ picks up anyPENDINGassignment past its 3-minute window, finds the next-closest available driver, and reassigns.rejected_assignmentsβ picks upREJECTEDassignments, excludes every driver who's already turned that order down, and reassigns from the remaining pool.
Order
stateDiagram-v2
[*] --> CREATED
CREATED --> ASSIGNED: driver accepts assignment
ASSIGNED --> PICKED_UP: driver picks up
PICKED_UP --> DELIVERED: driver delivers
CREATED --> EXCEPTION: no eligible driver found
ASSIGNED --> EXCEPTION: reassignment pool exhausted
(ADDRESS_VERIFICATION_PENDING is defined on the model as a reserved future state β not yet wired into the live flow.)
Assignment
stateDiagram-v2
[*] --> PENDING
PENDING --> ACCEPTED: driver accepts
PENDING --> REJECTED: driver rejects
PENDING --> EXPIRED: 3-minute timeout (background job)
REJECTED --> [*]: order reassigned, excluding this driver
EXPIRED --> [*]: order reassigned
When an order is created:
- Query every driver who is
available, has enoughmax_capacity_kgfor the order's weight, and operates out of a city served by the order's warehouse. - For each candidate, pull their live location from Redis and compute the great-circle distance to the warehouse via the Haversine formula.
- Sort by distance, assign to the closest, open a 3-minute response window.
Distance is calculated driver β warehouse, not driver β delivery address β the driver has to pick up the order before they can deliver it, so that's the leg that actually determines response time.
| Layer | Choice |
|---|---|
| API framework | FastAPI |
| Database | PostgreSQL via SQLAlchemy ORM |
| Hot-path store | Redis (live driver location) |
| Auth | JWT (python-jose) + bcrypt password hashing (passlib) |
| Background jobs | APScheduler |
| Geo distance | Haversine |
| Validation | Pydantic v2 |
| Testing | pytest, isolated SQLite test DB, in-memory Redis fake |
.
βββ main.py # app entrypoint β wires routers + schedulers
βββ database.py # SQLAlchemy engine/session setup
βββ models/ # ORM models
β βββ users.py # auth identity, role (ADMIN/DRIVER)
β βββ drivers.py # driver profile, vehicle, capacity
β βββ warehouses.py / cities.py
β βββ orders.py # order + OrderStatus state machine
β βββ assignments.py # orderβdriver assignment + AssignmentStatus
βββ routes/
β βββ admin.py # login, create_driver, JWT issuing/validation
β βββ drivers.py # shift mgmt, location push, accept/reject
β βββ order.py # order creation, nearest-driver matching, pickup/deliver
βββ pydanticValidations/ # request schemas, separate from ORM models
βββ jobs/
β βββ expired_assignments.py # timeout-driven reassignment
β βββ rejected_assignments.py # rejection-driven reassignment
βββ tests/ # pytest suite
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /auth/login |
β | OAuth2 password login, returns a JWT |
| POST | /auth/create_driver |
Admin | Creates a User + Driver profile in one transaction |
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /drivers/driver_location |
Driver | Pushes live lat/lng to Redis |
| PUT | /drivers/start_shift |
Driver | Marks driver available |
| PUT | /drivers/end_shift |
Driver | Marks driver unavailable |
| GET | /drivers/my_assignments |
Driver | Lists this driver's pending assignments |
| POST | /drivers/accept_assignment/{id} |
Driver | Accepts; order moves to ASSIGNED |
| POST | /drivers/reject_assignment/{id} |
Driver | Rejects; picked up by the rejection job |
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /order/create_order |
β | Creates an order, auto-assigns the nearest eligible driver |
| PUT | /order/pickup_order/{id} |
Driver | Marks order PICKED_UP |
| PUT | /order/deliver_order/{id} |
Driver | Marks order DELIVERED |
Prerequisites: Python 3.12+, a running PostgreSQL instance, a running Redis instance.
git clone <repo-url>
cd "Smart Delivery management system"
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install fastapi uvicorn sqlalchemy python-dotenv passlib bcrypt \
python-jose redis haversine apscheduler python-multipartCreate a .env in the project root:
| Variable | Description |
|---|---|
DATABASE_URL |
SQLAlchemy connection string, e.g. postgresql://user:pass@localhost:5432/sdms |
SECRET_KEY |
JWT signing secret β generate with openssl rand -hex 32 |
ALGORITHM |
JWT signing algorithm, e.g. HS256 |
Run it:
uvicorn main:app --reloadInteractive API docs at http://localhost:8000/docs.
pip install pytest python-multipart
python -m pytest tests/ -v28 tests covering auth and role checks, shift management, the full order lifecycle (create β match β accept β pickup β deliver), and both background reassignment jobs β including their EXCEPTION fallback paths when no driver is left to assign. Runs against an isolated SQLite database and an in-memory Redis fake; no external services required.
Documented intentionally rather than discovered later:
- No row-level locking between the accept/reject endpoints and the background expiry job β a driver accepting at the exact moment their assignment expires can produce an inconsistent state. Fix:
SELECT ... FOR UPDATEaround the read-modify-write. - No composite index on
assignments(status, expires_at)β the exact predicate the 5-second poller filters on. Fine at current scale, becomes a full table scan as data grows. get_available_driversfilters onDriver.latitude/longitude(Postgres columns), but no route currently writes to them β only Redis receives live location updates. Driver eligibility currently depends on those columns being seeded some other way.- In-process
APSchedulerdoesn't survive horizontal scaling β running multiple app instances would duplicate-process the same expired/rejected assignments. A distributed setup (Celery beat + worker, or a leader-elected lock) would be the next step. /order/create_orderhas no auth requirement β fine for local development, would need an internal-service auth layer before any real exposure.- No pinned
requirements.txtyet.
MIT β use freely as a reference or starting point.