Weather App

Weather App

A weather forecast application built with Ruby on Rails. Search any address or zip code to view current conditions, an hourly forecast (6 hours), and a daily forecast (7 days) β€” all powered by the Google Weather and Geocode APIs.

Documentation

Access the hosted documentation at pablowillians.github.io/weather-app.

Generate it locally with bin/doc (opens in the browser automatically).

Features

Architecture

The application follows a layered service architecture inspired by DDD (Domain-Driven Design). All business logic lives under app/services/:

app/services/
β”œβ”€β”€ application/               # Use-case orchestration
β”‚   └── weather_by_address/    # Geocode β†’ fetch β†’ map β†’ Result
β”‚       β”œβ”€β”€ acl.rb             # Anti-Corruption Layer (API β†’ domain)
β”‚       └── result.rb          # Immutable result wrapper
β”œβ”€β”€ domains/                   # Pure value objects, no dependencies
β”‚   β”œβ”€β”€ geocode/
β”‚   β”‚   └── location.rb
β”‚   └── weather/
β”‚       β”œβ”€β”€ current_weather.rb
β”‚       β”œβ”€β”€ daily_forecast_entry.rb
β”‚       β”œβ”€β”€ hourly_forecast_entry.rb
β”‚       β”œβ”€β”€ weather_at_location.rb     # Aggregate root
β”‚       └── weather_condition.rb
└── infrastructure/            # External API adapters
    └── adapters/
        β”œβ”€β”€ geocode/           # Google Geocode API
        └── weather/           # Google Weather API (current, hourly, daily)

Request flow β€” Presentation layer

From the user’s search to the rendered page. The application service is treated as a black box here (details in the next diagram).

sequenceDiagram
    actor User
    participant Browser
    participant Controller as WeatherController
    participant Service as WeatherByAddress

    User->>Browser: enters address and submits
    Browser->>Controller: GET /weather/search?address=SΓ£o Paulo
    Controller->>Browser: 302 redirect to /?address=SΓ£o Paulo
    Browser->>Controller: GET /?address=SΓ£o Paulo

    Controller->>Service: call("SΓ£o Paulo")
    Service-->>Controller: Result (location, current, hourly, daily + source metadata)

    Controller-->>Browser: rendered HTML with weather data
    Browser-->>User: current weather, hourly & daily forecasts

Request flow β€” Application service

What happens inside WeatherByAddress#call: geocoding, parallel weather fetching, caching, and domain mapping.

sequenceDiagram
    participant Service as WeatherByAddress
    participant ACL
    participant Cache as SolidCache
    participant Google as Google APIs

    Service->>Cache: geocode cached? (TTL 7 days)
    Cache-->>Service: hit or miss
    Service->>Google: Geocode API (on miss)
    Google-->>Service: lat, lng, zipcode
    Service->>ACL: build Location

    Note over Service,Google: 3 parallel threads (current, hourly, daily)
    Service->>Cache: weather cached? (TTL 30 min)
    Cache-->>Service: hit or miss
    Service->>Google: Weather API (on miss)
    Google-->>Service: weather JSON

    Service->>ACL: map JSON to domain objects
    ACL-->>Service: Result

Technologies

Setup

Prerequisites

Obligatory

Option 2 (Alternative)

Required steps

  1. Clone the repository:

git clone https://github.com/pablowillians/weather-app.git
cd weather-app
  1. Copy the environment file and add your API key:

cp .env.example .env
# Edit .env and set GOOGLE_PLACES_API_KEY=your_key_here

Running with Docker Compose

  1. Build and start the container:

docker compose up --build

Running without Docker Compose

  1. Install dependencies and prepare the database:

bin/setup
  1. Start the development server:

bin/dev

Accessing the application

http://localhost:3000

Running Tests

RSpec (unit, integration, request)

bundle exec rspec

Minitest system tests (browser)

bin/rails test:system

Generating Documentation

bin/doc

This runs RDoc over the README, controllers, helpers, and services, then opens the generated site in the browser. The same docs are deployed to GitHub Pages on push to main.

Decisions

Geocode cache with a 7-day TTL

Geographic coordinates for a given address change extremely rarely β€” a street address will resolve to the same latitude/longitude for years. By caching the geocode response for 7 days we avoid a redundant API call on every request for the same address, reducing latency and cutting costs while still allowing eventual updates if Google refines its data.

Caching the full API response

The cache stores the entire JSON payload returned by each Google API, not just the fields the UI currently uses. This means new UI features (e.g. wind speed, wind direction, humidity) can read from the same cached response without requiring a cache flush or migration β€” the data is already there.

The trade-off is storing more data than strictly necessary today, which could be seen as premature optimisation. This was accepted because (1) the payloads are small (a few KB each), (2) it guarantees consistency between cached and live responses β€” both always carry the same shape and fields, so switching between sources never causes missing-data bugs β€” and (3) it avoids coupling the cache schema to the current UI, making the system easier to evolve.

Weather cache keyed by zipcode or coordinates

Weather adapter cache keys use the zipcode when available, falling back to latitude/longitude. This maximises cache hits because different search terms often resolve to the same location: β€œNew York City” and β€œNew York, New York” both geocode to the same coordinates, and distinct street numbers on the same street share the same zipcode. By keying on zipcode (or coordinates as a fallback), all those variations reuse a single cached weather response instead of fetching from the API separately.

Future improvements