The API follows a classic layered architecture. The OpenAPI spec is the single source of truth for the HTTP contract — the generator produces interfaces and DTOs that the controller implements directly.
flowchart TD
Client([HTTP Client])
subgraph openapi ["OpenAPI Code Generation"]
Spec["sleep-api.yaml"]
Generated["Generated: SleepApi interface\nCreateSleepLogRequest, SleepLogResponse,\nSleepStatsResponse, MoodFrequencies, ErrorResponse"]
Spec --> Generated
end
subgraph controller ["Controller Layer"]
SC["SleepController\n(DTO ↔ Domain mapping)"]
GEH["GlobalExceptionHandler\n(Exception → HTTP status)"]
end
subgraph service ["Service Layer"]
SS["SleepServiceImpl\n• Validation (duration, overlap, today-check)\n• Circular-mean time averaging\n• Timezone-aware 'today' via Clock"]
end
subgraph repository ["Repository Layer"]
SLR["SleepLogRepository\n(interface)"]
UR["UserRepository\n(interface)"]
JSLR["JdbcSleepLogRepository"]
JUR["JdbcUserRepository"]
SLR --- JSLR
UR --- JUR
end
subgraph db ["PostgreSQL"]
PG["NamedParameterJdbcTemplate\nFlyway migrations · No ORM"]
end
subgraph domain ["Domain Models"]
Models["SleepLog · NewSleepLog · SleepStats · User · Mood"]
end
subgraph exceptions ["Exceptions"]
EX["ResourceNotFoundException → 404\nResourceConflictException → 409\nSleepLogInvalidException → 400"]
end
Client --> SC
Generated -.->|implements| SC
SC --> SS
SS --> SLR
SS --> UR
JSLR --> PG
JUR --> PG
exceptions -.->|caught by| GEH
PostgreSQL 13 with Flyway-managed migrations. The timezones table is a reference table seeded from
pg_timezone_names. The sleep_logs table uses a GiST exclusion constraint to prevent overlapping sleep ranges per
user at the database level.
erDiagram
timezones {
TEXT name PK "Seeded from pg_timezone_names"
}
users {
BIGINT id PK "GENERATED ALWAYS AS IDENTITY"
TEXT timezone FK "References timezones(name)"
TIMESTAMPTZ created_at "DEFAULT NOW()"
TIMESTAMPTZ updated_at "Trigger: set_updated_at()"
}
sleep_logs {
BIGINT id PK "GENERATED ALWAYS AS IDENTITY"
BIGINT user_id FK "References users(id) ON DELETE CASCADE"
mood_type mood "ENUM: BAD, OK, GOOD"
TIMESTAMPTZ bed_time
TEXT bed_timezone FK "References timezones(name)"
TIMESTAMPTZ wake_time
TEXT wake_timezone FK "References timezones(name)"
TIMESTAMPTZ created_at "DEFAULT NOW()"
TIMESTAMPTZ updated_at "Trigger: set_updated_at()"
}
timezones ||--o{ users: "timezone"
timezones ||--o{ sleep_logs: "bed_timezone / wake_timezone"
users ||--o{ sleep_logs: "user_id (CASCADE)"
Constraints and indexes on sleep_logs:
| Name | Type | Detail |
|---|---|---|
wake_after_bed |
CHECK | wake_time > bed_time |
no_overlapping_sleep |
EXCLUDE (GiST) | (user_id WITH =, tstzrange(bed_time, wake_time) WITH &&) |
idx_sleep_logs_user_id_wake_time |
INDEX | (user_id, wake_time DESC) |
Traces POST /api/v1/sleep-log through all layers, showing the happy path and error branches.
sequenceDiagram
participant C as Client
participant SC as SleepController
participant SS as SleepServiceImpl
participant UR as UserRepository
participant SLR as SleepLogRepository
participant DB as PostgreSQL
C ->> SC: POST /api/v1/sleep-log<br/>X-User-Id: 42<br/>{bedTime, wakeTime, mood}
SC ->> SC: Map DTO → NewSleepLog
SC ->> SS: createTodaySleepLog(42, newSleepLog)
SS ->> UR: findUserById(42)
UR ->> DB: SELECT from users
DB -->> UR: User row
UR -->> SS: User
Note over SS: Validate:<br/>• bedTime < wakeTime<br/>• duration ≥ 30 min<br/>• duration < 24 hr<br/>• wakeTime is today (user TZ)
SS ->> SLR: findLatestSleepLogByUserId(42)
SLR ->> DB: SELECT latest by wake_time DESC
DB -->> SLR: SleepLog or null
SLR -->> SS: existing log?
Note over SS: Check:<br/>• Log today? → 409<br/>• Overlap with previous? → 400
SS ->> SLR: saveSleepLog(sleepLog)
SLR ->> DB: INSERT + GiST constraint check
DB -->> SLR: saved row with ID
SLR -->> SS: SleepLog
SS -->> SC: SleepLog
SC ->> SC: Map SleepLog → SleepLogResponse
SC -->> C: 201 Created {SleepLogResponse}
Error paths (handled by GlobalExceptionHandler):
| Condition | Exception | HTTP Status |
|---|---|---|
| User not found | ResourceNotFoundException |
404 |
| Log already exists for today | ResourceConflictException |
409 |
| Invalid duration or times | SleepLogInvalidException |
400 |
| DB overlap constraint violated | DataIntegrityViolationException |
409 |
Missing X-User-Id header |
MissingRequestHeaderException |
400 |