Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 185 additions & 20 deletions solutions/golang/splitwise/README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,187 @@
# Designing Splitwise
# Splitwise - Low Level Design

## Requirements
1. The system should allow users to create accounts and manage their profile information.
2. Users should be able to create groups and add other users to the groups.
3. Users should be able to add expenses within a group, specifying the amount, description, and participants.
4. The system should automatically split the expenses among the participants based on their share.
5. Users should be able to view their individual balances with other users and settle up the balances.
6. The system should support different split methods, such as equal split, percentage split, and exact amounts.
7. Users should be able to view their transaction history and group expenses.
8. The system should handle concurrent transactions and ensure data consistency.

## Classes, Interfaces and Enumerations
1. The **User** class represents a user in the Splitwise system, with properties such as ID, name, email, and a map to store balances with other users.
2. The **Group** class represents a group in Splitwise, containing a list of member users and a list of expenses.
3. The **Expense** class represents an expense within a group, with properties such as ID, amount, description, the user who paid, and a list of splits.
4. The **Split** class is an abstract class representing the split of an expense. It is extended by EqualSplit, PercentSplit, and ExactSplit classes to handle different split methods.
5. The **Transaction** class represents a transaction between two users, with properties such as ID, sender, receiver, and amount.
6. The **SplitwiseService** class is the main class that manages the Splitwise system. It follows the Singleton pattern to ensure only one instance of the service exists.
7. The SplitwiseService class provides methods for adding users, groups, and expenses, splitting expenses, updating balances, settling balances, and creating transactions.
8. Multi-threading is achieved using concurrent data structures such as ConcurrentHashMap and CopyOnWriteArrayList to handle concurrent access to shared resources.
9. The **SplitwiseDemo** class demonstrates the usage of the Splitwise system by creating users, a group, adding an expense, settling balances, and printing user balances.
1. Users can create accounts and manage profile information
2. Users can create groups and add other users to groups
3. Users can add expenses within a group, specifying amount, description, and participants
4. The system automatically splits expenses among participants based on their share
5. Users can view individual balances with other users and settle up
6. Support for different split methods: equal, percentage, and exact amounts
7. Users can view transaction history and group expenses
8. The system handles concurrent transactions with data consistency

## Architecture

```
API call (CreateExpense)
|
v
Facade (SplitwiseService) <-- single entry point, holds the lock
|
+-- resolves paidBy user
|
+-- Factory (NewExpense) <-- builds the expense
| |
| +-- Registry lookup <-- picks the right SplitStrategy
| |
| +-- Strategy.Validate() <-- strategy-specific validation
| |
| +-- resolves participant IDs
| |
| +-- Strategy.Compute() <-- strategy-specific split math
|
+-- Group.AddExpense() <-- membership validation
|
+-- updateBalances() <-- balance ledger update
```

## Design Patterns

### 1. Strategy Pattern (`split.go`, `strategies.go`)

The `SplitStrategy` interface defines the contract for splitting algorithms:

```go
type SplitStrategy interface {
Validate(amount float64, participants []Participant) error
Compute(amount float64, users []*User, participants []Participant) []Split
}
```

Three concrete strategies implement this interface:
- **EqualStrategy** — divides the amount equally, ignores `Participant.Value`
- **PercentStrategy** — uses `Value` as percentage, validates they sum to 100
- **ExactStrategy** — uses `Value` as dollar amount, validates they sum to the total

### 2. Registry Pattern (`strategies.go`)

Strategies are registered in a map, decoupling strategy selection from expense creation:

```go
var strategyRegistry = map[ExpenseType]SplitStrategy{
Equal: NewEqualStrategy(),
Percent: NewPercentStrategy(),
Exact: NewExactStrategy(),
}
```

Adding a new split type (e.g., shares-based) requires:
1. Implement `SplitStrategy`
2. Add one entry to `strategyRegistry`

No changes to `NewExpense`, the service, or any existing strategy.

### 3. Singleton Pattern (`service.go`)

`GetService()` uses `sync.Once` for a thread-safe single instance:

```go
var once sync.Once
func GetService() *SplitwiseService {
once.Do(func() { instance = &SplitwiseService{...} })
return instance
}
```

`ResetService()` enables clean test isolation by resetting the singleton.

### 4. Factory Method Pattern (`expense.go`)

`NewExpense` is a single factory that:
- Looks up the strategy from the registry
- Runs strategy-specific validation
- Resolves user IDs via a lookup function
- Delegates split computation to the strategy
- Returns a fully constructed `Expense`

Callers never construct splits or pick strategies manually.

### 5. Facade Pattern (`service.go`)

`SplitwiseService.CreateExpense()` is the single public entry point that orchestrates user lookup, expense creation, group membership validation, and balance updates behind one method call.

## File Structure

```
splitwise/
├── user.go User model (ID, Name, Email)
├── split.go Split struct + SplitStrategy interface
├── strategies.go EqualStrategy, PercentStrategy, ExactStrategy + registry
├── expense.go Expense model, Participant input, NewExpense factory
├── group.go Group with membership management and validation
├── transaction.go Transaction record for settlements
├── service.go SplitwiseService (singleton, facade, concurrency)
├── demo.go Working demo exercising all features
└── splitwise_test.go Tests (equal/percent/exact, settlement, validation, concurrency)
```

## API Usage

