Skip to main content

Afia architecture rationale

Problem​

Afia is a health product (health.afia.app) with five Stitch modules and a workspace that only has health/version stubs. Stitch screens were not readable here (Google login). The non-obvious choice is how to cut modules: five apps/services vs one API and a few client surfaces.

Usage (caller's view)​

The Flutter app talks to one host. Paths stay under /api/v1/afia. Callers never see JPA, Stitch screen names, or storage schemas.

# Identity
POST /api/v1/afia/auth/otp/request
POST /api/v1/afia/auth/otp/verify
GET /api/v1/afia/me

# Patient
GET /api/v1/afia/appointments
POST /api/v1/afia/appointments
GET /api/v1/afia/results

# Clinician
GET /api/v1/afia/clinician/agenda
POST /api/v1/afia/encounters/{id}/notes

# Diagnostics (imaging + lab share the order/result shape)
GET /api/v1/afia/worklists?kind=IMAGING|LAB
POST /api/v1/afia/orders/{id}/results

# Admin
GET /api/v1/afia/admin/users

Flutter: AfiaApiClient + domain repositories. Five role shells in one binary. Marketing site stays static.

Shape​

Modular monolith in afia-api. Packages own knowledge, not screens:

  • health.afia.app.identity — account, OTP session, roles
  • health.afia.app.patient — patient profile, patient-facing booking
  • health.afia.app.clinician — clinician profile, agenda, encounter notes
  • health.afia.app.diagnostics — imaging + lab orders and results (one model, kind)
  • health.afia.app.admin — directory, roles, clinic config
  • health.afia.app.common — EndpointUri, envelope

Appointment / Encounter / DiagnosticOrder join the roles. Validation at HTTP/DTO boundary; domain types inside. One Flutter app with five role shells (Patient, Médecin, Admin, Imagerie, Laboratoire). Imaging and lab share diagnostics and kind.

Public surface is role + resource, not five module facades. That hides “how results attach to an encounter” behind diagnostics + patient reads.

Synthesis decision​

Two shapes were sketched.

Base: modular monolith (candidate A). One API, one EndpointUri, one auth filter chain, smaller ops.

Grafted from split-services (candidate B):

  • Imaging and lab share diagnostics instead of two shallow services.
  • The five Stitch modules are role shells in one app, not five store listings.

Rejected: five microservices (one per Stitch project). Callers would coordinate identity, booking, and results across processes. Rejected: five Flutter apps. Store and identity explode for one brand. Rejected (ADR 0004): admin as a later web console. design.txt lists Admin as a Stitch module next to Patient. Superseded by ADR 0007.

Tradeoffs accepted​

  • We accept a growing JVM in exchange for one deploy and one auth story.
  • We accept a combined diagnostics package in exchange for not leaking imaging-vs-lab as two public APIs.
  • We accept incomplete screen IA in exchange for not inventing Stitch screens.
  • We accept one mobile binary with roles in exchange for slower independent clinician-app release.

Alternatives considered​

  • Five services / five apps. Deep isolation, shallow product: every booking call spans three networks. Lost.
  • Admin as a separate web console. Contradicts the five-module Stitch list. Lost. Privileged UI is gated by ADMIN role inside afia-app.

Open questions and risks​

  • Which identity model (OTP session, or something else)?
  • After Stitch export, do imaging and lab need separate worklist UIs that still share DiagnosticOrder?
  • Is clinician a second store app later, or only a role in afia-app?
  • Postgres + Liquibase from day one, or delay persistence until first real feature?
  • Health-data review (Apple / Google) once clinical fields exist?

Next implementation step​

Do not implement features yet. When asked, start at WP-00, then hand G1 task files to parallel agents. Screen map is WP-10 after a Stitch export.