Records in Postgres, Rebuildable Results in Redis

Why evidence and history stay in Postgres while only rebuildable query results go to Redis

Remaining parking spaces can change whether someone goes to the Han River. To show “20 spaces left,” the screen also needs to explain the evidence and observation time behind that number.

Hangangjari stores evidence and history in Postgres and rebuildable query results in Redis.

If every request is handled by Postgres, frequently read first screens and forecast responses concentrate load in one place. If Redis owns reference data and history as well, a cache failure becomes data loss.

Data Owned by Postgres and Redis

The difference becomes visible by looking at how both stores are used in the request path. Postgres keeps records that may need explanation later, while Redis serves rebuildable query results quickly.

flowchart LR
  Workers["Collection · forecast · notification workers"] --> PG[("PostgreSQL / PostGIS<br/>Reference data · history · location · audit records")]
  Workers --> Redis[("Redis<br/>Current state · response cache · supporting indexes")]
  API["FastAPI read path"] --> Redis
  Redis -->|Hit| API
  Redis -->|Miss · malformed · stale| PG
  PG --> API

Postgres owns values that must be explained later: when a source value came in, which forecast run produced which result, and why a notification was sent or suppressed.

Redis is used to shorten user request time. It holds values that can be rebuilt, such as latest parking status, forecast responses, home-summary hot/stale responses, source status summaries, and indexes for push candidate lookup.

With this split, incident language becomes more precise. If Redis is empty, screen-ready responses can be rebuilt. If Postgres reference data or history is damaged, recovery is entirely different. Treating the two stores with the same weight slows response.

Data Groups by App Feature

The schema groups data by the product feature that owns it. Parking, outing sources, forecasts, push, transit, and translation share one DB but move at different speeds and fail in different ways.

erDiagram
  PARK ||--o{ PARKING_LOT : owns
  PARKING_LOT ||--o{ PARKING_STATUS : records
  PARKING_STATUS ||--o{ FORECAST_INPUT : becomes
  FORECAST_RUN ||--o{ FORECAST_RESULT : creates
  DATA_SOURCE ||--o{ INGESTION_RUN : reports
  DATA_SOURCE ||--o{ OUTING_SIGNAL : publishes
  PUSH_SUBSCRIPTION ||--o{ DELIVERY_DECISION : evaluates
  DELIVERY_DECISION ||--o{ DELIVERY_ATTEMPT : audits
  TRANSIT_DATASET ||--o{ TRANSIT_ROUTE : contains
  TRANSLATION_SOURCE ||--o{ TRANSLATION_CACHE : renders

This ERD contains only the reference data and history operators check first for each feature. Even within the same DB, knowing the owning feature narrows incident scope.

The real schema has more helper tables and indexes. This diagram keeps only how user-facing features and operator-facing records connect.

Notification Records in Postgres

In push, the system records the delivery decision, suppression reason, delivery attempt, and external sender response separately.

If this is kept only as one log string, it becomes hard to tune how quietly notifications should behave later. A notification suppressed because of quiet hours and a notification that failed to deliver both look like “no notification arrived” to the user, but require different operator actions.

The decision to send and the delivery audit record live in Postgres. Redis only helps find candidates quickly; Postgres records the final reason, so an operator can explain why a notification was not sent.

Observation Time Stored with Each Value

Each stored public-data value also needs its observation context.

Even if the value says “20 spaces available at Yeouido,” the next questions remain.

  • When was this value observed?
  • When did the server fetch it?
  • Which source produced it?
  • Is the source healthy now?
  • Is this a current value or a forecast?

That is why status, forecast, outing, and home summary records carry time and refresh state together. The app uses that information to speak differently about “fresh,” “delayed,” and “unavailable.”

This distinction changes screen copy. If stale values look current, the cache may have succeeded but the app has misled the user. If the app clearly says a value is old, users can treat it as a reference signal.

Rebuildable Cache

Redis cache follows these recovery rules.

  • Redis values must be rebuildable.
  • A Redis miss should be a normal path, not an incident by itself.
  • Malformed cache entries are ignored and fall back to Postgres records.
  • When a new forecast run is created, related forecast and home caches are cleared.
  • home-summary separates hot cache and stale backup cache so incidents can show the last trusted value.
  • Push candidate lookup can be Redis-first, but final delivery and audit follow Postgres.

Redis improves latency but does not guarantee a value’s time or source. The API includes freshness and source state even in responses read from cache.

The Decision Left on Screen

The API returns each value with a fresh, last-successful, or unavailable state.

The screen distinguishes “no information” from “empty” and uses timestamps to show when values were observed. This state matters especially in widgets, where stale values may be displayed.

Postgres keeps the evidence and history behind numbers, while Redis keeps rebuildable query values. The API uses that boundary to distinguish fresh values, last successful values, and unavailable information.

Comments

Comments

    Image preview