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
- Use strict mode in development to catch missing batch contexts early
- Use default mode in production for maximum deployment flexibility
- Always wrap request handlers with
runWithBatchContextfor optimal performance - Monitor batch context usage in production to identify optimization opportunities
- Use environment variables to configure behavior per environment
- 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
| Operation | Capacity Type | Notes |
|---|---|---|
find() | Read | Both direct and batched finds |
batchFind() | Read | Total capacity for the batch |
queryByIndex | Read | Including count-only queries |
create() | Write | Includes unique constraint writes |
update() | Read + Write | Read for existing item check |
delete() | Write | Includes unique constraint cleanup |
save() | Read + Write | Depends 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
- Call at the end of requests: Get capacity just before sending the response
- Use for billing/metering: Aggregate capacity by tenant, endpoint, or user
- Monitor in production: Log capacity to CloudWatch or your monitoring system
- Set up alerts: Detect unexpected capacity spikes early
- Combine with per-instance tracking: Use request-scoped for billing, per-instance for debugging