DynamoDB Data Models

The Underwriting API uses Amazon DynamoDB as its primary data store following a Single Table Design pattern. All entity types share one table, using generic PK/SK patterns for efficient querying by user, time, or entity type.

Table Overview

Table Name: {environment}-underwriting (e.g., prod-underwriting, test-underwriting)

Primary Keys: * Partition Key (PK): String - Groups related items together * Sort Key (SK): String - Enables range queries and versioning

Global Secondary Indexes: * GSI1: GSI1PK / GSI1SK - Alternative access patterns (e.g., query by result ID) * GSI2: GSI2PK / GSI2SK - Future use (currently unused)

Item Type Field: * item_type - Descriptive type for filtering and debugging

Time-to-Live (TTL): * ttl attribute - Unix timestamp for automatic item expiration

Single Table Design Pattern

All entities are stored in one table using composite keys. This provides:

  • Hot partition management - User data spreads across many partitions

  • Transactional integrity - Related items in same partition can use transactions

  • Cost efficiency - One table to provision and manage

  • Query flexibility - Rich access patterns via GSIs

Entity Relationship Diagram

┌─────────────────────────────────────────────────────────────┐
│                     USER PARTITION                          │
│  PK: USER#<user_id>                                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ PROFILE#<timestamp>                          │           │
│  │ • FloatProfile (versioned)                   │           │
│  │ • Tracks float/loan settings over time       │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ TEMP_FLOAT_PROFILE#EXPIRES#<timestamp>       │           │
│  │ • TemporaryFloatProfile                      │           │
│  │ • Override profiles with expiration          │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ RULE_OUTCOME#<rule_name>                     │           │
│  │ • RuleOutcome (per rule)                     │           │
│  │ • Stores individual rule evaluation results  │           │
│  │ • TTL: Auto-expire after 32 days             │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ EVAL_RESULTS#<item>#<account>#<timestamp>    │           │
│  │ • EvaluationResult                           │           │
│  │ • Aggregated decision from all rules         │           │
│  │ • TTL: Auto-expire after 32 days             │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ HISTORICAL_EVALUATION#<item>#<acct>#<time>   │           │
│  │ • HistoricalEvaluation                       │           │
│  │ • Permanent record when float/loan taken     │           │
│  │ • No TTL (kept forever)                      │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                  RULEBOOK PARTITION                         │
│  PK: RULEBOOK                                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ RULEBOOK#<rulebook_id>                       │           │
│  │ • Rulebook (configuration)                   │           │
│  │ • Defines rule sets and evaluation logic     │           │
│  │ • GSI1: Query by RULEBOOK_TYPE               │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│               RULEBOOK UPDATE PARTITION                     │
│  PK: RULEBOOKCONFIGUPDATE                                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────────────────────────────────┐           │
│  │ USER#<timestamp>#<user>                      │           │
│  │ • Tracks rulebook changes over time          │           │
│  └──────────────────────────────────────────────┘           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Visual Entity Model:

DynamoDB Entity Relationships

The diagram above shows the relationships between entities, including:

  • User-scoped entities - All entities with PK: USER#<user_id> are co-located

  • Evaluation flow - RuleOutcomes aggregate into EvaluationResults

  • Historical tracking - EvaluationResults create HistoricalEvaluations when floats are taken

  • TTL indicators - Red/orange for temporary data, green for permanent records

  • GSI1 usage - Alternative access pattern for result ID lookups

Entity Types

FloatProfile

Stores user-specific settings for float and loan products. Profiles are versioned by creation timestamp, allowing full history tracking. Each update creates a new profile entry.

Access Pattern:

PK: USER#<user_id>
SK: PROFILE#<created_on>

Fields:

Field Type Description

user_id

string

User’s unique identifier

is_float_enabled

boolean

Whether user can take floats at all

is_loan_enabled

boolean

Whether user can take loans at all

cfi_enabled

boolean

Whether Continuous Float Increase is enabled

floats

array

Array of FloatSetting (id, amount, is_enabled)

loans

array

Array of LoanSetting (id, amount_cents, is_enabled)

created_on

string

RFC3339 timestamp of profile creation

reason

string

Reason for profile creation or update

notes

string

Administrative notes

Query Patterns:

  1. Get Latest Profile

    • Query: PK = USER#<user_id>, SK begins_with PROFILE#

    • Sort: Descending (newest first)

    • Limit: 1

  2. Get Profile History

    • Query: PK = USER#<user_id>, SK begins_with PROFILE#

    • Returns: All profiles ordered by creation date

