Skip to main content

Model Philosophy

At Village Labs, we built our Repurchase Engine on a set of high-fidelity data models that we designed to accurately reflect ESOP accounting and legal structures. We believe that models should not be mere data containers; they must embody the complex relationships and rules that govern ESOP operations.
Our Design Goal: To create models that map 1:1 to real-world ESOP concepts, making the system intuitive for practitioners while maintaining the technical precision we demand.

Core Models

Model Hierarchy

Key Modeling Concepts

1. Non-Fungible Cash

Problem: In traditional accounting, all cash is fungible (interchangeable). But ESOP trust cash has source-based restrictions on use. Solution: The TrustCashLedger segregates cash by source:
class TrustCashLedger:
    participant_cash_accounts: Decimal        # Can only be used for participant distributions
    unallocated_company_contributions: Decimal  # Flexible use per plan rules
    unallocated_forfeiture_cash: Decimal      # Restricted use (typically contributions or reallocations)
Real-World Analog: Think of it like a restaurant where tips (participant cash) can only go to employees, while owner contributions can be used flexibly.

2. Loan-Owned Suspense Shares

Problem: Multiple ESOP loans each collateralized by specific shares. Shares from Loan A shouldn’t be released when paying Loan B. Solution: Each ESOPLoan directly owns its suspense shares:
class ESOPLoan:
    loan_id: str
    principal_balance: Decimal
    interest_rate: Decimal
    suspense_shares: Decimal  # ← Directly owned by THIS loan
Critical: This prevents cross-contamination and ensures ERISA compliance.
Problem: Mixing unchanging legal requirements with variable business decisions leads to configuration errors. Solution: Two distinct input models:

Data Model Principles

All models use strong typing with validation:
from pydantic import BaseModel, Field, validator

class ESOPLoan(BaseModel):
    principal_balance: Decimal = Field(ge=0)
    interest_rate: Decimal = Field(ge=0, le=1)
    suspense_shares: Decimal = Field(ge=0)
    
    @validator('interest_rate')
    def reasonable_interest_rate(cls, v):
        if v > 0.20:  # 20%
            raise ValueError('Interest rate seems unreasonably high')
        return v
Historical snapshots are immutable; current state is mutable during processing:
@dataclass(frozen=True)  # Immutable
class AnnualTrustSnapshot:
    year: int
    cash_balance: Decimal
    allocated_shares: Decimal

@dataclass  # Mutable during processing
class TrustCashLedger:
    participant_cash: Decimal
    unallocated_contributions: Decimal
Models include descriptions and constraints:
class VestingSchedule(BaseModel):
    """
    Defines how participants earn ownership of their ESOP accounts.
    
    Common schedules:
    - Graded: Gradual vesting over 2-6 years
    - Cliff: All-or-nothing after 3 years
    """
    type: Literal['graded', 'cliff']
    years_to_full_vesting: int = Field(
        ge=2, le=7,
        description="Years until 100% vested (ERISA limits: 2-7)"
    )
Models enforce referential integrity:
class ESOPTrust:
    loans: List[ESOPLoan]
    
    def total_suspense_shares(self) -> Decimal:
        """Sum suspense shares across all loans."""
        return sum(loan.suspense_shares for loan in self.loans)
    
    def validate_share_conservation(self):
        """Ensure total shares equal allocated + suspense + unallocated."""
        total = (
            self.allocated_shares + 
            self.total_suspense_shares() + 
            self.unallocated_shares
        )
        assert total == self.total_shares_outstanding

Model Lifecycle

Models flow through distinct lifecycle stages:
1

Configuration

User provides input data (PlanRules, OperatingAssumptions, InitialState)
2

Validation

Models are validated for completeness, consistency, and legal compliance
3

Processing

Engine manipulates mutable models during annual simulation cycle
4

Snapshot

End-of-year state captured as immutable snapshot
5

Persistence

Snapshot saved to database with full audit trail

Common Patterns

Composition Over Inheritance

Models favor composition for flexibility:
class ESOPTrust:
    cash_ledger: TrustCashLedger      # ← Composed
    loans: List[ESOPLoan]              # ← Composed
    share_pool: SharePool              # ← Composed
    
    # Not inheritance:
    # class ESOPTrust(CashLedger, LoanContainer, ShareManager)

Builder Pattern for Complexity

Complex models use builders:
trust = (
    ESOPTrustBuilder()
    .with_cash_ledger(initial_cash=100_000)
    .add_loan(
        principal=2_000_000,
        rate=0.065,
        term_years=10,
        suspense_shares=20_000
    )
    .with_allocated_shares(30_000)
    .build()
)

Factory Methods for Common Scenarios

# Standard graded vesting
vesting = VestingSchedule.standard_graded()

# Quick cliff vesting
vesting = VestingSchedule.cliff(years=3)

# Custom
vesting = VestingSchedule(
    type='graded',
    schedule=[0, 0, 20, 40, 60, 80, 100]
)

Serialization & Deserialization

All models support JSON serialization:
# To JSON
trust_json = trust.model_dump_json()

# From JSON
trust = ESOPTrust.model_validate_json(trust_json)

# To database
db.save(trust.model_dump())

# From database
trust = ESOPTrust.model_validate(db.load(trust_id))

Model Documentation

Each model includes comprehensive documentation:
  • Field descriptions: What each field represents
  • Constraints: Valid ranges and rules
  • Examples: Common use cases
  • Related models: How models connect
  • Legal context: ERISA/IRS requirements

Next Steps