Case 03 · HSA Group super app · 2026

Built to split: a modular monolith for the HSA super app

A customer app and a field app for pickers and couriers on one TypeScript codebase that runs as separate roles on AWS ECS Fargate. Modules own their data, kill switches replace emergency deploys, and every alarm links to a runbook. Production-ready and in canary testing.

ArchitectureAWSTerraform5 min read

HSA Group is building a super app. Its first service needs a customer app, a picking app and a delivery app, plus web consoles, all working on the same live data. The modules underneath have to serve every service that comes after it, with one customer account everywhere. This is the design, and why it looks the way it does.

The constraints

  • The network. Many of our customers are on slow links in Aden that drop again and again, on mid-range Android phones with full storage, where the app can be killed at any moment.
  • The team. It is small, works in one language across backend and mobile, and is not split into separate backend and mobile teams.
  • Operations. In an incident, people flip switches. They do not deploy by hand.
  • Cost. Lean now, with the upgrade path written down.

The decisions

  • One codebase, one image, several roles. An early draft gave each service its own image. Review showed that isolation comes from the permissions each running service gets, not from the image, so separate images meant more pipelines and no extra safety. Splitting a module out later is mechanical, because no module reads another module’s tables.
  • ECS Fargate. There is no cluster to operate, and every role runs the same commit. Kubernetes was rejected for its operational load, and Lambda for long database transactions, connection pooling and cold starts on the payment path.
  • PostgreSQL as the truth. An order, its payment and its events commit in one transaction. A key-value store would have turned those into sagas.
  • Valkey for speed only. It runs on ElastiCache Serverless, with no nodes to size. The rule is simple: PostgreSQL is the truth, and the cache only makes things faster.
  • Events in the database. Events are written in the same transaction as the change and published from PostgreSQL. A managed event bus would not run on a laptop, a cache-backed queue can lose jobs in a failover, and Kafka costs more to run than this stage needs.
  • Infrastructure in its own lane. Terraform lives in its own repository, so the application pipeline cannot change permissions, the network or the data stores. Staging and production are separate AWS accounts, so a staging mistake cannot reach production.

The design

One image, several roles

idempotency key

device certificate

Customer app

Customer API

Field app: picker and courier

Back-office API

Worker

PostgreSQL: a schema per module

Fig. 1 · One TypeScript codebase runs as separate services (the catalog publisher and payment gateway are two more roles); each module owns its schema in one PostgreSQL.
  • Roles, not services. The same image starts as the customer API, the back-office API, a worker, the catalog publisher or an isolated payment gateway. Each runs as its own ECS service with its own permissions and its own scaling.
  • Hard module boundaries. Each module owns its database schema and its route prefix. Database grants make other modules’ tables unreachable, and CI checks imports between modules.
  • Two apps, three audiences. The customer app uses the public API. Pickers and couriers share one field app with two modes, which reaches a separate host that requires an enrolled device certificate. The consoles sit behind single sign-on.
  • Kill switches. Each one is a permanent, named off-switch, so an emergency is handled by flipping a switch, not by a deploy. Every flip is audited, there is a break-glass path for when the console itself is down, and Terraform never undoes a flip.
  • Circuit breakers. Every outbound call is wrapped with a timeout, a concurrency cap, a retry budget and a breaker. Under load, the order path is the last thing refused.
  • Alarms that say what to do. Every alarm and every kill switch has a runbook page, and each alarm links to its page.
  • An AWS Organization from day one. Management, log archive, staging, production and CI are separate accounts. Organization policies protect the audit trail. Only the pipeline applies changes, through GitHub OIDC, with a guard against destructive changes and a nightly drift check.
  • One SDK for the hard parts. Retries, idempotency and offline behaviour live in a shared mobile SDK, because if each app built them alone, each would get them wrong in its own way. Every mutating call keeps its idempotency key from end to end.
  • A local-first catalog. Customers browse a replica on the phone, built from immutable releases served by a CDN, so the phone never waits on the back office.
  • Reads at the edge, writes in one region. The home region will be chosen against a written rule, from measurements taken in Aden with Net Check, a small app I wrote for that.

What I did as the lead

I was the product owner and the architect. The kill switches and the protective layer around every outbound call came from my review of the first draft, which asked for a way to switch features off and to protect the system under load. I designed the system and wrote its design documents and decision records, the service catalog, the catalog delivery spec and the hosting-region record. I wrote the platform layer of the backend: the module scaffold, the circuit breakers and kill switches, and most of the runbooks. I wrote the Terraform for the AWS Organization and for observability, started the shared SDK and the mobile repository, and built Net Check. The mobile engineers wrote most of the app code and much of the SDK, and two engineers contributed to the backend and infrastructure.

Where it stands

The staging environment has run since October 2026. A merge builds the image, runs the migrations, deploys every role blue/green and smoke-tests the result. The platform is production-ready and in canary testing, and the rollout starts in November 2026. I will add production results here once there are some.

What came from Masdary

The super app starts where Masdary’s network work ended. The app-side retry policy and breakers became the SDK’s retry policy plus load shedding on the backend. Idempotency keys became the SDK’s order protocol. The resumable catalog sync became a full local replica built from CDN releases, and the field diagnostics in Aden became Net Check, run before choosing a region.