Focus Area:

Requirements analysis & Documentation, Data modelling, Validation & edge-case definition, API specification, Requirements traceability, UX/UI design

Type:

Team project (NUS),
Team size: 3

Role:

BA, dev, documentation & UX design (owner of the expense/ split and member-invitation domain)

Duration:

Sep -
Oct 2025

Tech Stack

Python, FastAPI, Uvicorn, Pydantic, SQLModel, MySQL (PyMySQL), JWT (PyJWT), bcrypt, cryptography (hashlib / base64), HTML, CSS, JavaScript, Swagger UI / OpenAPI, Figma, Mermaid, Docker, Git / GitHub

Project Summary

Splitzy is a team-built, Splitwise-style app for splitting and settling shared expenses. I owned the expense-and-split domain — turning user-story requirements into the data model, validation rules, and FastAPI endpoints behind it.

Splitzy: Expense & Split Engine for a Shared-Cost Platform

The Problem

Splitting shared costs across a group breaks down in predictable ways: people pay on different cards, nobody agrees on who owes whom, and "I'll get you back later" quietly becomes a dispute.

Splitzy's job was to make shared spending accurate, transparent, and auditable where every expense recorded with its payer and a precise per-person breakdown, balances computed automatically, and a trail of who changed what.

The project came with its requirements already framed as user stories, plus a set of non-functional requirements that constrained how the system had to behave: money stored as integers (never floats) to avoid rounding errors, clear validation feedback to users, all critical actions logged for audit, and actions completing within two seconds.

That framing matters: it meant the real work was requirements analysis and implementation discipline, not feature invention.

Requirements analysis: turning stories into specifications

A user story is a sentence; a working feature needs that sentence decomposed into concrete rules. For Story 6: "create an expense, specifying amount, date, payer, and cost-sharing breakdown". I worked out what an expense actually is as a data structure and what has to be true for one to be valid:


  • An expense belongs to exactly one group and names exactly one payer.

  • It carries a total amount, an optional description, and a date.

  • It is divided into one or more splits, each naming a member and the amount they owe.

  • Every person referenced the payer and each split member must be a member of that group.


For Story 4, I specified the membership model: a person doesn't simply appear in a group; they pass through a pending → active lifecycle. An invite creates a pending record; the member list shows only active members; and you can't invite yourself, invite someone already invited, or re-add an existing member.


This decomposition is the spine of the feature. It's what turns "let users add an expense" into something an engineer (in this case, me) can actually build and test against.

Data modelling

The cost-sharing breakdown is the part of the model that carries the product's value, and it's a clean one-to-many: one Expense is divided into many Expense-split rows, each pairing a member with the amount they owe. That structure is what later lets the balances logic compute who owes whom without any manual maths. Membership is modelled separately, with the pending/active status that drives the invitation flow.

The single most consequential modelling decision was storing money as integers, not floats, a non-functional requirement but one that lives or dies in this exact data structure. Floating-point amounts produce rounding drift the moment you split a bill three ways; integer storage with whole-number validation prevents a class of bug that would otherwise surface as "the balances don't add up."

Validation & edge cases

Most of the thinking in this domain isn't the happy path, it's everything that can go wrong. I specified and enforced the rules below; together they're what makes the feature trustworthy rather than merely functional.

Rule

Payer must be an existing user and a member of the group

Every split member must be a member of the group

Total and every split amount must be whole numbers

Expense must belong to the group in the request path

Only the payer or the group admin may edit or delete an expense

Can't invite yourself; can't double-invite a pending user; can't re-add an active member

If splits fail to save, the parent expense is rolled back and removed

Why it exists

Prevents expenses attributed to outsiders or ghosts


A bill can only be split among people actually in the group

Satisfies the integer-money NFR; blocks rounding drift


Stops cross-group tampering via mismatched IDs


Authorisation: you can't alter someone else's record


Keeps the membership list clean and unambiguous



Transactional integrity — no half-written expenses


