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

ESOPTrust

Central accounting hub for all plan assets and liabilities

TrustCashLedger

Non-fungible cash accounting by source

ESOPLoan

Self-contained loan with dedicated suspense shares

Participant

Individual participant account and demographics

PlanRules

Legal framework and compliance rules

OperatingAssumptions

Annual strategy and financial assumptions

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

ESOPTrust

The central accounting hub

TrustCashLedger

Non-fungible cash tracking

ESOPLoan

Loan-specific suspense shares

PlanRules

Legal framework schema

OperatingAssumptions

Strategy configuration

API Schemas

Full API schema reference