JJT — Child Education Sponsorship Platform
Small education NGOs track child sponsorships in spreadsheets and WhatsApp threads and reconcile donations by hand. The failure mode isn't inefficiency — it's that when a board member or a donor asks where a specific payment went, nobody can answer with confidence. SponsorOne records every payment against an append-only ledger. Sponsors see their own money trail. Admins get reconciliation and a system-wide audit log. Zakat is tracked separately from general donations, because the accounting rules differ and combining them is a genuine problem for the organizations I built this for.
Sponsorship money was moving with no audit trail.
Education NGOs typically run child sponsorship programs on spreadsheets, WhatsApp threads, and paper receipts. Sponsors have no visibility into where their money actually goes, reconciliation is done by hand at month end, and there's no defensible record to show a board, a donor, or a regulator when they ask a hard question.
JJT needed to replace that entirely — for payments, donations, and Zakat, which carries its own compliance obligations under Islamic finance rules. The organization needed every transaction traceable, every sponsor able to see their own impact, and every action in the system auditable after the fact.
Design the invariants first, write code second.
Before a line of code was written, the domain model, database architecture, user roles and permissions, and financial tracking rules were designed and documented in a BRD and a set of architecture diagrams. That sequencing mattered: for a system handling other people's money, the rules that can never be broken have to be decided before the framework that will enforce them.
Clean Architecture
The domain layer is framework-free — no JPA annotations, no Spring types, no HTTP concerns inside business logic. Persistence and web layers are adapters around the domain, not the other way around. That's what let the same core survive decisions about hosting, database tooling, and deployment target without a rewrite.
Append-only financial ledger
Every payment, donation, and Zakat transaction is written once and never mutated. Immutability is enforced twice — once in the domain model, which exposes no update or delete path for ledger entries, and again at the PostgreSQL level, so even a direct database session can't quietly edit history.
Security by design
Access tokens live in memory only, never in local storage; refresh tokens rotate and are stored hashed. Every query is claims-scoped, so a sponsor's session simply cannot resolve another sponsor's or organization's data — parameter manipulation isn't a risk to patch later, because there's no path to the data in the first place.
Multi-tenant & compliance-ready
Organization scoping is enforced on every table, not bolted on with a filter in the service layer. Combined with a system-wide, immutable audit trail, the platform is built to support multiple NGOs on shared infrastructure without any risk of cross-tenant data leakage.
Three surfaces, one ledger underneath.
5-step sponsorship wizard
A donor can commit to sponsoring a child without creating an account first — reducing the drop-off that comes from forcing signup before someone has decided to give.
18 operational modules
Payments, donations, Zakat, funds, and the audit log all live behind role-gated screens, so NGO staff can run day-to-day operations without ever touching the database directly.
Claims-scoped ledger view
Every sponsor sees a progress view and transaction history scoped to exactly their own contributions — the same claims-based access control that protects the backend also drives what's rendered in the UI.
The hard decisions, and why they went that way.
Making "immutable" actually mean immutable
Problem — An append-only ledger is easy to describe and easy to accidentally violate — a stray UPDATE in a migration, an admin "fix" script, or a well-meaning bug fix can quietly break the guarantee the whole audit story depends on.
Solution — The domain model exposes no mutation path for a posted ledger entry — corrections are new, linked, offsetting entries, never edits. That rule is mirrored at the PostgreSQL level so the guarantee holds even outside the application layer.
Claims-scoped access without a performance tax
Problem — Scoping every query by organization and by sponsor is the right security model, but naive implementations turn into a filter bolted onto every service method — easy to forget on a new endpoint, and easy to get subtly wrong.
Solution — Scoping lives at the data-access layer, derived from the authenticated principal's claims, so every new query inherits it by construction rather than by convention. A missing filter isn't a possible bug class here.
Session security without breaking UX
Problem — Memory-only access tokens are the right call for exposure risk, but they raise the obvious question of what happens on a page refresh, and rotating refresh tokens raise the question of what happens if a rotation request is replayed or lost mid-flight.
Solution — Rotating, hashed refresh tokens re-issue a short-lived access token silently on load, and rotation is designed to be safely retryable, so a dropped request degrades to a re-auth prompt instead of a stuck session.
Shipping a one-person production platform to AWS
Problem — With no separate ops team, deployment, migrations, and rollback all had to be safe enough for one engineer to run confidently — including on a database holding real financial records.
Solution — Docker containerization plus ECS/Fargate keeps the runtime environment reproducible and the deploy target stateless; Flyway-versioned migrations make schema changes forward-only and reviewable; and integration tests run against a real PostgreSQL instance via Testcontainers, so "the tests pass" means the database behavior was actually exercised, not mocked.
Docker, ECS/Fargate, and a CI/CD path built for one engineer.
The production deployment was planned and executed end to end — containerizing the Spring Boot and Angular services with Docker, running them on ECS/Fargate so there's no server fleet to patch, and building a CI/CD pipeline that runs the Testcontainers-backed integration suite before anything reaches production. Flyway owns every schema change, so migrations are versioned, ordered, and reviewable rather than run ad hoc against a live database.