Batch Context Configuration

Batch Context Configuration

DynamoBao provides flexible batch context behavior that can be configured based on your application's needs. This tutorial covers how to configure and use the batch context system for optimal performance and safety.

Overview

The batch context system in DynamoBao enables:

  • Efficient batching: Multiple database operations are combined into fewer requests
  • Automatic caching: Results are cached within the same request context
  • Request isolation: Ensures data doesn't leak between concurrent requests
  • Flexible enforcement: Configure whether batch context is required or optional

Configuration Options

Add the batchContext configuration to your config file:

// config.js or dynamo-bao.config.js
module.exports = {
  aws: {
    region: "us-west-2",
  },
  db: {
    tableName: "your-table-name",
  },
  batchContext: {
    requireBatchContext: false, // Default: allow fallback behavior
  },
  // ... other config
};

Behavior Modes

Default Mode (requireBatchContext: false)

Best for: Production environments, gradual migration, maximum flexibility

const manager = initModels({
  batchContext: { requireBatchContext: false },
});

// Works with direct execution (no batching/caching)
const user = await User.find("user123");

// Also works with batching + caching
await runWithBatchContext(async () => {
  const user = await User.find("user123");
});

Behavior:

  • Operations inside runWithBatchContext: Full batching and caching enabled
  • Operations outside runWithBatchContext: Direct execution without batching or caching
  • No errors thrown, maximum backward compatibility

Strict Mode (requireBatchContext: true)

Best for: Development, testing, ensuring consistent batch context usage

const manager = initModels({
  batchContext: { requireBatchContext: true },
});

// This throws an error
await User.find("user123");
// Error: Batch operations must be executed within runWithBatchContext()

// This works
await runWithBatchContext(async () => {
  const user = await User.find("user123"); // ✅
});

Behavior:

  • Operations inside runWithBatchContext: Full batching and caching enabled
  • Operations outside runWithBatchContext: Throws an error
  • Ensures all database operations use proper batch context

Context Detection API

You can check if code is currently running within a batch context:

const { User } = require("./models/user");

// Check if inside batch context
const isInBatchContext = User.isInsideBatchContext();

if (isInBatchContext) {
  console.log("Running with batching enabled");
  // Can use advanced batching features
} else {
  console.log("Running in direct execution mode");
  // Operations will be direct database calls
}

Environment Variable Support

Configure batch context behavior via environment variables:

# Enable strict mode globally
export DYNAMO_BAO_REQUIRE_BATCH_CONTEXT=true

# Your application will now require runWithBatchContext for all operations
node your-app.js

This is particularly useful for:

  • Development/Testing: Use strict mode to catch missing batch contexts early
  • Production: Use default mode for maximum flexibility
  • CI/CD: Different environments can have different enforcement levels

Usage Patterns

Web Application with Express.js

const express = require("express");
const { runWithBatchContext, initModels } = require("dynamo-bao");

const app = express();

// Initialize with development-friendly strict mode
const manager = initModels({
  batchContext: {
    requireBatchContext: process.env.NODE_ENV === "development",
  },
});

// Middleware to wrap requests in batch context
app.use((req, res, next) => {
  runWithBatchContext(() => {
    next();
  });
});

// Routes automatically benefit from batching
app.get("/api/users/:id/dashboard", async (req, res) => {
  const { User, Post } = manager.models;

  // These operations will be batched and cached
  const user = await User.find(req.params.id);
  const posts = await user.queryPosts();

  // Load related data efficiently
  await Promise.all(
    posts.items.map((post) => post.loadRelatedData(["userId"])),
  );

  res.json({ user, posts: posts.items });
});

AWS Lambda Functions

const { runWithBatchContext, initModels } = require("dynamo-bao");

// Configure based on environment
const manager = initModels({
  batchContext: {
    requireBatchContext: process.env.STAGE === "dev", // Strict in dev, flexible in prod
  },
});

exports.handler = async (event) => {
  return runWithBatchContext(async () => {
    const { User, Order } = manager.models;

    // Efficient batch operations
    const userId = event.pathParameters.userId;
    const user = await User.find(userId);
    const orders = await user.queryOrders();

    return {
      statusCode: 200,
      body: JSON.stringify({ user, orders: orders.items }),
    };
  });
};

Background Jobs

const { runWithBatchContext } = require("dynamo-bao");

