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:
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 |
|---|---|---|
|
string |
User’s unique identifier |
|
boolean |
Whether user can take floats at all |
|
boolean |
Whether user can take loans at all |
|
boolean |
Whether Continuous Float Increase is enabled |
|
array |
Array of FloatSetting (id, amount, is_enabled) |
|
array |
Array of LoanSetting (id, amount_cents, is_enabled) |
|
string |
RFC3339 timestamp of profile creation |
|
string |
Reason for profile creation or update |
|
string |
Administrative notes |
Query Patterns:
-
Get Latest Profile
-
Query:
PK = USER#<user_id>,SK begins_with PROFILE# -
Sort: Descending (newest first)
-
Limit: 1
-
-
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"
}
See also: Float Profiles Documentation
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 |
|---|---|---|
|
string |
User’s unique identifier |
|
string |
RFC3339 timestamp when profile becomes invalid |
|
string |
RFC3339 timestamp of creation |
|
string |
Reason for temporary override (e.g., "promotional_campaign_Q1") |
|
boolean |
Temporary float eligibility override |
|
boolean |
Temporary loan eligibility override |
|
array |
Temporary float settings (overrides regular profile) |
|
array |
Temporary loan settings |
Query Patterns:
-
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
-
-
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 |
|---|---|---|
|
string |
User being evaluated |
|
string |
Name of the rule (e.g., "RuleAgeOfAccount") |
|
int |
Result: -1 = approved, 0 = denied, >0 = approved for specific amount |
|
boolean |
Whether rule execution encountered an error |
|
map |
Input data and intermediate calculations used by rule |
|
string |
RFC3339 timestamp of last update |
|
int64 |
Unix timestamp for automatic deletion (32 days from creation) |
Query Patterns:
-
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
-
-
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 |
|---|---|---|
|
string |
Unique evaluation ID (timestamp_uuid format) |
|
string |
User being evaluated |
|
string |
Plaid item ID |
|
string |
Plaid account ID |
|
Evaluation |
Float decision (approved, amount, deciding rulebook) |
|
Evaluation |
Loan decision |
|
CFIData |
Snapshot of CFI metrics (ranks, balance, etc.) |
|
string |
RFC3339 timestamp |
|
int64 |
Unix timestamp for expiration (32 days) |
Nested: Evaluation Object:
| Field | Type | Description |
|---|---|---|
|
boolean |
Whether user is approved |
|
int |
Approved amount in cents |
|
string |
Rulebook that made the approval decision |
|
string |
Evaluation status (OK, NOEVAL, CALCERR, EVALERR) |
|
string |
Error messages if any |
|
[]RulebookResult |
Array of individual rulebook evaluations |
|
string |
References parent result_id |
Nested: RulebookResult Object:
| Field | Type | Description |
|---|---|---|
|
string |
Rulebook identifier |
|
int |
-1 = approved, 0 = denied, >0 = specific amount |
|
string |
Calculation validity (OK, NODATA, CALCERR) |
|
int |
A/B test percentage (10000 = 100%) |
|
int |
Rulebook priority |
|
map |
Aggregated features from rules in this rulebook |
|
boolean |
Whether this rulebook approved |
|
*boolean |
Whether rulebook applies to this user (A/B test) |
Query Patterns:
-
Get Latest Result for User/Account
-
Query:
PK = USER#<user_id>,SK begins_with EVAL_RESULTS#<item>#<account> -
Sort: Descending
-
Limit: 1
-
-
Get Result by ID
-
Query GSI1:
GSI1PK = USER#<user_id>,GSI1SK = EVAL_RESULTS#<result_id>
-
-
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 |
|---|---|---|
|
string |
Original evaluation result ID |
|
string |
User who took float/loan |
|
string |
Plaid item ID |
|
string |
Plaid account ID |
|
int |
Amount taken in cents |
|
string |
ID of float created (if applicable) |
|
string |
ID of loan created (if applicable) |
|
Evaluation |
Snapshot of float evaluation |
|
Evaluation |
Snapshot of loan evaluation |
|
CFIData |
CFI state at time of decision |
|
string |
RFC3339 timestamp |
Query Patterns:
-
Get by Result ID
-
GetItem:
PK = USER#<user_id>,SK = HISTORICAL_EVALUATION#<item>#<account>#<date>
-
-
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 |
|---|---|---|
|
string |
Unique identifier (e.g., "core_v2", "stringent_approval") |
|
string |
Human-readable name |
|
string |
Type: "floats" or "loan" |
|
[]Rule |
Array of rule configurations |
|
int |
Percentage of users (0-10000, where 10000 = 100%) |
|
int |
Evaluation order (higher = first, typically 0-200) |
|
boolean |
If true, stops evaluation when matched |
|
string |
RFC3339 timestamp of last modification |
|
*string |
Optional A/B test group identifier |
Nested: Rule Object:
| Field | Type | Description |
|---|---|---|
|
string |
Rule name (e.g., "RuleAgeOfAccount") |
|
string |
Lambda ARN for rule execution (if external) |
|
map |
Custom properties for rule configuration |
|
string |
When to run: "txns", "insights", or "all" |
|
*string |
Optional A/B test group for this specific rule |
Query Patterns:
-
Get All Rulebooks
-
Query:
PK = RULEBOOK -
Returns: All rulebooks
-
-
Get Rulebooks by Type
-
Query GSI1:
GSI1PK = RULEBOOK_TYPE#<type> -
Example:
RULEBOOK_TYPE#floatsgets all float rulebooks
-
-
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"
}
]
}
See also: Rulebooks Documentation
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 |
|---|---|---|
|
string |
Username/identifier of person who made the update |
|
string |
RFC3339 timestamp of update |
|
[]Rulebook |
Complete snapshot of all rulebooks at this point in time |
Query Patterns:
-
Get All Updates (Audit Trail)
-
Query:
PK = RULEBOOKCONFIGUPDATE -
Sort: By SK (timestamp order)
-
Returns: Complete history of rulebook changes
-
-
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 |
|
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
-
Float Profiles - FloatProfile entity details
-
Continuous Float Increase - CFI state in evaluations
-
Rule Engine - How entities are populated
-
Rulebooks - Rulebook configuration