Skip to content

6. Runtime View

This chapter describes the runtime behavior of Tanker24 by documenting the most important usage scenarios as sequence diagrams. Each scenario illustrates the interaction between the user, frontend, backend, database, and external systems.

6.1 Scenario: Search Nearby Gas Stations (UC1, UC7)

The user opens the map page, selects a location, and the system returns nearby gas stations with current prices.

UserFrontendBackendPostgreSQLTankerk.nig APIUserUserFrontend(SvelteKit + Leaflet)Frontend(SvelteKit + Leaflet)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLTankerkönig APITankerkönig APIOpens the map page (/map)Gets user's current position(Geolocation API or map click)GET /api/v0/stations/nearby?latitude=X&longitude=Y(Authorization: Bearer <JWT>)Validates JWT token via fastapi-usersChecks rate limit (10/min per user)SlowAPI/memory rate limiterSELECT * FROM stationsWHERE cache_lat ≈ X AND cache_lon ≈ YAND cache_radius = radiusAND cached_at > now - 30 minCached stations (or empty)alt[Cache hit (fresh data)]Data is recent enough[Cache miss (stale or no data)]Acquires token from global rate limiter(100 req/min to Tankerkönig)GET list.php?lat=X&lng=Y&rad=5&type=all&apikey=KEYJSON: { "ok": true, "stations": [...] }UPSERT stations with cache metadata(lat, lon, radius, cached_at timestamp)DELETE stale stations not in new result setJSON: [{tankerkoenig_id, name, brand, diesel, e5, e10, distance, ...}]Renders markers on Leaflet mapwith fuel prices in popupsMap is displayed with station markers
UserFrontendBackendPostgreSQLTankerk.nig APIUserUserFrontend(SvelteKit + Leaflet)Frontend(SvelteKit + Leaflet)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLTankerkönig APITankerkönig APIOpens the map page (/map)Gets user's current position(Geolocation API or map click)GET /api/v0/stations/nearby?latitude=X&longitude=Y(Authorization: Bearer <JWT>)Validates JWT token via fastapi-usersChecks rate limit (10/min per user)SlowAPI/memory rate limiterSELECT * FROM stationsWHERE cache_lat ≈ X AND cache_lon ≈ YAND cache_radius = radiusAND cached_at > now - 30 minCached stations (or empty)alt[Cache hit (fresh data)]Data is recent enough[Cache miss (stale or no data)]Acquires token from global rate limiter(100 req/min to Tankerkönig)GET list.php?lat=X&lng=Y&rad=5&type=all&apikey=KEYJSON: { "ok": true, "stations": [...] }UPSERT stations with cache metadata(lat, lon, radius, cached_at timestamp)DELETE stale stations not in new result setJSON: [{tankerkoenig_id, name, brand, diesel, e5, e10, distance, ...}]Renders markers on Leaflet mapwith fuel prices in popupsMap is displayed with station markers

Steps: 1. User navigates to /map; the SvelteKit frontend loads the Leaflet map component.
2. The user selects a location (via geolocation or map click).
3. Frontend sends GET /api/v0/stations/nearby?latitude=X&longitude=Y with the JWT token in the Authorization header.
4. Backend validates the JWT and checks the user-based rate limit (10 requests per minute).
5. Backend queries the stations cache table for matching entries within the configured tolerance (0.01 km) and expiry (30 minutes).
6. If cache hit: cached stations are returned directly.
7. If cache miss: the backend acquires a token from the global Tankerkönig rate limiter (100 requests/minute), then calls the Tankerkönig list.php API. Results are upserted into the cache with metadata (search coordinates, radius, timestamp). Stale entries from previous searches are cleaned up.
8. Backend returns the station list as JSON with fuel prices and distances.
9. Frontend renders station markers on the Leaflet map with price information.

Error Handling: - If the Tankerkönig API is unavailable, the service catches the exception, logs it, and returns an empty list (graceful degradation). - If the user exceeds the rate limit, a 429 Too Many Requests response is returned. - If latitude or longitude are out of valid range, a 400 Bad Request is returned.

6.2 Scenario: User Registration (UC5)

A new user registers with an invitation key.

UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLNavigates to /registerfills form (email, password, forename, surname, invitation key)Client-side validation(password rules, required fields)POST /api/v0/auth/register{email, password, forename, surname, invitation_key}Validates password policy:- Min 8 chars- Upper + lower + digit + special charChecks if email already exists(UserAlreadyExists → 400)SELECT * FROM invitation_keys WHERE key = <key>InvitationKey (or None)alt[Invalid or missing invitation key]400 Invalid invitation key[Valid key]Hashes password with passlibINSERT INTO users (... invitation_key_id=...)Logs "User registered: id=X email=Y"201 Created {id, email, forename, surname, is_active, ...}Stores success message, redirects to /login"Registration successful. Please log in."
UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLNavigates to /registerfills form (email, password, forename, surname, invitation key)Client-side validation(password rules, required fields)POST /api/v0/auth/register{email, password, forename, surname, invitation_key}Validates password policy:- Min 8 chars- Upper + lower + digit + special charChecks if email already exists(UserAlreadyExists → 400)SELECT * FROM invitation_keys WHERE key = <key>InvitationKey (or None)alt[Invalid or missing invitation key]400 Invalid invitation key[Valid key]Hashes password with passlibINSERT INTO users (... invitation_key_id=...)Logs "User registered: id=X email=Y"201 Created {id, email, forename, surname, is_active, ...}Stores success message, redirects to /login"Registration successful. Please log in."

