Low Level Design
Building a Scalable Weather Station
A Practical Look at Pub/Sub Architecture in Java
A Practical Look at Pub/Sub Architecture in Java Real-time systems are everywhere—live cricket scores, stock tickers, Uber ride updates, and even weather dashboards. But have you ever wondered how these systems instantly notify multiple views when something changes? In this blog, we design a Weather Station System using the Observer Design Pattern, applying clean architecture, maintainability, and extensibility.
Problem Statement#
Build a weather monitoring system where:
- Multiple clients (mobile, web, desktop, IoT) can subscribe for weather updates.
- Each client should receive new data only for the cities they subscribed to.
- When weather changes, the system should instantly notify all listeners.
- Support dynamic add/remove subscribers at runtime.
This is a classic Publish–Subscribe (Pub-Sub) model.
Why Observer Pattern?#
Observer pattern fits perfectly because: ✔ One-to-many relationship between Weather Station (Subject) & Listeners (Observers) ✔ Observers get notified automatically when data updates ✔ Loose coupling — Weather Station doesn't need to know how listeners behave ✔ Easy scalability — Add new listener types without modifying the subject
High-Level Architecture#
┌───────────────────────────────┐
│ WeatherManager │
│ (Publisher / Subject) │
├───────────────────────────────┤
│ city → Set<WeatherListener> │
│ weatherMap: city → WeatherData │
└───────────────┬───────────────┘
Notify()
┌─────────────┼─────────────┐
│ │ │
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ MobileListener │ │ WebListener │ │ DesktopListener │ (easily add new)
└─────────────────┘ └─────────────────┘ └─────────────────┘
Core Components#
Data#
WeatherData — Immutable snapshot of temperature, humidity, and wind speed captured at a point in time.
WeatherEvent — Pairs a city name with its WeatherData. This is the message passed to every notified listener, keeping city and data co-located so listeners don't have to look anything up.
Contracts#
WeatherListener — The Observer interface. A single method update(WeatherEvent) is all a concrete listener needs to implement. Adding a new platform (SMS, TV, IoT) means writing a new class that implements this interface — nothing else changes.
WeatherSubject — The Subject contract that WeatherManager fulfills: subscribe, unSubscribe, and notifyListeners, all scoped per city so listeners only receive updates for the cities they care about.
Listener Implementations#
MobileWeatherListener and WebWeatherListener are concrete observers. Each formats and outputs the update for its own platform. The pattern makes it trivial to add more — just implement WeatherListener.
WeatherManager#
The heart of the system. Maintains two maps:
- city → Set<WeatherListener> — subscriber registry
- city → WeatherData — current weather cache
updateWeather() persists new data and immediately calls notifyListeners(), which fans the event out to every subscriber for that city.
Why This Design Works Well#
| Property | How it's achieved |
|---|---|
| Decoupled | WeatherManager holds references to WeatherListener interfaces, never to concrete classes |
| Scalable | New listener types require zero changes to WeatherManager |
| Per-city granularity | Subscribers opt into only the cities they need |
| Testable | Every class can be tested in isolation; mock WeatherListener in unit tests |
Complete Implementation#
All seven files that make up the system — browse each tab to see how they fit together:
public class WeatherData {
private int temperature;
private int humidity;
private int windSpeed;
public WeatherData(int temp, int humidity, int windSpeed) {
this.temperature = temp;
this.humidity = humidity;
this.windSpeed = windSpeed;
}
public int getTemperature() { return temperature; }
public int getHumidity() { return humidity; }
public int getWindSpeed() { return windSpeed; }
@Override
public String toString() {
return "Temp: " + temperature + ", Humidity: " + humidity + ", Wind: " + windSpeed;
}
}public class WeatherEvent {
private final String city;
private final WeatherData data;
public WeatherEvent(String city, WeatherData data) {
this.city = city;
this.data = data;
}
public String getCity() { return city; }
public WeatherData getData() { return data; }
}public interface WeatherListener {
void update(WeatherEvent event);
}public interface WeatherSubject {
void subscribe(String city, WeatherListener listener);
void unSubscribe(String city, WeatherListener listener);
void notifyListeners(String city, WeatherData data);
}public class MobileWeatherListener implements WeatherListener {
@Override
public void update(WeatherEvent event) {
System.out.println("[Mobile] Weather update for "
+ event.getCity() + ": " + event.getData());
}
}public class WebWeatherListener implements WeatherListener {
@Override
public void update(WeatherEvent event) {
System.out.println("[Web] Weather update for "
+ event.getCity() + ": " + event.getData());
}
}import java.util.*;
public class WeatherManager implements WeatherSubject {
private final Map<String, Set<WeatherListener>> listeners;
private final Map<String, WeatherData> weatherMap;
public WeatherManager() {
listeners = new HashMap<>();
weatherMap = new HashMap<>();
weatherMap.put("Pune", new WeatherData(32, 40, 10));
weatherMap.put("Bangalore", new WeatherData(28, 50, 8));
}
@Override
public void subscribe(String city, WeatherListener listener) {
listeners.computeIfAbsent(city, k -> new HashSet<>()).add(listener);
}
@Override
public void unSubscribe(String city, WeatherListener listener) {
if (!listeners.containsKey(city)) {
System.out.println("Error: No listeners exist for " + city);
return;
}
listeners.get(city).remove(listener);
if (listeners.get(city).isEmpty()) {
listeners.remove(city);
System.out.println("No more listeners for " + city + ". Removing from subscription list.");
}
System.out.println("Unsubscribed successfully from " + city);
}
@Override
public void notifyListeners(String city, WeatherData data) {
if (!listeners.containsKey(city)) {
System.out.println("No listeners subscribed for " + city);
return;
}
WeatherEvent event = new WeatherEvent(city, data);
for (WeatherListener listener : listeners.get(city)) {
listener.update(event);
}
}
public void updateWeather(String city, WeatherData newData) {
weatherMap.put(city, newData);
notifyListeners(city, newData);
}
public WeatherData getWeather(String city) {
return weatherMap.get(city);
}
}