Skip to main content
Building a billing system that handles real money requires more than tracking subscription plans and invoices. It demands financial-grade accounting infrastructure with double-entry bookkeeping, audit trails, and multi-jurisdiction support.

The Challenge

When implementing billing for a multi-tenant platform, several requirements emerged: Financial Integrity Requirements:
  • Every transaction must balance (debits = credits)
  • Complete audit trail for compliance
  • Support for multiple legal entities
  • Fiscal period management for reporting
  • Capsule-isolated financial data
Technical Constraints:
  • Event-sourced architecture
  • DynamoDB single-table design
  • Multi-tenant isolation guarantees
  • Zero data leakage between capsules
Compliance Needs:
  • GAAP/IFRS compatibility
  • Financial statement generation
  • Period closing and reconciliation
  • Multi-currency support foundation
Rather than building ad-hoc invoice tracking, we needed proper accounting infrastructure that could support future requirements like revenue recognition, tax calculation, and financial reporting.

The Solution

We implemented a complete accounting foundation using double-entry bookkeeping principles with four core domain models: Every billing operation happens within a legal entity context:
pub struct LegalEntity {
    pub legal_entity_id: LegalEntityId,
    pub tenant_id: TenantId,
    pub capsule_id: CapsuleId,
    pub legal_name: String,
    pub tax_id: Option<String>,
    pub jurisdiction: String,
    pub functional_currency: String,
}
Legal entities represent the company doing business, enabling multi-jurisdiction support and tax compliance.

2. Chart of Accounts

The chart of accounts defines the financial structure:
pub struct ChartOfAccounts {
    pub chart_id: ChartOfAccountsId,
    pub legal_entity_id: LegalEntityId,
    pub accounts: Vec<Account>,
}

pub struct Account {
    pub account_number: String,
    pub account_name: String,
    pub account_type: AccountType,
    pub normal_balance: DebitCredit,
}

pub enum AccountType {
    Asset,
    Liability,
    Revenue,
    Expense,
    Equity,
}
We bootstrap each legal entity with 18 standard accounts for SaaS operations: Assets:
  • 1000: Cash
  • 1100: Accounts Receivable
  • 1200: Undeposited Funds
Liabilities:
  • 2000: Accounts Payable
  • 2100: Deferred Revenue
  • 2200: Sales Tax Payable
Revenue:
  • 4000: Subscription Revenue
  • 4100: Usage Revenue
  • 4200: Professional Services
Expenses:
  • 5000: Cost of Goods Sold
  • 5100: Operating Expenses
  • 5200: Payment Processing Fees
Equity:
  • 3000: Owner’s Equity
  • 3100: Retained Earnings

3. Journal Entries

All financial transactions are recorded as journal entries with balanced debits and credits:
pub struct JournalEntry {
    pub entry_id: JournalEntryId,
    pub legal_entity_id: LegalEntityId,
    pub entry_date: NaiveDate,
    pub description: String,
    pub lines: Vec<JournalEntryLine>,
    pub posted: bool,
}

pub struct JournalEntryLine {
    pub account_number: String,
    pub debit_amount: Decimal,
    pub credit_amount: Decimal,
    pub description: String,
}
The domain enforces the fundamental accounting equation:
impl JournalEntry {
    pub fn validate_balanced(&self) -> Result<(), DomainError> {
        let total_debits: Decimal = self.lines.iter()
            .map(|l| l.debit_amount)
            .sum();
        let total_credits: Decimal = self.lines.iter()
            .map(|l| l.credit_amount)
            .sum();

        if total_debits != total_credits {
            return Err(DomainError::UnbalancedEntry {
                debits: total_debits,
                credits: total_credits,
            });
        }
        Ok(())
    }
}

4. Accounting Periods

Fiscal periods enable financial reporting and period closing:
pub struct AccountingPeriod {
    pub period_id: AccountingPeriodId,
    pub legal_entity_id: LegalEntityId,
    pub period_type: PeriodType,
    pub start_date: NaiveDate,
    pub end_date: NaiveDate,
    pub status: PeriodStatus,
}

pub enum PeriodType {
    Month,
    Quarter,
    Year,
}

pub enum PeriodStatus {
    Open,
    Closed,
    Locked,
}
Closed periods prevent backdated transactions, ensuring financial statement integrity.

Example: Recording a Subscription Payment

When a customer pays for a subscription:
// Create journal entry
let entry = JournalEntry::new(
    legal_entity_id,
    Utc::now().date_naive(),
    "Monthly subscription payment - Customer XYZ",
    vec![
        JournalEntryLine {
            account_number: "1000".to_string(), // Cash
            debit_amount: Decimal::new(9900, 2), // $99.00
            credit_amount: Decimal::ZERO,
            description: "Payment received".to_string(),
        },
        JournalEntryLine {
            account_number: "4000".to_string(), // Subscription Revenue
            debit_amount: Decimal::ZERO,
            credit_amount: Decimal::new(9900, 2), // $99.00
            description: "Monthly subscription".to_string(),
        },
    ],
)?;

// Domain validates balance before posting
entry.validate_balanced()?;
The system automatically validates that debits equal credits, preventing unbalanced entries.