Example Item:

{
  "PK": "USER#user-12345",
  "SK": "PROFILE#2024-02-10T14:30:00Z",
  "item_type": "float_profile",
  "user_id": "user-12345",
  "is_float_enabled": true,
  "is_loan_enabled": true,
  "cfi_enabled": true,
  "floats": [
    {"id": "1", "amount": 1000, "is_enabled": true},
    {"id": "2", "amount": 2000, "is_enabled": true},
    {"id": "3", "amount": 3000, "is_enabled": false}
  ],
  "loans": [
    {"id": "1", "amount_cents": 20000, "is_enabled": true}
  ],
  "created_on": "2024-02-10T14:30:00Z",
  "reason": "CFI limit increase to $30",
  "notes": "User reached sub rank 2, float rank 3"
}

TemporaryFloatProfile

Temporary overrides for float profiles with automatic expiration. Used for promotions, A/B tests, or temporary limit increases. Takes precedence over regular FloatProfile while unexpired.

Access Pattern:

PK: USER#<user_id>
SK: TEMP_FLOAT_PROFILE#EXPIRES#<expires_on>

Fields:

Field Type Description

user_id

string

User’s unique identifier

expires_on

string

RFC3339 timestamp when profile becomes invalid

created_on

string

RFC3339 timestamp of creation

reason

string

Reason for temporary override (e.g., "promotional_campaign_Q1")

is_float_enabled

boolean

Temporary float eligibility override

is_loan_enabled

boolean

Temporary loan eligibility override

floats

array

Temporary float settings (overrides regular profile)

loans

array

Temporary loan settings

Query Patterns:

  1. Get Active Temporary Profiles

    • Query: PK = USER#<user_id>, SK > TEMP_FLOAT_PROFILE#EXPIRES#<current_time>

    • Returns: All unexpired profiles

    • Note: Should typically return 0 or 1 items

  2. Get All Temporary Profiles

    • Query: PK = USER#<user_id>, SK begins_with TEMP_FLOAT_PROFILE#

    • Returns: All temporary profiles (expired and active)

TTL Strategy: * DynamoDB TTL set to expires_on timestamp * Automatic deletion shortly after expiration * No manual cleanup required

Example Item:

{
  "PK": "USER#user-12345",
  "SK": "TEMP_FLOAT_PROFILE#EXPIRES#2024-02-24T00:00:00Z",
  "item_type": "temp_float_profile",
  "user_id": "user-12345",
  "created_on": "2024-02-10T14:30:00Z",
  "expires_on": "2024-02-24T00:00:00Z",
  "reason": "promotional_campaign_presidents_day",
  "is_float_enabled": true,
  "floats": [
    {"id": "1", "amount": 1000, "is_enabled": true},
    {"id": "2", "amount": 2000, "is_enabled": true},
    {"id": "3", "amount": 3000, "is_enabled": true},
    {"id": "4", "amount": 4000, "is_enabled": true}
  ]
}

RuleOutcome

Stores the result of individual rule execution. Each rule in a rulebook produces one RuleOutcome. These are transient records used during evaluation aggregation.

Access Pattern:

PK: USER#<user_id>
SK: RULE_OUTCOME#<rule_name>

Fields:

Field Type Description

user_id

string

User being evaluated

rule_name

string

Name of the rule (e.g., "RuleAgeOfAccount")

loan

int

Result: -1 = approved, 0 = denied, >0 = approved for specific amount

error

boolean

Whether rule execution encountered an error

features

map

Input data and intermediate calculations used by rule

updated_date

string

RFC3339 timestamp of last update

ttl

int64

Unix timestamp for automatic deletion (32 days from creation)

Query Patterns:

  1. Get All Outcomes for User

    • Query: PK = USER#<user_id>, SK begins_with RULE_OUTCOME#

    • Filter: ttl > current_time (exclude expired)

    • Returns: Map of rule_name → RuleOutcome

  2. Get Specific Rule Outcome

    • GetItem: PK = USER#<user_id>, SK = RULE_OUTCOME#<rule_name>

TTL Strategy: * Set to 32 days from creation * Ensures stale outcomes don’t affect new evaluations * Automatic DynamoDB cleanup

Example Item:

