-
-
Notifications
You must be signed in to change notification settings - Fork 0
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
Each layer documents its own rules in a colocated CONTEXT.md.
| 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). |
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.
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.
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.
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 |
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.
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.
- Project Structure — where each layer lives on disk
- Web Application · Mobile App — per-platform specifics