Weather Dashboard
Ingests raw meteorological data from a keyless provider and runs it through a rules-based recommendation engine that answers specific, practical questions (should I carry an umbrella, is today good for a run, is the air safe to breathe, will this disrupt travel) with a plain-English reasoning trace behind every verdict, and a documented fallback path if the primary provider goes down.
Why I Built This
Almost every weather app answers the same question (what's the temperature), and leaves the actual decision to the person reading it. I wanted a dashboard that answers the decision directly: should I carry an umbrella, is today good for a run, is the air safe to breathe.
The reasoning had to be visible too. A recommendation without an explanation is just another number to second-guess, so every verdict here carries the specific factor that drove it.
The Recommendation Engine
Five rule classes (Umbrella, Running Suitability, Outdoor Activity, Travel Suitability, and Air Quality) each evaluate the same normalized weather snapshot independently and produce their own verdict with a reasoning string. The most severe active verdict is synthesized into a single dashboard headline, but every individual verdict stays inspectable in the recommendation grid below it.
Provider Fallback
Weather, air quality, and geocoding all come from Open-Meteo by default, with OpenWeatherMap wired in as a documented fallback used only on primary failure. Both providers sit behind the same WeatherProvider interface, so the recommendation engine never has to know which one actually answered.
Caching by Data Type, Not by Default
A single blanket TTL doesn't fit this data, geocoding a city name to coordinates almost never changes, current weather goes stale in minutes, and air quality readings drift within the hour. So each data type gets its own cache TTL instead of one policy applied uniformly across the board.
That distinction matters for the fallback path too: cached geocoding results stay valid even if a provider later goes down, so a partial outage doesn't force every request back through geocoding from scratch; only the genuinely time-sensitive data needs to be re-fetched.
Responsive Dashboard Architecture
The dashboard is a desktop-first grid (headline verdict up top, the five recommendation cards laid out beneath it) that has to collapse to a single mobile column without losing the "most severe verdict first" hierarchy that makes the page scannable in the first place.
Because the recommendation grid is rendered server-side with Thymeleaf rather than client-side JavaScript, the responsive behavior is handled entirely through layout and CSS rather than reflow logic; the same markup just has to read correctly at every width, which pushed most of the real design work into getting the grid's breakpoints right.
Technical Challenges
The hardest part wasn't any individual rule; it was keeping the five rules genuinely independent so a change to the Air Quality rule could never quietly shift the Running Suitability verdict. Each rule reads from the same immutable snapshot and returns its own verdict, with nothing shared between them but the input.
Caching had to be tuned per data type rather than with one TTL for everything, since geocoding results are effectively static while air quality readings go stale within the hour.
Key Decisions
Provider and rule logic were both built on the Strategy pattern, a WeatherProvider interface with interchangeable primary/fallback implementations, and a Rule interface so adding a new recommendation means writing one new class, not touching the engine, controller, or templates.
Open-Meteo was chosen as the primary provider specifically because it's keyless; the project runs locally with zero setup, and OpenWeatherMap is documented as an explicit, opt-in fallback rather than a second requirement.
The Controller layer was kept strictly for request orchestration, no weather logic, no provider knowledge, no recommendation logic lives there, so the request lifecycle stays easy to reason about independent of the domain logic.
What I Learned
Normalizing two different weather providers into one internal domain model took more care than expected, the providers disagree on units, field names, and what counts as "missing" data, and the adapter boundary is where all of that has to get resolved so it never leaks into the rules engine.
Explicit timeouts on the provider layer turned out to matter as much as the fallback logic itself; a fallback path is only useful if a slow primary provider can't hang the whole request first.
Reliability in a project like this isn't one big feature, it's a lot of small ones stacked up: timeouts, fallback, per-data-type caching, and defensive parsing of provider responses all had to work together, and any single missing piece would have made the whole "explainable dashboard" premise fall apart the first time a provider hiccuped.