{
  "PK": "USER#user-12345",
  "SK": "RULE_OUTCOME#RuleAgeOfAccount",
  "item_type": "rule_outcome",
  "user_id": "user-12345",
  "rule_name": "RuleAgeOfAccount",
  "loan": -1,
  "error": false,
  "features": {
    "account_age_days": 180,
    "min_required_days": 90,
    "passed": true
  },
  "updated_date": "2024-02-10T14:30:00Z",
  "ttl": 1710288000
}

EvaluationResult

Aggregated final decision from all rule evaluations. Represents the complete answer to a float-check or loan-check request. Includes CFI state snapshot at time of evaluation.

Access Pattern:

PK: USER#<user_id>
SK: EVAL_RESULTS#<item_id>#<account_id>#<created_date>

GSI1PK: USER#<user_id>
GSI1SK: EVAL_RESULTS#<result_id>

Fields:

Field Type Description

result_id

string

Unique evaluation ID (timestamp_uuid format)

user_id

string

User being evaluated

item_id

string

Plaid item ID

account_id

string

Plaid account ID

float_results

Evaluation

Float decision (approved, amount, deciding rulebook)

loan_results

Evaluation

Loan decision

cfi_state

CFIData

Snapshot of CFI metrics (ranks, balance, etc.)

created_date

string

RFC3339 timestamp

ttl

int64

Unix timestamp for expiration (32 days)

Nested: Evaluation Object:

Field Type Description

approved

boolean

Whether user is approved

amount

int

Approved amount in cents

deciding_rulebook

string

Rulebook that made the approval decision

status

string

Evaluation status (OK, NOEVAL, CALCERR, EVALERR)

errors

string

Error messages if any

results

[]RulebookResult

Array of individual rulebook evaluations

result_id

string

References parent result_id

Nested: RulebookResult Object:

Field Type Description

rulebook_id

string

Rulebook identifier

result

int

-1 = approved, 0 = denied, >0 = specific amount

calc_status

string

Calculation validity (OK, NODATA, CALCERR)

apply_to

int

A/B test percentage (10000 = 100%)

priority

int

Rulebook priority

features

map

Aggregated features from rules in this rulebook

approved

boolean

Whether this rulebook approved

is_applicable

*boolean

Whether rulebook applies to this user (A/B test)

Query Patterns:

  1. Get Latest Result for User/Account

    • Query: PK = USER#<user_id>, SK begins_with EVAL_RESULTS#<item>#<account>

    • Sort: Descending

    • Limit: 1

  2. Get Result by ID

    • Query GSI1: GSI1PK = USER#<user_id>, GSI1SK = EVAL_RESULTS#<result_id>

  3. Get Recent Results

    • Query: PK = USER#<user_id>, SK begins_with EVAL_RESULTS#

    • Filter: ttl > current_time

    • Limit: N

TTL Strategy: * Set to 32 days from creation * Balances storage costs with audit/debugging needs * Historical evaluations (when float taken) saved separately without TTL

Example Item:

{
  "PK": "USER#user-12345",
  "SK": "EVAL_RESULTS#item-abc#account-xyz#2024-02-10T14:30:00Z",
  "GSI1PK": "USER#user-12345",
  "GSI1SK": "EVAL_RESULTS#1707574200_550e8400-e29b-41d4",
  "item_type": "evaluation_result",
  "result_id": "1707574200_550e8400-e29b-41d4",
  "user_id": "user-12345",
  "item_id": "item-abc",
  "account_id": "account-xyz",
  "float_results": {
    "approved": true,
    "amount": 3000,
    "deciding_rulebook": "core_v2",
    "status": "OK",
    "errors": "",
    "results": [
      {
        "rulebook_id": "core_v2",
        "result": -1,
        "calc_status": "OK",
        "apply_to": 10000,
        "priority": 100,
        "approved": true,
        "is_applicable": true,
        "features": {
          "RuleAgeOfAccount": {"account_age_days": 180}
        }
      }
    ],
    "result_id": "1707574200_550e8400-e29b-41d4"
  },
  "loan_results": {
    "approved": false,
    "amount": 0,
    "status": "OK"
  },
  "cfi_state": {
    "current_limit": 3000,
    "sub_rank": 2,
    "float_rank": 3,
    "account_balance": 120000,
    "highest_float": 2000
  },
  "created_date": "2024-02-10T14:30:00Z",
  "ttl": 1710288000
}

HistoricalEvaluation