Each rule returns a specific, human-readable error with the correct HTTP status (400 / 403 / 404), which is how my domain meets the "clear validation feedback" requirement when the user is told what was wrong, not just that something failed.

Traceability & the audit trail

Because the project was specified as numbered user stories, I could trace each of my endpoints straight back to the requirement it satisfies CRUD (create/ update/ delete/ view) expenses to Stories 6 and 9, invite and member-list to Story 4.


Every state-changing action in my domain (creating an expense, sending an invite) also writes an audit-log entry, which is how my work feeds the project-wide transparency-and-accountability requirement: a group admin can later see exactly who recorded or changed what.

What was built

Invite members, Add an expenses, View expenses

Outcome 🚀

All three of my user stories shipped and were accepted: the expense/split engine recorded custom breakdowns, enforced the membership and integer-money rules, returned clear errors, and fed clean data to the balances calculation.

The planning and documentation I authored gave the team a shared spec to build against, and my interface carried the core journeys. The app deployed to staging (SQLite) and production (MySQL over HTTPS).

As an academic build it has no usage metrics, the honest outcome is a correct, well-specified domain that met its requirements and integrated cleanly into a team system.

Reflection

Two things I'd do differently:

  1. Treat documentation as living: my plans were right when written, but the build moved on: float amounts became integers for the NFR, and a planned React front end shipped as vanilla JS and keeping docs current as decisions change is the analyst's job

  2. Make the requirements executable: I wrote a Definition of Done and edge cases for every feature but verified them manually via curl; turning those into automated tests would make the spec self-checking.

How it fit the wider system

My domain plugged into a larger architecture the team built across two iterations: a FastAPI backend with a modular router per domain, a browser frontend, JWT-secured access, and Docker-based staging and production deployments. Working inside that shared structure, agreeing interfaces with the balances and audit domains, conforming to the team's data model and the project's NFRs was as much of the job as the code itself.

My scope (and what was the team's)

This was a team project, and I want to be precise about my ownership, because the rest of this case study only claims what was genuinely mine.


My work fell into three areas:


Analysis & documentation (system-wide). I authored the project's planning and documentation on GitHub: a development plan that, feature by feature, set out the user stories, input/output API contracts with status codes, database designs, a Definition of Done, and edge cases. This covered the whole system, including domains my teammates coded, plus the supporting docs: a requirements-to-implementation checklist, the entity-relationship diagram, the API specification, the architecture write-up, and the milestone tracker.


UX/UI design. I designed the platform's interface and user flows and built the front end, working from a Figma design system through to the live screens.


Owned code domain. We split implementation with a person-in-charge (PIC) model, and I was PIC for three user stories (core transaction domain):

  • Story 4: Invite new members to a group and view the current member list

  • Story 6: Create, update, and delete expenses, specifying amount, date, payer, and the cost-sharing breakdown

  • Story 9: View all expenses in a group, with each expense's split shown

Design as analysis: surfacing cross-domain flow dependencies

The contribution I'm proudest of didn't come from the code, it came from the design work feeding back into it. Designing the actual screens forced every flow to become concrete, and that's where the gaps showed. Even though we each owned a separate domain, the flows were coupled, so a decision in one place quietly constrained another:


  • An expense can only be split among active members, so the expense flow depends on the invitation/membership status flow working correctly first.

  • Removing a member who is the payer on a recurring expense would orphan that rule, so the member-removal flow has to account for the recurring-expense domain.

  • An expense with no valid members to split across produces a divide-by-zero, bad data that then flows straight into the balances calculation.


Because I was the one mapping these journeys end to end, I could spot the interdependencies and raise them with the teammates who owned the affected domains, so they were handled deliberately rather than discovered late. That's the part of the work that was most clearly business analysis: not building one feature, but seeing how the features constrain each other and getting the right person the information in time.

Copyright © Website design & Content by Chew Lijuan

Interested in working together? Drop me a line

Copy Email

  • Available for Work

    Get in Touch

    Available for Work

    Get in Touch

    Available for Work

    Get in Touch

  • Available for Work

    Get in Touch