async function processUserEmails() {
  await runWithBatchContext(async () => {
    const { User } = manager.models;

    // Iterate through users efficiently
    for await (const batch of User.iterateAll({ batchSize: 100 })) {
      await Promise.all(
        batch.map(async (user) => {
          if (user.emailPreferences.notifications) {
            await sendNotificationEmail(user);
          }
        }),
      );
    }
  });
}

Migration Strategies

Gradual Migration from Direct Calls

Step 1: Start with default mode (no errors)

// config.js
module.exports = {
  batchContext: { requireBatchContext: false }, // Start permissive
};

Step 2: Wrap critical paths

// Start with high-traffic endpoints
app.get("/api/dashboard", async (req, res) => {
  await runWithBatchContext(async () => {
    // Your existing code here - now with batching!
  });
});

Step 3: Enable detection and monitoring

app.use((req, res, next) => {
  const isInBatch = User.isInsideBatchContext();
  if (!isInBatch) {
    console.warn(`Route ${req.path} not using batch context`);
  }
  next();
});

Step 4: Gradually enable strict mode

// config.js
module.exports = {
  batchContext: {
    requireBatchContext: process.env.NODE_ENV === "development",
  },
};

Performance Considerations

With Batch Context (Recommended)

await runWithBatchContext(async () => {
  // These 10 finds become 1 DynamoDB BatchGet operation
  const users = await Promise.all([
    User.find("user1"),
    User.find("user2"),
    // ... 8 more
  ]);

  // Subsequent finds for same users return cached instances
  const user1Again = await User.find("user1"); // Returns same object
  expect(user1Again).toBe(users[0]); // Same object reference
});

Without Batch Context (Direct Mode)

// These become 10 separate DynamoDB Get operations
const users = await Promise.all([
  User.find("user1"), // Individual DynamoDB call
  User.find("user2"), // Individual DynamoDB call
  // ... 8 more individual calls
]);

// No caching - each find creates a new request
const user1Again = await User.find("user1"); // Another DynamoDB call
expect(user1Again).not.toBe(users[0]); // Different object instances

Best Practices

  1. Use strict mode in development to catch missing batch contexts early
  2. Use default mode in production for maximum deployment flexibility
  3. Always wrap request handlers with runWithBatchContext for optimal performance
  4. Monitor batch context usage in production to identify optimization opportunities
  5. Use environment variables to configure behavior per environment
  6. Gradually migrate existing codebases using the permissive default mode

Error Handling

When requireBatchContext: true, operations outside batch context throw descriptive errors:

try {
  await User.find("user123"); // Outside batch context
} catch (error) {
  console.log(error.message);
  // "Batch operations must be executed within runWithBatchContext().
  //  Wrap your database operations in runWithBatchContext() to enable batching and caching."
}

Integration with Other Features

Multi-Tenancy

Batch context works seamlessly with multi-tenancy:

await TenantContext.runWithTenant("tenant-123", async () => {
  await runWithBatchContext(async () => {
    // Operations are both tenant-scoped AND batched
    const users = await User.queryByIndex("byStatus", "active");
  });
});

Testing

Batch context is perfect for test isolation:

// Each test gets its own isolated batch context
test("user creation", async () => {
  await runWithBatchContext(async () => {
    const user = await User.create({ name: "Test User" });
    expect(user.name).toBe("Test User");
  });
});

Request-Scoped Capacity Tracking

DynamoBao provides getBatchContextCapacity() to retrieve the total DynamoDB consumed capacity (RCUs/WCUs) for all operations within the current runWithBatchContext scope.

Basic Usage

const {
  runWithBatchContext,
  getBatchContextCapacity,
} = require("dynamo-bao");

await runWithBatchContext(async () => {
  // Perform various operations
  const user = await User.find(userId);
  const posts = await Post.queryByIndex("postsForUser", userId);
  await user.save();

  // Get total capacity consumed
  const capacity = getBatchContextCapacity();
  console.log(`Read: ${capacity.read} RCUs, Write: ${capacity.write} WCUs`);
  // Example output: "Read: 1.5 RCUs, Write: 2.0 WCUs"
});

Multi-Tenant Usage Metering

A primary use case is billing tenants for their DynamoDB usage:

const {
  runWithBatchContext,
  getBatchContextCapacity,
  TenantContext,
} = require("dynamo-bao");