All interaction goes through `SplitwiseService` using string IDs — ready for HTTP/gRPC:

```go
service := GetService()

// Register users
service.AddUser(NewUser("1", "Alice", "alice@example.com"))
service.AddUser(NewUser("2", "Bob", "bob@example.com"))

// Create group
group := NewGroup("g1", "Apartment")
group.AddMember(alice)
group.AddMember(bob)
service.AddGroup(group)

// Add expense — single uniform signature for all split types
service.CreateExpense("g1", "e1", 300, "Rent", "1", Equal, []Participant{
{UserID: "1"},
{UserID: "2"},
})

service.CreateExpense("g1", "e2", 200, "Groceries", "2", Percent, []Participant{
{UserID: "1", Value: 60},
{UserID: "2", Value: 40},
})

service.CreateExpense("g1", "e3", 150, "Dinner", "1", Exact, []Participant{
{UserID: "1", Value: 50},
{UserID: "2", Value: 100},
})

// Query balances
balance := service.GetBalance("1", "2")
allBalances := service.GetUserBalances("1")

// Settle up
tx, err := service.SettleBalance("2", "1")

// View history
transactions := service.GetTransactions()
```

## Concurrency

All public methods on `SplitwiseService` are protected by `sync.RWMutex`:
- **Read operations** (`GetUser`, `GetBalance`, `GetUserBalances`, `GetTransactions`) use `RLock` — multiple readers can proceed concurrently
- **Write operations** (`AddUser`, `AddGroup`, `CreateExpense`, `SettleBalance`) use `Lock` — exclusive access

This is verified by `TestConcurrentExpenses` which fires 100 goroutines adding expenses simultaneously and asserts correct final balances.

## Validation

The system validates at multiple levels:
- **Expense factory**: amount > 0, participants non-empty, user IDs exist
- **Strategy**: percent sums to 100, exact sums to total amount
- **Group**: payer and all participants must be group members

All errors are returned explicitly — no silent failures.

## Running

```bash
# Run demo
go run main.go

# Run tests
go test ./splitwise/ -v
```
101 changes: 101 additions & 0 deletions solutions/golang/splitwise/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package splitwise

import "fmt"

func Run() {
ResetService()
service := GetService()

// --- Create Users ---
alice := NewUser("1", "Alice", "alice@example.com")
bob := NewUser("2", "Bob", "bob@example.com")
charlie := NewUser("3", "Charlie", "charlie@example.com")
diana := NewUser("4", "Diana", "diana@example.com")

service.AddUser(alice)
service.AddUser(bob)
service.AddUser(charlie)
service.AddUser(diana)

// --- Create Group ---
group := NewGroup("g1", "Apartment")
group.AddMember(alice)
group.AddMember(bob)
group.AddMember(charlie)
group.AddMember(diana)
service.AddGroup(group)

// --- Expense 1: Equal Split ---
// For equal, Value is ignored — just list the user IDs
fmt.Println(">>> Expense 1: Alice pays $300 rent (equal split)")
_, err := service.CreateExpense("g1", "e1", 300, "Rent", "1", Equal, []Participant{
{UserID: "1"}, {UserID: "2"}, {UserID: "3"},
})
if err != nil {
fmt.Println("Error:", err)
return
}
service.PrintBalances()

// --- Expense 2: Percent Split ---
// Value = percentage share
fmt.Println("\n>>> Expense 2: Bob pays $200 groceries (percent split)")
_, err = service.CreateExpense("g1", "e2", 200, "Groceries", "2", Percent, []Participant{
{UserID: "1", Value: 25},
{UserID: "2", Value: 25},
{UserID: "3", Value: 25},
{UserID: "4", Value: 25},
})
if err != nil {
fmt.Println("Error:", err)
return
}
service.PrintBalances()

// --- Expense 3: Exact Split ---
// Value = exact dollar amount
fmt.Println("\n>>> Expense 3: Charlie pays $150 dinner (exact split)")
_, err = service.CreateExpense("g1", "e3", 150, "Dinner", "3", Exact, []Participant{
{UserID: "1", Value: 50},
{UserID: "2", Value: 30},
{UserID: "3", Value: 40},
{UserID: "4", Value: 30},
})
if err != nil {
fmt.Println("Error:", err)
return
}
service.PrintBalances()

// --- Settle ---
fmt.Println("\n>>> Settling Bob's debt with Alice")
tx, err := service.SettleBalance("2", "1")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(" ", tx)
}

fmt.Println("\n>>> Final Balances")
service.PrintBalances()

fmt.Println("\n>>> Transaction History")
for _, t := range service.GetTransactions() {
fmt.Println(" ", t)
}

// --- Validation: bad percent ---
fmt.Println("\n>>> Validation: percent that doesn't sum to 100")
_, err = service.CreateExpense("g1", "e4", 100, "Bad", "1", Percent, []Participant{
{UserID: "1", Value: 50},
{UserID: "2", Value: 30},
})
fmt.Println(" Error:", err)

// --- Validation: unknown user ---
fmt.Println("\n>>> Validation: unknown user ID")
_, err = service.CreateExpense("g1", "e5", 100, "Bad", "1", Equal, []Participant{
{UserID: "1"}, {UserID: "999"},
})
fmt.Println(" Error:", err)
}
22 changes: 0 additions & 22 deletions solutions/golang/splitwise/equal_split.go

This file was deleted.

Loading