feat: initial backend setup with Bun, Hono, and Drizzle ORM
- Added documentation for architecture and code plan - Implemented database schema for users, expenses, settlements, and transactions - Set up double-entry bookkeeping system with multi-currency support - Configured Hono web framework with middleware - Integrated Drizzle ORM for database operations
This commit is contained in:
585
docs/architecture.md
Normal file
585
docs/architecture.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# Splitwise Architecture
|
||||
|
||||
This document outlines the detailed architecture for the Splitwise-style application, focusing on the database design and backend endpoints. The application implements a double-entry bookkeeping system and supports multiple currencies.
|
||||
|
||||
## Database Architecture
|
||||
|
||||
### Core Entities and Relationships
|
||||
|
||||
```
|
||||
[Users] 1--N [Auth]
|
||||
1
|
||||
|
|
||||
N
|
||||
[Contacts]
|
||||
|
|
||||
N
|
||||
[Users] 1--N [Accounts] N--1 [Currencies]
|
||||
1 1 1 1
|
||||
| | | |
|
||||
| | | N
|
||||
N | N |
|
||||
[Expenses] | [Transactions]-+
|
||||
1 | N N
|
||||
| | | |
|
||||
N | | |
|
||||
[ExpenseParticipants] |
|
||||
|
|
||||
[Settlements]
|
||||
|
|
||||
N
|
||||
[Notifications]
|
||||
```
|
||||
|
||||
Entity Relationship Legend:
|
||||
- `1` and `N` indicate one-to-many relationships
|
||||
- Lines connect related entities
|
||||
- Entities in square brackets `[]`
|
||||
|
||||
### Schema Details
|
||||
|
||||
#### Users
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
profile_photo_url TEXT,
|
||||
default_currency_code CHAR(3) NOT NULL DEFAULT 'USD',
|
||||
locale VARCHAR(10) NOT NULL DEFAULT 'en-US',
|
||||
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### Auth (managed by better-auth)
|
||||
|
||||
```sql
|
||||
CREATE TABLE auth (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
provider_user_id VARCHAR(255),
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash TEXT,
|
||||
refresh_token TEXT,
|
||||
last_login TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Contacts
|
||||
|
||||
```sql
|
||||
CREATE TABLE contacts (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
contact_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'accepted', 'declined')),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, contact_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Currencies
|
||||
|
||||
```sql
|
||||
CREATE TABLE currencies (
|
||||
code CHAR(3) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
symbol VARCHAR(10) NOT NULL,
|
||||
decimal_digits INT NOT NULL DEFAULT 2,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
```
|
||||
|
||||
#### Exchange Rates
|
||||
|
||||
```sql
|
||||
CREATE TABLE exchange_rates (
|
||||
id UUID PRIMARY KEY,
|
||||
from_currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
to_currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
rate DECIMAL(24, 12) NOT NULL,
|
||||
effective_date DATE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(from_currency_code, to_currency_code, effective_date)
|
||||
);
|
||||
```
|
||||
|
||||
#### Accounts (for double-entry bookkeeping)
|
||||
|
||||
```sql
|
||||
CREATE TABLE accounts (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('asset', 'liability')),
|
||||
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, currency_code)
|
||||
);
|
||||
```
|
||||
|
||||
#### Expenses
|
||||
|
||||
```sql
|
||||
CREATE TABLE expenses (
|
||||
id UUID PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0),
|
||||
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
payer_id UUID NOT NULL REFERENCES users(id),
|
||||
receipt_image_url TEXT,
|
||||
created_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
#### Expense Participants
|
||||
|
||||
```sql
|
||||
CREATE TABLE expense_participants (
|
||||
id UUID PRIMARY KEY,
|
||||
expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
share_amount DECIMAL(20, 2) NOT NULL CHECK (share_amount >= 0),
|
||||
share_percentage DECIMAL(5, 2) GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN (SELECT amount FROM expenses WHERE id = expense_id) > 0
|
||||
THEN (share_amount / (SELECT amount FROM expenses WHERE id = expense_id)) * 100
|
||||
ELSE 0
|
||||
END
|
||||
) STORED,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(expense_id, user_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Transactions (double-entry bookkeeping)
|
||||
|
||||
```sql
|
||||
CREATE TABLE transactions (
|
||||
id UUID PRIMARY KEY,
|
||||
expense_id UUID REFERENCES expenses(id) ON DELETE SET NULL,
|
||||
settlement_id UUID REFERENCES settlements(id) ON DELETE SET NULL,
|
||||
from_account_id UUID NOT NULL REFERENCES accounts(id),
|
||||
to_account_id UUID NOT NULL REFERENCES accounts(id),
|
||||
amount DECIMAL(20, 2) NOT NULL,
|
||||
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
exchange_rate DECIMAL(24, 12) NOT NULL DEFAULT 1.0,
|
||||
native_amount DECIMAL(20, 2) GENERATED ALWAYS AS (amount * exchange_rate) STORED,
|
||||
date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('expense', 'settlement')),
|
||||
memo TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CHECK (expense_id IS NOT NULL OR settlement_id IS NOT NULL),
|
||||
CHECK (from_account_id != to_account_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Settlements
|
||||
|
||||
```sql
|
||||
CREATE TABLE settlements (
|
||||
id UUID PRIMARY KEY,
|
||||
payer_id UUID NOT NULL REFERENCES users(id),
|
||||
receiver_id UUID NOT NULL REFERENCES users(id),
|
||||
amount DECIMAL(20, 2) NOT NULL CHECK (amount > 0),
|
||||
currency_code CHAR(3) NOT NULL REFERENCES currencies(code),
|
||||
date TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
memo TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
CHECK (payer_id != receiver_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Balances (materialized view or calculated on-the-fly)
|
||||
|
||||
```sql
|
||||
CREATE VIEW user_balances AS
|
||||
SELECT
|
||||
u1.id AS user_id,
|
||||
u2.id AS other_user_id,
|
||||
t.currency_code,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN t.from_account_id = a1.id THEN -t.amount
|
||||
WHEN t.to_account_id = a1.id THEN t.amount
|
||||
ELSE 0
|
||||
END
|
||||
) AS balance
|
||||
FROM
|
||||
users u1
|
||||
CROSS JOIN users u2
|
||||
JOIN accounts a1 ON u1.id = a1.user_id
|
||||
JOIN accounts a2 ON u2.id = a2.user_id
|
||||
JOIN transactions t ON
|
||||
(t.from_account_id = a1.id AND t.to_account_id = a2.id) OR
|
||||
(t.from_account_id = a2.id AND t.to_account_id = a1.id)
|
||||
WHERE
|
||||
u1.id != u2.id
|
||||
AND a1.currency_code = a2.currency_code
|
||||
AND a1.currency_code = t.currency_code
|
||||
GROUP BY
|
||||
u1.id, u2.id, t.currency_code;
|
||||
```
|
||||
|
||||
#### Notifications
|
||||
|
||||
```sql
|
||||
CREATE TABLE notifications (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type VARCHAR(50) NOT NULL CHECK (type IN ('expense_added', 'expense_edited', 'expense_deleted', 'settlement_received')),
|
||||
content JSONB NOT NULL,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
### Double-Entry Bookkeeping Implementation
|
||||
|
||||
Every financial event in the system (expenses and settlements) is recorded as a transaction with two sides:
|
||||
|
||||
1. A debit to one account (increasing an asset or decreasing a liability)
|
||||
2. A credit to another account (decreasing an asset or increasing a liability)
|
||||
|
||||
#### Example: Creating an Expense
|
||||
|
||||
When User A records a $30 expense that they paid for, which is split equally between User A, User B, and User C:
|
||||
|
||||
1. User A paid $30 (creates a transaction)
|
||||
|
||||
- Credit User A's account (they're owed money) +$20
|
||||
- Debit User B's account (they owe money) -$10
|
||||
- Debit User C's account (they owe money) -$10
|
||||
|
||||
2. Corresponding transactions are created:
|
||||
- Transaction 1: from User B's account to User A's account for $10
|
||||
- Transaction 2: from User C's account to User A's account for $10
|
||||
|
||||
#### Example: Settling a Debt
|
||||
|
||||
When User B settles their $10 debt to User A:
|
||||
|
||||
1. A settlement record is created
|
||||
2. A transaction is recorded:
|
||||
- Debit User A's account -$10 (they received money)
|
||||
- Credit User B's account +$10 (they paid money)
|
||||
|
||||
### Multi-Currency Support
|
||||
|
||||
The system supports multiple currencies through:
|
||||
|
||||
1. A currencies table with ISO currency codes
|
||||
2. Exchange rates tracking between currency pairs
|
||||
3. Storing all transactions in their original currency
|
||||
4. Recording exchange rates used at the time of transaction
|
||||
5. Converting balances to user's preferred currency on-the-fly
|
||||
|
||||
When an expense is created in a different currency:
|
||||
|
||||
1. The expense is recorded in its original currency
|
||||
2. Each participant's share is calculated in the original currency
|
||||
3. Transactions are created in the original currency
|
||||
4. Exchange rates are applied when viewing balances in a different currency
|
||||
|
||||
## Backend API Endpoints
|
||||
|
||||
### Authentication (managed by better-auth)
|
||||
|
||||
```
|
||||
POST /api/auth/register
|
||||
POST /api/auth/login
|
||||
POST /api/auth/social/:provider
|
||||
POST /api/auth/social/:provider/callback
|
||||
POST /api/auth/logout
|
||||
GET /api/auth/me
|
||||
POST /api/auth/refresh-token
|
||||
POST /api/auth/forgot-password
|
||||
POST /api/auth/reset-password
|
||||
```
|
||||
|
||||
### Users and Profiles
|
||||
|
||||
```
|
||||
GET /api/users/profile
|
||||
PUT /api/users/profile
|
||||
PUT /api/users/settings
|
||||
GET /api/users/search?query=:query
|
||||
```
|
||||
|
||||
### Contacts
|
||||
|
||||
```
|
||||
GET /api/contacts
|
||||
POST /api/contacts
|
||||
GET /api/contacts/:id
|
||||
PUT /api/contacts/:id
|
||||
DELETE /api/contacts/:id
|
||||
```
|
||||
|
||||
### Currencies and Exchange Rates
|
||||
|
||||
```
|
||||
GET /api/currencies
|
||||
GET /api/currencies/:code
|
||||
GET /api/exchange-rates?from=:code&to=:code&date=:date
|
||||
```
|
||||
|
||||
### Expenses
|
||||
|
||||
```
|
||||
GET /api/expenses?page=:page&limit=:limit
|
||||
POST /api/expenses
|
||||
GET /api/expenses/:id
|
||||
PUT /api/expenses/:id
|
||||
DELETE /api/expenses/:id
|
||||
POST /api/expenses/:id/receipt
|
||||
```
|
||||
|
||||
### Expense Participants
|
||||
|
||||
```
|
||||
GET /api/expenses/:expenseId/participants
|
||||
POST /api/expenses/:expenseId/participants
|
||||
PUT /api/expenses/:expenseId/participants/:id
|
||||
DELETE /api/expenses/:expenseId/participants/:id
|
||||
```
|
||||
|
||||
### Balances
|
||||
|
||||
```
|
||||
GET /api/balances?currency=:code
|
||||
GET /api/balances/:userId?currency=:code
|
||||
GET /api/balances/summary?currency=:code
|
||||
```
|
||||
|
||||
### Settlements
|
||||
|
||||
```
|
||||
POST /api/settlements
|
||||
GET /api/settlements?page=:page&limit=:limit
|
||||
GET /api/settlements/:id
|
||||
```
|
||||
|
||||
### Transactions (mostly internal)
|
||||
|
||||
```
|
||||
GET /api/transactions?page=:page&limit=:limit
|
||||
GET /api/transactions/:id
|
||||
```
|
||||
|
||||
### Notifications
|
||||
|
||||
```
|
||||
GET /api/notifications
|
||||
PUT /api/notifications/:id/read
|
||||
PUT /api/notifications/read-all
|
||||
```
|
||||
|
||||
## API Request/Response Examples
|
||||
|
||||
### Creating an Expense
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
POST /api/expenses
|
||||
{
|
||||
"title": "Dinner at Restaurant",
|
||||
"description": "Birthday celebration",
|
||||
"amount": 150.00,
|
||||
"currency_code": "USD",
|
||||
"date": "2023-05-15T19:30:00Z",
|
||||
"payer_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"participants": [
|
||||
{
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"share_amount": 50.00
|
||||
},
|
||||
{
|
||||
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
||||
"share_amount": 50.00
|
||||
},
|
||||
{
|
||||
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
|
||||
"share_amount": 50.00
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
"title": "Dinner at Restaurant",
|
||||
"description": "Birthday celebration",
|
||||
"amount": 150.0,
|
||||
"currency_code": "USD",
|
||||
"date": "2023-05-15T19:30:00Z",
|
||||
"payer_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"participants": [
|
||||
{
|
||||
"id": "d9d9e7b6-a5c5-4988-810f-f81b137e31b3",
|
||||
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"share_amount": 50.0,
|
||||
"share_percentage": 33.33
|
||||
},
|
||||
{
|
||||
"id": "8a8a8a8a-7425-40de-944b-e07fc1f90ae7",
|
||||
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
||||
"share_amount": 50.0,
|
||||
"share_percentage": 33.33
|
||||
},
|
||||
{
|
||||
"id": "9b9b9b9b-c54c-4b01-90e6-d701748f0851",
|
||||
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
|
||||
"share_amount": 50.0,
|
||||
"share_percentage": 33.33
|
||||
}
|
||||
],
|
||||
"created_at": "2023-05-15T19:35:00Z",
|
||||
"updated_at": "2023-05-15T19:35:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Viewing Balances
|
||||
|
||||
**Request:**
|
||||
|
||||
```
|
||||
GET /api/balances?currency=USD
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"balances": [
|
||||
{
|
||||
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
||||
"display_name": "John Doe",
|
||||
"profile_photo_url": "https://example.com/photos/john.jpg",
|
||||
"currency_code": "USD",
|
||||
"amount": -50.0,
|
||||
"last_activity": "2023-05-15T19:35:00Z"
|
||||
},
|
||||
{
|
||||
"user_id": "6a2f41a3-c54c-4b01-90e6-d701748f0851",
|
||||
"display_name": "Jane Smith",
|
||||
"profile_photo_url": "https://example.com/photos/jane.jpg",
|
||||
"currency_code": "USD",
|
||||
"amount": -50.0,
|
||||
"last_activity": "2023-05-15T19:35:00Z"
|
||||
}
|
||||
],
|
||||
"total_owed_to_you": 100.0,
|
||||
"total_you_owe": 0.0,
|
||||
"net_balance": 100.0
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Settlement
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
POST /api/settlements
|
||||
{
|
||||
"payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
||||
"receiver_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"amount": 50.00,
|
||||
"currency_code": "USD",
|
||||
"date": "2023-05-16T14:00:00Z",
|
||||
"memo": "Paying back for dinner"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "3a3a3a3a-58cc-4372-a567-0e02b2c3d479",
|
||||
"payer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
|
||||
"payer_name": "John Doe",
|
||||
"receiver_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"receiver_name": "Alice Johnson",
|
||||
"amount": 50.0,
|
||||
"currency_code": "USD",
|
||||
"date": "2023-05-16T14:00:00Z",
|
||||
"memo": "Paying back for dinner",
|
||||
"created_at": "2023-05-16T14:02:00Z",
|
||||
"updated_at": "2023-05-16T14:02:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### Double-Entry Bookkeeping Logic
|
||||
|
||||
1. **Transaction Creation**:
|
||||
|
||||
- Every expense and settlement must create balanced transactions
|
||||
- System must ensure debits equal credits for all transactions
|
||||
|
||||
2. **Balance Calculation**:
|
||||
- Balances are derived from the transaction ledger
|
||||
- Each user's balance with another user is calculated by summing all transactions between them
|
||||
- Negative balance means the user owes money; positive means they are owed money
|
||||
|
||||
### Multi-Currency Implementation
|
||||
|
||||
1. **Currency Conversion**:
|
||||
|
||||
- Use exchange rates from reliable sources (or integrate with exchange rate APIs)
|
||||
- Store historical exchange rates for accurate reporting
|
||||
- Implement conversion functions at the service layer
|
||||
|
||||
2. **User Preferences**:
|
||||
|
||||
- Allow users to set default currency
|
||||
- Provide options to view balances in different currencies
|
||||
|
||||
3. **Settlement Handling**:
|
||||
- Support settlements in different currencies
|
||||
- Record the exchange rate used at settlement time
|
||||
- Handle currency conversion fees if applicable
|
||||
|
||||
### Security and Data Integrity
|
||||
|
||||
1. **Transaction Atomicity**:
|
||||
|
||||
- Use database transactions to ensure all related changes succeed or fail together
|
||||
- Implement validation to ensure expense shares sum to the total amount
|
||||
|
||||
2. **Audit Trail**:
|
||||
|
||||
- Log all financial operations
|
||||
- Prevent modification of completed transactions
|
||||
- Allow corrections through compensating transactions
|
||||
|
||||
3. **Data Validation**:
|
||||
- Validate all currency amounts and exchange rates
|
||||
- Ensure proper decimal precision handling
|
||||
- Implement comprehensive input validation
|
||||
|
||||
## Conclusion
|
||||
|
||||
This architecture leverages double-entry bookkeeping principles to provide a robust foundation for tracking expenses and debts between users. The multi-currency support ensures the application can be used globally, with accurate conversion and balance tracking across different currencies.
|
||||
|
||||
The system design prioritizes data integrity, ensuring that all financial records are accurate, balanced, and traceable. By implementing this architecture, the Splitwise application will provide users with a reliable platform for managing shared expenses across multiple currencies.
|
||||
Reference in New Issue
Block a user