// Express middleware for usage tracking
app.use(async (req, res, next) => {
  const tenantId = req.headers["x-tenant-id"];

  await TenantContext.runWithTenant(tenantId, async () => {
    await runWithBatchContext(async () => {
      // Store original end function
      const originalEnd = res.end;

      // Override to capture capacity before response completes
      res.end = function (...args) {
        const capacity = getBatchContextCapacity();

        // Record usage for billing
        usageTracker.record({
          tenantId,
          endpoint: req.path,
          method: req.method,
          readCapacityUnits: capacity.read,
          writeCapacityUnits: capacity.write,
          timestamp: new Date().toISOString(),
        });

        return originalEnd.apply(this, args);
      };

      await next();
    });
  });
});

AWS Lambda with Usage Logging

const {
  runWithBatchContext,
  getBatchContextCapacity,
  initModels,
} = require("dynamo-bao");

exports.handler = async (event) => {
  return runWithBatchContext(async () => {
    const { User, Order } = initModels(config).models;

    // Your business logic
    const userId = event.pathParameters.userId;
    const user = await User.find(userId);
    const orders = await user.queryOrders();

    // Get capacity for logging/monitoring
    const capacity = getBatchContextCapacity();

    // Log for CloudWatch metrics
    console.log(
      JSON.stringify({
        metric: "DynamoDBCapacity",
        userId,
        readCapacityUnits: capacity.read,
        writeCapacityUnits: capacity.write,
      })
    );

    return {
      statusCode: 200,
      body: JSON.stringify({
        user,
        orders: orders.items,
      }),
    };
  });
};

Tracking Capacity for Specific Operations

You can check capacity at any point within a batch context:

await runWithBatchContext(async () => {
  const capacityBefore = getBatchContextCapacity();

  // Expensive operation
  await User.batchFind(userIds); // Load 100 users

  const capacityAfter = getBatchContextCapacity();
  const operationCost = {
    read: capacityAfter.read - capacityBefore.read,
    write: capacityAfter.write - capacityBefore.write,
  };

  console.log(`Batch find consumed ${operationCost.read} RCUs`);
});

What Operations Are Tracked

OperationCapacity TypeNotes
find()ReadBoth direct and batched finds
batchFind()ReadTotal capacity for the batch
queryByIndexReadIncluding count-only queries
create()WriteIncludes unique constraint writes
update()Read + WriteRead for existing item check
delete()WriteIncludes unique constraint cleanup
save()Read + WriteDepends on whether item exists

Behavior Outside Batch Context

When called outside a batch context, getBatchContextCapacity() returns zeros:

// Outside batch context
const capacity = getBatchContextCapacity();
console.log(capacity); // { read: 0, write: 0 }

// Inside batch context
await runWithBatchContext(async () => {
  await User.find("user123");
  const capacity = getBatchContextCapacity();
  console.log(capacity); // { read: 0.5, write: 0 }
});

// After batch context ends, back to zeros
const capacityAfter = getBatchContextCapacity();
console.log(capacityAfter); // { read: 0, write: 0 }

Context Isolation

Each batch context has its own isolated capacity accumulator:

// First request
await runWithBatchContext(async () => {
  await User.create({ name: "User 1", email: "user1@example.com" });
  console.log(getBatchContextCapacity()); // { read: 0, write: 7 }
});

// Second request - starts fresh
await runWithBatchContext(async () => {
  console.log(getBatchContextCapacity()); // { read: 0, write: 0 }
  await User.find("user123");
  console.log(getBatchContextCapacity()); // { read: 0.5, write: 0 }
});

Combining with Per-Instance Capacity

DynamoBao also tracks capacity per model instance via getNumericConsumedCapacity(). Use both for different purposes:

await runWithBatchContext(async () => {
  const user = await User.find(userId);

  // Per-instance: capacity for this specific object's operations
  const instanceCapacity = user.getNumericConsumedCapacity("read");

  // Request-scoped: total capacity for all operations in this request
  const requestCapacity = getBatchContextCapacity();

  console.log(`Instance read: ${instanceCapacity} RCUs`);
  console.log(`Total request: ${requestCapacity.read} RCUs`);
});

Best Practices

  1. Call at the end of requests: Get capacity just before sending the response
  2. Use for billing/metering: Aggregate capacity by tenant, endpoint, or user
  3. Monitor in production: Log capacity to CloudWatch or your monitoring system
  4. Set up alerts: Detect unexpected capacity spikes early
  5. Combine with per-instance tracking: Use request-scoped for billing, per-instance for debugging