Event Sourcing Integration

Journal entries emit domain events for auditability:
pub enum AccountingEvent {
    JournalEntryCreated {
        entry_id: JournalEntryId,
        legal_entity_id: LegalEntityId,
        total_amount: Decimal,
    },
    JournalEntryPosted {
        entry_id: JournalEntryId,
        posted_at: DateTime<Utc>,
    },
    PeriodClosed {
        period_id: AccountingPeriodId,
        closing_balance: Decimal,
    },
}
Every financial transaction is permanently recorded in the event stream, providing complete audit trails for compliance.

AI Agent Workflow

The accounting foundation required careful planning due to financial domain complexity: Evaluator Phase (Planning):
  • Researched double-entry bookkeeping principles
  • Designed chart of accounts structure for SaaS
  • Mapped business requirements to accounting concepts
  • Planned journal entry validation rules
Builder Phase (Implementation):
  • Generated domain models with proper validation
  • Implemented account balance calculations
  • Created bootstrap logic for standard accounts
  • Built repository layer with DynamoDB patterns
Verifier Phase (Testing):
  • 15+ Level 1 unit tests for balance validation
  • 8 Level 2 integration tests for journal workflows
  • Test scenarios for period closing
  • Multi-entity isolation verification
The AI excelled at:
  • Pattern implementation - Double-entry rules are well-documented
  • Systematic validation - Clear invariants (debits = credits)
  • Repository boilerplate - Standard DynamoDB patterns
  • Test generation - Financial scenarios are formulaic
The AI required guidance on:
  • Account structure - Which accounts for SaaS billing
  • Revenue recognition - When to recognize vs defer revenue
  • Period closing rules - Fiscal calendar decisions

Implementation Scale

Commit: 1430bb7 Scope: 6,862 lines changed across 95 files Core Domain Models:
  • LegalEntity (643 lines)
  • ChartOfAccounts (783 lines)
  • JournalEntry (745 lines)
  • AccountingPeriod (705 lines)
Infrastructure:
  • Repository implementations for all models
  • DynamoDB schema with GSI for queries
  • Event handlers for balance updates
  • Bootstrap logic for default accounts
Testing:
  • 15+ unit tests covering validation rules
  • 8 integration tests for journal workflows
  • Period closing test scenarios
  • Multi-tenant isolation tests

Key Learnings

1. Domain Modeling Matters

Proper accounting abstractions made complex requirements simple:
  • Journal entries enforce balance invariants
  • Account types drive financial statement classification
  • Periods enable time-based reporting
Rather than ad-hoc invoice tables, domain-driven design provided a solid foundation for future billing features.

2. Event Sourcing Fits Accounting

Financial transactions are naturally event-sourced:
  • Every entry is immutable
  • Complete audit trail by default
  • Temporal queries for period reporting
  • Reconciliation through event replay
The event store provides compliance-grade auditability without additional infrastructure.

3. Validation at Domain Boundary

Financial integrity rules belong in the domain:
// This belongs in domain logic, not application layer
impl JournalEntry {
    pub fn validate_balanced(&self) -> Result<(), DomainError> {
        // Balance validation
    }
}
Making invalid states unrepresentable prevents financial data corruption.

4. Bootstrap for Consistency

Default chart of accounts ensures consistency:
  • Every legal entity starts with standard accounts
  • Account numbers are predictable
  • Financial reports work immediately
  • Customization happens through extension
The 18-account bootstrap handles 80% of SaaS billing needs.

5. AI Handles Systematic Implementation

The AI excelled at implementing well-defined patterns:
  • Repository boilerplate
  • Balance calculation logic
  • Test scenario generation
  • Validation rule encoding
Human guidance focused on business rules and account structure decisions.

Results

Development Speed:
  • Full accounting foundation in single commit
  • 95 files with consistent patterns
  • Comprehensive test coverage
  • Estimated 5-7x faster than manual implementation
Financial Integrity:
  • Zero unbalanced entries possible
  • Complete audit trail through events
  • Period closing prevents backdating
  • Multi-tenant isolation enforced
Extensibility:
  • Foundation supports revenue recognition
  • Ready for tax calculation integration
  • Multi-currency expansion path
  • Financial reporting infrastructure
Compliance Ready:
  • GAAP-compatible structure
  • Audit trail for all transactions
  • Period-based reporting
  • Multi-jurisdiction support

Conclusion

Building a billing system on proper accounting foundations pays dividends: Correctness: Double-entry bookkeeping catches errors through balance validation. Auditability: Event sourcing provides compliance-grade audit trails. Extensibility: Domain models support complex future requirements like revenue recognition and tax calculation. Maintainability: Clear domain abstractions make financial logic understandable. The investment in accounting infrastructure—rather than quick invoice tables—creates a foundation for sophisticated billing features while maintaining financial integrity. AI agents excel at implementing systematic patterns like repository boilerplate and validation logic, while human expertise drives domain modeling decisions and business rule definition. When building financial systems, start with proper accounting abstractions. The upfront investment in domain modeling creates a stable foundation that scales with business complexity.