6.3 Scenario: Record Fuel Filling (UC3)

An authenticated user records a fuel filling event for one of their cars.

UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLOpens account page, selects a car,fills form (litres, price/litre, mileage, fuel type)POST /api/v0/fillings/create(Authorization: Bearer <JWT>)Validates JWT, extracts user IDCheck if the car already exists or create a new oneCar record (or 404)Get FuelTypes from database for foreign keyFuelType (diesel/e5/e10)Create history record with reference to car and fuel type200 OKUpdates history list on account page"Filling recorded. Total: €XX.XX"
UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLOpens account page, selects a car,fills form (litres, price/litre, mileage, fuel type)POST /api/v0/fillings/create(Authorization: Bearer <JWT>)Validates JWT, extracts user IDCheck if the car already exists or create a new oneCar record (or 404)Get FuelTypes from database for foreign keyFuelType (diesel/e5/e10)Create history record with reference to car and fuel type200 OKUpdates history list on account page"Filling recorded. Total: €XX.XX"

6.4 Scenario: Export User Data (UC4)

The authenticated user exports their fueling history as JSON or CSV.

UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLClicks "Export as JSON" or "Export as CSV"alt[JSON Export]GET /api/v0/export/json(Authorization: Bearer <JWT>)Validates JWT, extracts user IDSELECT cars WHERE owner_id = user_idList of carsloop[For each car]SELECT history_records WHERE car_id = XJOIN fuel_typesList of history records with fuel type namesNestedExportDataService builds JSON:[{car, history: [{record, fuel_type}]}]JSONResponse with Content-Disposition attachment[CSV Export]GET /api/v0/export/csv(Authorization: Bearer <JWT>)Validates JWT, extracts user IDSELECT cars, history, fuel_types (same as JSON)DataFlatExportDataService flattens to CSV rowswith car_id repeated per row(using semicolon delimiter)StreamingResponse (text/csv) with Content-Disposition attachmentTriggers browser downloadFile is downloaded
UserFrontendBackendPostgreSQLUserUserFrontend(SvelteKit)Frontend(SvelteKit)Backend(FastAPI)Backend(FastAPI)PostgreSQLPostgreSQLClicks "Export as JSON" or "Export as CSV"alt[JSON Export]GET /api/v0/export/json(Authorization: Bearer <JWT>)Validates JWT, extracts user IDSELECT cars WHERE owner_id = user_idList of carsloop[For each car]SELECT history_records WHERE car_id = XJOIN fuel_typesList of history records with fuel type namesNestedExportDataService builds JSON:[{car, history: [{record, fuel_type}]}]JSONResponse with Content-Disposition attachment[CSV Export]GET /api/v0/export/csv(Authorization: Bearer <JWT>)Validates JWT, extracts user IDSELECT cars, history, fuel_types (same as JSON)DataFlatExportDataService flattens to CSV rowswith car_id repeated per row(using semicolon delimiter)StreamingResponse (text/csv) with Content-Disposition attachmentTriggers browser downloadFile is downloaded

6.5 Scenario: Application Startup

Docker ComposeBackend .FastAPI.PostgreSQLDocker ComposeDocker ComposeBackend (FastAPI)Backend (FastAPI)PostgreSQLPostgreSQLdocker compose up -d(startup)setup_logging()Structured logging to stdoutSQLAlchemy: WARNING (INFO in debug)init_db()SQLAlchemy: Base.metadata.create_all(Creates tables if not existing)CREATE TABLE IF NOT EXISTS(users, cars, history_records,stations, invitation_keys, fuel_types)sync_invitation_keys()Reads INVITATION_KEYS from envSELECT * FROM invitation_keysExisting DB keysKey diff: env vs DBDELETE keys not in env(unset invitation_key_id for affected users)INSERT new keys from env"Application startup complete"Ready to serve requests on port 8000
Docker ComposeBackend .FastAPI.PostgreSQLDocker ComposeDocker ComposeBackend (FastAPI)Backend (FastAPI)PostgreSQLPostgreSQLdocker compose up -d(startup)setup_logging()Structured logging to stdoutSQLAlchemy: WARNING (INFO in debug)init_db()SQLAlchemy: Base.metadata.create_all(Creates tables if not existing)CREATE TABLE IF NOT EXISTS(users, cars, history_records,stations, invitation_keys, fuel_types)sync_invitation_keys()Reads INVITATION_KEYS from envSELECT * FROM invitation_keysExisting DB keysKey diff: env vs DBDELETE keys not in env(unset invitation_key_id for affected users)INSERT new keys from env"Application startup complete"Ready to serve requests on port 8000

Startup Steps: 1. Docker Compose starts the backend container (after PostgreSQL is healthy).
2. lifespan context manager calls setup_logging() to configure structured logging.
3. init_db() runs Base.metadata.create_all to create any missing tables (idempotent).
4. sync_invitation_keys() reads the INVITATION_KEYS environment variable (comma-separated 32-char hex strings), diff against the DB, removes expired keys, and adds new ones. Users with deleted keys have their invitation_key_id set to NULL.
5. The application starts serving requests on port 8000.