Skip to content

ADR 001: Flexible Nutrient Schema

Metadata

  • Status: Accepted & Implemented
  • Date: 2026-01-06
  • Type: Architecture Decision Record (ADR)
  • Driver: Advanced Nutrient Tracking (Fiber, Sodium)

Summary

This document outlines the architectural decision to support user-defined extended nutrients (Fiber, Sodium, etc.) using a Metadata-Driven DynamoDB Map strategy. It balances schema flexibility with data integrity, performance, and cost efficiency.

1. Problem Statement

Current Limitation

The current schema hardcodes calories, protein, carbs, and fat as top-level attributes. Adding new nutrients requires:

  1. Schema migrations (DynamoDB & GraphQL).
  2. Updates to aggregation logic (addMeal resolver).
  3. Frontend type changes.

Goal: Support arbitrary nutrients (e.g., Fiber, Sodium, Sugar) without schema changes.

2. Solution Options

Add fiber, sodium as explicit columns.

  • ✅ Pros: Simple query/indexing.
  • ❌ Cons: Rigid. Requires migration for every new nutrient.

Store nutrients: String (JSON).

  • ✅ Pros: Maximum flexibility.
  • ❌ Cons: No Server-Side Aggregation. Client must fetch all meals to calc totals. Breaks "Day Summary" architecture.

Store nutrients: Map<String, Number>.

  • ✅ Pros:
    • Flexible keys.
    • Supports Atomic Aggregation via SET path = path + val.
  • ✅ Verdict: Best balance of flexibility and performance.

3. The Metadata-Driven Strategy

To solve the "Black Box" problem of dynamic keys (where AWSJSON offers no type safety), we will use the CONFIG#TARGETS item as a Schema Registry.

User Settings Schema (CONFIG#TARGETS)

We will extend the existing Configuration item to store a definition for every nutrient the user wants to track. We use a unified Registry Map to preserve metadata (units) even for archived items.

{
  "nutrientRegistry": {
    "fiber": {
      "unit": "g",
      "label": "Fiber",
      "target": 30,
      "status": "ACTIVE"
    },
    "sodium": {
      "unit": "mg",
      "label": "Sodium",
      "target": 2300,
      "status": "ACTIVE"
    },
    "sugar": {
      "unit": "g",
      "label": "Sugar",
      "target": null,
      "status": "ARCHIVED"
    }
  },
  "version": 6
}

Schema Improvements

  • Unit Persistence: ARCHIVED items retain their unit definitions, ensuring safe reactivation without data corruption.
  • Optional Targets: Users can track a nutrient without setting a goal (target can be null).
  • Single Source: No need to sync separate active vs historical lists.

Cost & Performance Justification

This decision is driven by Resource Efficiency:

  1. Minimize Get Requests: Combining targets, registry, and history into one row reduces the total number of DynamoDB GetItem calls.
  2. Stay Within Free Tier: By reducing RCU/WCU consumption, we ensure the application remains cost-effective and stays within AWS Free Tier limits longer.

4. Aggregation & Technical Implementation

To maintain real-time Day Summaries, we update the DaySummary item atomically when adding a meal.

Dynamic Update Logic

The AppSync Resolver (JS) will dynamically construct the UpdateItem expression.

// Input: { fiber: 5, sodium: 200 }
// Output Expression:
SET
nutrients.fiber = if_not_exists(nutrients.fiber,: zero) +: fiberVal,
    nutrients.sodium = if_not_exists(nutrients.sodium,: zero) +: sodiumVal

Performance Optimization: "Read-Before-Write"

To avoid doubling the RCU cost of every addMeal call (fetching the Registry to validate keys), we use Frontend-Assisted Validation.

  • Mechanism: The frontend passes a registrySnapshot (list of active keys derived from nutrientRegistry) along with the addMeal mutation.
  • Security: The AppSync resolver uses this list to filter the extendedNutrients map. While the frontend could be tampered with, it provides a sufficient guard against AI hallucinations without requiring an extra database hit.
  • Gatekeeping: The resolver enforces Reserved Keyword filtering (blocking PK, SK, type) regardless of the snapshot.

Precision Strategy

We will enforce rounding to 2 decimal places in the AppSync Resolver.

  • Reasoning: While frontend formatting hides artifacts, accumulating floating point drift (e.g. 10.1 + 20.2 = 30.299999999999997) in the DaySummary aggregation over thousands of meals can eventually cause issues.
  • Implementation: The resolver performs Math.round(val * 100) / 100 before writing to DynamoDB.

5. Math Integrity (Deletion)

The Math Ghost

If a user logs Sodium, then disables Sodium tracking, and then deletes that old meal, we must still subtract the Sodium from the DaySummary.

Rule: The deleteMeal resolver ignores current settings and subtracts based on the values stored on the meal record itself.

6. Prompt Engineering

We need to inject the user's active nutrient list into the prompt dynamically.

Dynamic Injection:

We will append a specific instruction block to the System Prompt based on the user's activeNutrients:

"You must also estimate the following additional nutrients: Fiber (unit: g), Sodium (unit: mg). Return these inside an extendedNutrients object in the JSON."

Response Format: We will separate core macros from extended nutrients to preserve backward compatibility.

Strict Numeric Return

The LLM must be instructed to return only the number (e.g., 200) for extended nutrients, not the string with units (e.g., "200mg"). The prompt will explicitly state: "CRITICAL: The values must be numbers ONLY. Do not include the unit string."

{
  "mealSummary": "...",
  "calories": 500,
  "macros": {
    "protein": 30,
    "carbs": 50,
    "fat": 10
  },
  "extendedNutrients": {
    "fiber": 5,
    "sodium": 200
  }
}

7. Lifecycle & Data Integrity

Rule: Once a nutrient is created in the Registry, its Unit is permanent.

  • Change Path: If a user wants to switch from mg to g, they must disable the old nutrient and create a new one (e.g., "Sodium_G").
  • Reasoning: Prevents "mathematical radioactivity" where a single Daily Total contains mixed units.

Rule: The CONFIG#TARGETS item will use a version attribute for updates.

  • Mechanism: Every update must include the expectedVersion.
  • Conflict: If two devices update settings simultaneously, the second one will fail. The user must refresh their settings state and re-apply their change.
  • Result: Prevents the "God Row" from being corrupted by stale device states.

Constraint: This data is Report Only.

  • Limit: Dynamic nutrients cannot be indexed or searched (e.g., "Find all meals > 500mg Sodium") without a full table scan.
  • Verdict: Accepted tradeoff for schema flexibility and cost efficiency.

8. Proposed Schema Changes

type Meal {
  # ... existing fields
  extendedNutrients: AWSJSON 
  # Serialized JSON string: "{\"fiber\": 5}"
}

type DaySummary {
  # ... existing fields
  extendedNutrients: AWSJSON
}

9. Decision Record

Final Decision

Proceed with Metadata-Driven Maps + Frontend-Assisted Validation + Optimistic Locking.

  • Storage: Extended CONFIG#TARGETS item with versioning and unified nutrientRegistry.
  • API: registrySnapshot passed in addMeal to avoid extra RCU.
  • Logic: Unit locking enforced at the UI/Registry level.