Skip to content

Architecture

Ferran Buireu edited this page Jun 13, 2026 · 4 revisions

Architecture

Both the web and mobile apps follow the same DDD-ish layered architecture. The dependency direction is strict: domain knows nothing about anything else; everything points inward toward it.

---
config:
  look: handDrawn
  theme: neutral
---
flowchart RL
    application --> domain
    infrastructure --> application
    ui["ui / pages"] --> application
    ui --> domain
Loading

Each layer documents its own rules in a colocated CONTEXT.md.


Layers (web)

Layer Role
domain Pure business core: value objects, entities, failures, geometry. No Astro, no Cloudflare, no fetch.
application Curried use cases that orchestrate the domain, plus Failure → HTTP mapping.
infrastructure GitHub HTML scraping, the SVG string renderer, logging. Implements domain interfaces.
ui Astro components, client interactivity, styles.
pages Routes — the only layer that wires everything together (the composition root).

Core principles

Pure domain

domain/ imports no packages — only TS stdlib types plus the design-token JSON from @shared as data. Functional style: factory functions return readonly objects with a discriminating _tag. No classes.

Validated value objects

Value objects validate on construction. If a Username exists, it is valid; same for Year, Palette, ShapeKind, ContributionLevel. Validation happens once, at the boundary, so the rest of the code never re-checks.

Typed failures, no throwing

Errors are a Failure discriminated union — NotFound, InvalidInput, Network, Parse. Functions return T | Failure; nothing throws across layers. The mapping from Failure to an HTTP status and message lives in exactly one place: application/http/failure-http.ts (statusFor, messageFor), guarded by isFailure.

Curried use cases

Use cases are curried factory functions: they take repository/service implementations and return the operation. They hold no state and know nothing about Astro, Cloudflare, or fetch — everything arrives via the closure.

Use case Purpose
fetchContributions(repo)({ username, year }) Loads a ContributionCalendar for a user/year
renderCalendarSvg(renderer)({ calendar, options }) Renders the SVG string for a calendar
loadInitialContributions(load)({ username?, year? }) Validates input, loads, and returns the built 53×7 grid

Repositories are interfaces

domain/repositories/ declares interfaces only. Implementations live in infrastructure/ — e.g. createGithubHtmlContributionsRepository. Network and parsing errors are converted to Failure at that boundary; a raw Error never escapes.


Shared design tokens

Palettes, shapes, and suggested usernames are defined once in shared/*.json and consumed by both apps. The web imports them via the @shared alias at build time; the Flutter app bundles generated copies under app/assets/. See Project Structure.


See also

Clone this wiki locally