Beyond Splitwise: What a Modern Bill-Splitting Backend Actually Looks Like in 2026
Most developers look at Splitwise and think: "Ah, it's just a group ledger with some math." Then they try to build one. That's when the real complexity surfaces. This is a deep dive into the system design of expense-splitting applications, starting from Splitwise's well documented architecture and walking through the layers of complexity that a modern implementation introduces. If you are a backend engineer or a curious product builder, buckle up.

Why This Problem is Harder Than It Looks?
The core of any expense splitting app is a new balance graph: nodes are users, edges are directed debts. The challenge is that this graph is built from an immutable stream of financial events (expenses), not directly stored values.
Every design decision downstream flows from this: do you store a materialized balances table that gets updated on every write, or do you compute balances live from the expenses + splits ledger? Splitwise historically used a materialized balance approach. A dedicated balance table storing the running (user_id, owes_to, amount) tuple, updated atomically with each expense write. This is fast to reach but introduces dual-write consistency risk. If the expense insert succeeds but the balance update fails, your ledger diverges from reality.
A modern approach inverts this: keep expenses and splits as the source of truth and compute balances as a SQL view. Slower on read, but zero consistency drift.
Splitwise's Architecture: The Known Baseline
Splitwise's publicly documented schema is relatively lean for what it does:
users- identitygroups+members- social graphexpenses- the event recordexpense_splits- per-user share of each expensebalances- materialized net balance per user pairpayments/settlements- recorded payoffs
The debt simplification algorithm. The "Simplify Debts" button is the most technically interesting piece. It models the balances graph as a Minimum Cost Flow problem: given N users with net balance (some positive, some negative), find the minimum number of transactions to zero out all balances while preserving each person's net position.
Splitwise uses a greedy heuristic (not the NP-hard optimal solution because the true optimal solution via backtracking is )(2ⁿ) which is impractical beyond ~20 users. The greedy approach sorts users by net balance, then greedily matches the highest creditor with the highest debtor in each pass. This runs in O(n log n) and gets very close to optimal in practice.
// Simplified greedy debt minimization
while (max_creditor > 0 && max_debtor < 0):
settle = min(max_creditor.balance, abs(max_debtor.balance))
record transaction(max_debtor → max_creditor, settle)
update balances
What Splitwise's architecture does well: It's fast, the schema is flat and queryable, and the greedy algorithm is battle-tested at scale.
What it was not designed for: Real-time collaboration, trust/verification layers, AI-driven context, or legal enforceability of agreements. All of which are growing user expectations in 2026.
Where Modern Complexity Begins
When you design an expense-splitting system today, you inherit new requirements that Splitwise's 2011-era schema didn't anticipate. Here's where the architectural complexity multiples:
3.1 Multi-dimensional Split Types
Splitwise supports equal and percentage splits. A modern implementation adds shared-based splitting where user A has 3 shares and user B has 1 share out of 4 total, getting a proportional 75/25 split. This sounds simple until you realise your expense_splits table must now carry a polymorphic split_type field and the calculation logic must branch into four strategies. The Factory design pattern handles this cleanly, one SplitStrategy interface, four implementations: EqualSplit, CustomSplit, PercentageSplit, SharedSplit. each is independently testable and the controller never needs to know which one is running.
3.2 The Approval State Machine
A naive implementation create and expense and immediately marks it active. But in systems where the added expense affects multiple people financially, there's a legitimate need for an approval workflow. The expense table grows a status column with pending_approval → active → rejected and each row in expense_splits tracks individual approval per participant. This is a significant schema and business logic addition: you now need DB triggers or application-level event handlers to determine when enough approvals have been collected to flip the expense to active.
The complication: what if one of five participants rejects? Does the whole expense collapse? Does it split into a modified expense? Your state machine needs to define this explicitly and your schema needs to record rejection_reason per participant for auditability.
3.3 Legal-Grade Agreement Layers
This is where the complexity jumps a full order of magnitude. Imagine extending expense creation with an opt-in agreement mode: the expense initiator writes terms, all participants receive a document for digital signature (scribble-based), and the signed agreement is stored with the transaction permanently. Now you're not just a financial ledger, you're a lightweight legal record system.
The schema complexity this introduces:
An
agreementstable with its own status machine (pending → partially_signed → completed)An
agreement_signaturestable with blob storage references for each scribble signature.Storage policies that make signatures immutable once written (no UPDATE, only INSERT)
A notification pipeline that delivers the agreement to each participant and collects their action
A document rendering layer that assembles the final signed document from parts
The architectural implication: your Edge Functions layer now handles document orchestration, not just CRUD. The send_agreement_for_signature function must: fetch the expense, generate terms, create the agreement row, fan out to all participants via push notification, and track state transitions asynchronously.
3.4 Realtime Graph Consistency
Splitwise is session-based. So you refresh to see new data. A modern implementation uses WebSocket subscription. In Supabase, enabling Postgres' REPLICA IDENTITY FULL on tables like expense_splits, notifications, and settlements lets the client subscribe to row-level changes live. The challenge: you need to design your React Query cache invalidation strategy around these realtime events, not just API response. An incoming Realtime change on expense_splits should trigger a re-query of the computed balance view, not just update the splits list in isolation.
The Modern Tech Stack Comparison
| Dimension | Splitwise-Era Pattern | Modern Pattern |
|---|---|---|
| Auth | Email + password, OAuth | Phone OTP (Twilio), passwordless |
| DB | MySQL/PostgreSQL, direct queries | PostgreSQL + RLS policies as the security layer |
| Balance storage | Materialized balances table |
Computed SQL view from immutable ledger |
| Real-time | Polling / manual refresh | WebSocket subscriptions (Supabase Realtime) |
| File storage | S3 direct | Supabase Storage with per-row signed URLs |
| Notifications | Email via SendGrid | Push (Expo Push API) + in-app Realtime |
| Backend logic | Rails/Django controller layer | Serverless Edge Functions (Deno) |
| Security | App-layer auth checks | Row-Level Security in the DB itself supabase |
| AI | None | Embedded inference via Edge Functions supabase |
The most underrated shift in this table is RLS replacing application-layer auth. In a Splitwise-style codebase, every controller method has guards like if expense.group_id not in user.groups: raise Forbidden. In a Supabase-powered implementation, the database itself enforces: SELECT * FROM expenses WHERE id IN (SELECT expense_id FROM expense_splits WHERE user_id = auth.uid()). The policy runs at the Postgres executor level — it's impossible to bypass with a misconfigured controller.
AI as a First-Class Architectural Concern
Splitwise's AI involvement is limited to spending reports. What changes when you treat AI as a core system layer, not a feature?
You need an ai_insights table that stores generated insights per user with insights_type, generated_at and deduplication logic to avoid re-generating the same pattern twice. You need cron-based Edge Functions that query the user's expense history on a schedule, construct a structured prompt with their debt summary and spending patterns, call an LLM API, and result back to the DB.
The harder problem is AI-powered payment reminders where the system not only sends a push notification but generates contextually appropriate copy. "Hey, you haven't settled with Priya in 12 days and you owe $400" reads very differently from a inference at Edge Function runtime, and delivery via the push notification pipeline, all chained together as an asynchronous job.
The key architectural principal. AI lives in the Edge Functions layer, not in the client. The React Native app only consumes pre-generated insights from the ai_insights table, it never calls an LLM directly. This keeps API keys server-side, controls costs via cron cadence, and makes insights cacheable.
The Complexity Scorecard
| System Concern | Splitwise (Classic) | Modern Implementation |
|---|---|---|
| Schema tables | ~6 core tables | 16–18 tables with state machines |
| Split strategies | Equal + percentage | Equal, custom, percentage, shares |
| Security model | App-layer guards | DB-level RLS policies |
| Agreement/trust | None | Digital signature pipeline |
| Settlement methods | Manual record | Cash, bank proof, deep-link payment apps |
| AI involvement | Reports only | Insights, reminders, notifications |
| Real-time | Polling | WebSocket subscriptions |
| Debt algorithm | Greedy O(n log n) | Greedy + optional backtracking for small groups |
| Notification types | Push, in-app, AI-generated copy | |
| Storage | Receipt photos | Receipts + signatures + banners (immutable policies) |
Building an expense-splitting app is fantastic system design exercise precisely because it spans so many domains: financial ledger design, graph algorithms, real-time data, asynchronous workflows, access control, document management and increasingly, AI orchestration. Splitwise solved the core problem elegantly for its era. The next generation of these system isn't just about adding features, it's about rethinking each layer of the stack to handle trust, legality and intelligence as first-class concerns.
The debt simplification algorithm hasn't changed. Everything around it has.

