Functional
Non-functional
GET /v1/weather, reliable prioritized delivery for crisis alerts, and cost-effective autoscaling.All endpoints JSON over TLS. Standard error envelope:
{ "code":"BAD_REQUEST", "message":"reason", "details":{} }
POST /v1/subscribe/{city} — subscribe/update
city (canonical id/slug)
{
"user_id":"string",
"push_token":"string",
"platform":"ios|android|web",
"notify_level":"ALL|CRISIS_ONLY",
"channels":["push","email"] // optional
}
201 Created subscription created (or 200 OK if updated)400 Bad Request, 404 Not Found (bad city), 409 Conflict (semantic)user_id + city.DELETE /v1/unsubscribe/{city} — unsubscribe
{ "user_id": "string" }200 OK or 404 Not Found.GET /v1/weather/{city}/{YYYY-MM-DD} — forecast read
{
"city_id":"city:2643743",
"date":"2025-12-08",
"forecast":{"temp_min_c":4,"temp_max_c":8,"temp_avg_c":6,"humidity_percent":78,"precip_probability":0.6,"wind_kph":40,"sunny":false},
"hazards":[{"type":"HIGH_WIND","level":"SEVERE","confidence":0.9}],
"generated_at":"2025-12-07T23:00:00Z",
"source":["provA","provB"]
}
400 invalid date, 404 not found, 500 server error.Operational notes
Purpose: persist & serve who is subscribed to which city (fast fan-out key).
How it works
subscribers::city::<city_id>::shard::<n>.PK = city#<city_id>; SK = user#<user_id> (NoSQL).city#id#shard-0..k-1).Purpose: persist & serve who is subscribed to which city (fast fan-out key).
How it works
subscribers::city::<city_id>::shard::<n>.PK = city#<city_id>; SK = user#<user_id> (NoSQL).city#id#shard-0..k-1).Purpose: accept provider feeds, normalize schema, detect hazards, dedupe, persist canonical events, and update Redis cache used by GET API.
How it works
event_id, city_id, date(s), metrics, source, confidence.wind_kph >= 80 → HURRICANE; wind_kph >= 35 → HIGH_WIND.weather::city::<id>::date::<YYYY-MM-DD> to Redis.Scaling
Purpose: prioritized, reliable fan-out of events to subscribers and final push to providers (Airship/FCM/APNs).
How it works
weather.city.<city_id>.normal / ...urgent.shard = hash(city_id) % N_SHARDS. Bind routing keys to deliver.normal.<shard> and deliver.urgent.<shard>.delivered::<event_id>::<user_id> TTL to avoid duplicate sends.Scaling
N_SHARDS, add more worker instances; autoscale by queue length and delivery latency.city#<id>, SK=user#<id>, attrs: push_token, platform, notify_level, channels, status.user_id, tz, home_city, locale, consent_flags.POST /subscribe → Subscription service writes DB → update Redis shard cache.GET /weather → read Redis → if miss, fallback to canonical store & populate Redis.Assumptions: Users = 1,000,000, C = 10,000 cities, updates per city per hour U = 4, avg subscribers per city S = Users / C.
Step-by-step:
Users ÷ C = 1,000,000 ÷ 10,000 = 100.= C × U = 10,000 × 4 = 40,000.M_s = 40,000 ÷ 3,600.40,000 ÷ 100 = 400; 3,600 ÷ 100 = 36 ⇒ 400 ÷ 36 = 11.111... events/sec.D_s = M_s × S ≈ 11.111... × 100 = 1,111.11 deliveries/sec.If one worker (with batching) handles ~400 deliveries/sec, required workers = ceil(1,111.11 ÷ 400) = 3 workers initially (autoscale on queue length). For crisis spikes (e.g., 200k subscribers), design for burst strategies (sharding, staggered batches, provider scaling).