Permanent record of evaluations that resulted in a float or loan being taken. Created when the Float Service notifies that a float was created. No TTL - kept indefinitely for compliance and analysis.

Access Pattern:

PK: USER#<user_id>
SK: HISTORICAL_EVALUATION#<item_id>#<account_id>#<created_date>

Fields:

Field Type Description

result_id

string

Original evaluation result ID

user_id

string

User who took float/loan

item_id

string

Plaid item ID

account_id

string

Plaid account ID

amount

int

Amount taken in cents

float_id

string

ID of float created (if applicable)

loan_id

string

ID of loan created (if applicable)

float_results

Evaluation

Snapshot of float evaluation

loan_results

Evaluation

Snapshot of loan evaluation

cfi_state

CFIData

CFI state at time of decision

created_date

string

RFC3339 timestamp

Query Patterns:

  1. Get by Result ID

    • GetItem: PK = USER#<user_id>, SK = HISTORICAL_EVALUATION#<item>#<account>#<date>

  2. Get All Historical Evaluations for User

    • Query: PK = USER#<user_id>, SK begins_with HISTORICAL_EVALUATION#

    • Returns: All floats/loans user has taken

No TTL: * Permanent records for compliance * Used for float history analysis * Supports CFI calculations (float rank, highest float)

Example Item:

{
  "PK": "USER#user-12345",
  "SK": "HISTORICAL_EVALUATION#item-abc#account-xyz#2024-02-10T14:30:00Z",
  "item_type": "historical_evaluation",
  "result_id": "1707574200_550e8400-e29b-41d4",
  "user_id": "user-12345",
  "item_id": "item-abc",
  "account_id": "account-xyz",
  "amount": 3000,
  "float_id": "float-789",
  "loan_id": "",
  "float_results": {
    "approved": true,
    "amount": 3000,
    "deciding_rulebook": "core_v2",
    "status": "OK"
  },
  "cfi_state": {
    "current_limit": 3000,
    "float_rank": 3,
    "sub_rank": 2
  },
  "created_date": "2024-02-10T14:30:00Z"
}

Note: No ttl field - kept permanently.

Rulebook

Configuration for a set of rules that define eligibility logic. Rulebooks are globally scoped (not per-user) and define evaluation behavior for all users or specific A/B test cohorts.

Access Pattern:

PK: RULEBOOK
SK: RULEBOOK#<rulebook_id>

GSI1PK: RULEBOOK_TYPE#<type>
GSI1SK: RULEBOOK#<rulebook_id>

Fields:

Field Type Description

rulebook_id

string

Unique identifier (e.g., "core_v2", "stringent_approval")

rulebook_name

string

Human-readable name

type

string

Type: "floats" or "loan"

rules

[]Rule

Array of rule configurations

apply_to

int

Percentage of users (0-10000, where 10000 = 100%)

priority

int

Evaluation order (higher = first, typically 0-200)

superseding

boolean

If true, stops evaluation when matched

last_updated

string

RFC3339 timestamp of last modification

ab_group

*string

Optional A/B test group identifier

Nested: Rule Object:

Field Type Description

rule

string

Rule name (e.g., "RuleAgeOfAccount")

rule_arn

string

Lambda ARN for rule execution (if external)

props

map

Custom properties for rule configuration

rule_type

string

When to run: "txns", "insights", or "all"

ab_group

*string

Optional A/B test group for this specific rule

Query Patterns:

  1. Get All Rulebooks

    • Query: PK = RULEBOOK

    • Returns: All rulebooks

  2. Get Rulebooks by Type

    • Query GSI1: GSI1PK = RULEBOOK_TYPE#<type>

    • Example: RULEBOOK_TYPE#floats gets all float rulebooks

  3. Get Specific Rulebook

    • GetItem: PK = RULEBOOK, SK = RULEBOOK#<id>

Caching Strategy: * Rulebooks are cached in-memory by Lambda functions * Cache TTL: 5-10 minutes * Reduces DynamoDB read costs * Cache invalidation on rulebook update events

Example Item:

{
  "PK": "RULEBOOK",
  "SK": "RULEBOOK#core_v2",
  "GSI1PK": "RULEBOOK_TYPE#floats",
  "GSI1SK": "RULEBOOK#core_v2",
  "item_type": "rulebook",
  "rulebook_id": "core_v2",
  "rulebook_name": "Core Float Approval v2",
  "type": "floats",
  "apply_to": 10000,
  "priority": 100,
  "superseding": false,
  "last_updated": "2024-01-15T10:00:00Z",
  "rules": [
    {
      "rule": "RuleGoodStanding",
      "rule_arn": "",
      "props": {},
      "rule_type": "all"
    },
    {
      "rule": "RuleAgeOfAccount",
      "rule_arn": "",
      "props": {
        "min_days": 90
      },
      "rule_type": "all"
    },
    {
      "rule": "RuleRecurringDeposits",
      "rule_arn": "",
      "props": {
        "lookback_days": 60,
        "min_deposits": 2
      },
      "rule_type": "txns"
    }
  ]
}

RulebookUpdate

Audit trail for rulebook configuration changes. Every time rulebooks are updated (via Admin API or scripts), a RulebookUpdate record is created capturing who made the change and the complete state of all rulebooks.

Access Pattern:

PK: RULEBOOKCONFIGUPDATE
SK: USER#<timestamp>#<update_user>

Fields:

Field Type Description

update_user

string

Username/identifier of person who made the update

update_time

string

RFC3339 timestamp of update

rulebooks

[]Rulebook

Complete snapshot of all rulebooks at this point in time

Query Patterns:

  1. Get All Updates (Audit Trail)

    • Query: PK = RULEBOOKCONFIGUPDATE

    • Sort: By SK (timestamp order)

    • Returns: Complete history of rulebook changes

  2. Get Recent Updates

    • Query: PK = RULEBOOKCONFIGUPDATE

    • Sort: Descending

    • Limit: N

Use Cases: * Compliance audit trail * Debugging: "What rulebooks were active on date X?" * Rollback: Restore previous rulebook configuration * Analysis: Correlation between rulebook changes and approval rates

Example Item:

{
  "PK": "RULEBOOKCONFIGUPDATE",
  "SK": "USER#2024-02-10T14:30:00Z#admin-jane",
  "item_type": "rulebook_update",
  "update_user": "admin-jane",
  "update_time": "2024-02-10T14:30:00Z",
  "rulebooks": [
    {
      "rulebook_id": "core_v2",
      "rulebook_name": "Core Float Approval v2",
      "type": "floats",
      "priority": 100,
      "rules": [...]
    },
    {
      "rulebook_id": "stringent_v1",
      "rulebook_name": "Stringent Approval",
      "type": "floats",
      "priority": 90,
      "rules": [...]
    }
  ]
}

Access Patterns Summary

Use Case Index Keys

Get user’s latest float profile

Primary

PK=USER#<id>, SK begins_with PROFILE#, descending, limit 1

Get active temporary profile

Primary

PK=USER#<id>, SK > TEMP_FLOAT_PROFILE#EXPIRES#<now>

Get all rule outcomes for evaluation

Primary

PK=USER#<id>, SK begins_with RULE_OUTCOME#

Get latest evaluation result

Primary

PK=USER#<id>, SK begins_with EVAL_RESULTS#<item>#<acct>, desc, limit 1

Get evaluation by result ID

GSI1

GSI1PK=USER#<id>, GSI1SK=EVAL_RESULTS#<result_id>

Get historical evaluation

Primary

GetItem: PK=USER#<id>, SK=HISTORICAL_EVALUATION#<item>#<acct>#<date>

Get all historical evaluations

Primary

PK=USER#<id>, SK begins_with HISTORICAL_EVALUATION#

Get all rulebooks

Primary

PK=RULEBOOK

Get rulebooks by type

GSI1

GSI1PK=RULEBOOK_TYPE#<type>

Get specific rulebook

Primary

GetItem: PK=RULEBOOK, SK=RULEBOOK#<id>

Get rulebook update history

Primary

PK=RULEBOOKCONFIGUPDATE, ordered by SK

TTL Strategy

Entity TTL Reason

FloatProfile

None

Version history kept for audit

TemporaryFloatProfile

expires_on

Auto-cleanup after promotional period

RuleOutcome

32 days

Prevent stale data in evaluations

EvaluationResult

32 days

Balance debugging needs vs storage costs

HistoricalEvaluation

None

Permanent compliance record

Rulebook

None

Configuration data

RulebookUpdate

None

Audit trail

TTL Processing: * DynamoDB processes TTL deletions within 48 hours of expiration * Items may still be queryable briefly after TTL timestamp * Application code should filter by ttl > current_time when querying

See Also