By Awa Engineering Team| 31 August 2023
Most of what we’re building in Awa isn’t rocket science and
that’s kind of the point.
It’s not that complicated… but we’d like you to see how cool it is.
We start with one simple fact: one person covers the cost,
and everyone else owes their portion. From there, we keep the process
transparent list the participants (minus the payer) and split the total
down to two decimals. If tehre's a leftopver cent, we add it to the first share so
everything balances perfectly, At creation :
-The total amount is normalized to two decimal places using BigDecimal
to eliminate floating-point drift.
-The amount is divided evenly across participants.
-Any rounding remainder is allocated to the first share to guarantee that Σ shares = total.
val total = BigDecimal.valueOf(req.amount).setScale(2, HALF_UP) val per = total.divide(BigDecimal.valueOf(participants.size.toLong()), 2, HALF_UP) ...
This kinda just keeps aggregates consistent across the UI, repository, and database.
Payment status
Paying people back usually involves a bit of back-and-forth.
To keep it simple, we treat each
share as being in one of three states: pending, marked as paid,
or confirmed.(Thanks Dr Bautista lmao , and Michael Sips - FINITE AUTOMATA mit opencourse ware)
-PENDING → nothing claimed yet
-MARK_PAID → “I sent it”
-CONFIRMED → “Yep, received”
Only the bill creator is authorized to confirm transitions into CONFIRMED.
Once all shares reach CONFIRMED,
the bill itself resolves to isPaid = true.
| From \ Event | markPaid (any debtor) |
confirm (creator only) |
reject (creator only) |
|---|---|---|---|
PENDING |
MARKED_PAID |
— | — |
MARKED_PAID |
— | CONFIRMED |
PENDING |
CONFIRMED |
— | — | — |
Idempotency by construction
We avoided explicit idempotency keys by structuring operations to be idempotent:
- Re-tapping “Mark as paid” is a noop( no-operation we just wanted to sound smart 😔 ) if the share is already MARKED_PAID or CONFIRMED.
- Confirming a share twice results in no state change.
-Rejecting a share resets it to PENDING and clears its timestamp, ensuring consistent state replay.
This pattern allows the UI to optimistically update without risking double-entries or inconsistent transactions.
Frontend synchronization
On the Flutter side:
-Amount inputs are formatted dynamically using a currency symbol provider. (( this doesn't even work well and we don't even know how to fix it 😔))
-Splits are recalculated client-side before the request is sent, ensuring the user sees per-person amounts immediately.
- The payer’s displayed “amount” reflects their net receivable, not the full bill amount, aligning the UI with what's in the backend
This guarantees that what the user sees is exactly what will be persisted.
Contracts as bill templates
Recurring obligations (e.g., rent, utilities) are treated as contracts: bills
flagged with isContract = true. In the current implementation,
contracts are just bills filtered into a separate list( so they're easier to find). Future iterations
can extend this model with recurrence metadata and automated bill generation.
Aggregates: you owe / you’re owed
Two aggregates should help the dashboard( we didnt later add this , couldnt make it work well):
- “You owe” = sum of the current user’s non-CONFIRMED shares.
- “You’re owed” = sum of all outstanding shares on bills where the user is the creator.
- The latter is computed server-side during bill fetch, so it accounts for shares that have been MARKED_PAID but not yet confirmed.
Well , there goes our simple maths created service for sorting bills
****************************************
States (Q):
Q={PENDING,MARKED_PAID,CONFIRMED}
Alphabet (Σ):
Σ={markPaid,confirm,reject}
Transition function (δ):
δ:Q×Σ→Q
Defined as:
δ(PENDING,markPaid)=MARKED_PAID
δ(MARKED_PAID,confirm)=CONFIRMED
δ(MARKED_PAID,reject)=PENDING
All other transitions are identity (noop).
Start state (q₀):
q0=PENDING
Accepting state(s) (F):
F={CONFIRMED}