Example: Scan Operations
A product and review catalog that demonstrates scan operations — reading all items from a table without targeting a specific partition key. Scans are the broadest read operation in DynamoDB, and effect-dynamodb makes them composable with the same BoundQuery combinators used for index queries.
What you’ll learn:
db.entities.Entity.scan()— full-table scan returning a BoundQuery- BoundQuery combinators:
.filter(),.limit(),.consistentRead()on scans - Terminals:
.collect(),.fetch(),.paginate()for executing scans - Entity-type isolation — scans automatically filter by
__edd_e__so each entity only sees its own items - When to use scan vs query
Step 1: Models
Section titled “Step 1: Models”Two entity types sharing a single table — products and reviews:
class Product extends Schema.Class<Product>("Product")({ productId: Schema.String, name: Schema.NonEmptyString, category: Schema.String, price: Schema.Number, inStock: Schema.Boolean,}) {}
class Review extends Schema.Class<Review>("Review")({ reviewId: Schema.String, productId: Schema.String, rating: Schema.Number, comment: Schema.String,}) {}Step 2: Schema, Entities, and Table
Section titled “Step 2: Schema, Entities, and Table”Both entities share one table with GSI indexes for category/product lookups:
const AppSchema = DynamoSchema.make({ name: "scan-demo", version: 1 })
const Products = Entity.make({ model: Product, entityType: "Product", primaryKey: { pk: { field: "pk", composite: ["productId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byCategory: { name: "gsi1", pk: { field: "gsi1pk", composite: ["category"] }, sk: { field: "gsi1sk", composite: ["productId"] }, }, }, timestamps: true,})
const Reviews = Entity.make({ model: Review, entityType: "Review", primaryKey: { pk: { field: "pk", composite: ["reviewId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byProduct: { name: "gsi1", pk: { field: "gsi1pk", composite: ["productId"] }, sk: { field: "gsi1sk", composite: ["reviewId"] }, }, }, timestamps: true,})
const MainTable = Table.make({ schema: AppSchema, entities: { Products, Reviews } })Note that both entities overload GSI1 — Products use it for category lookups while Reviews use it for product lookups. This is standard single-table design.
Step 3: Seed Data
Section titled “Step 3: Seed Data”yield* db.entities.Products.put({ productId: "p-1", name: "Wireless Mouse", category: "electronics", price: 29.99, inStock: true,})yield* db.entities.Products.put({ productId: "p-2", name: "Mechanical Keyboard", category: "electronics", price: 89.99, inStock: true,})yield* db.entities.Products.put({ productId: "p-3", name: "USB-C Cable", category: "accessories", price: 9.99, inStock: false,})yield* db.entities.Products.put({ productId: "p-4", name: "Monitor Stand", category: "accessories", price: 49.99, inStock: true,})yield* db.entities.Reviews.put({ reviewId: "r-1", productId: "p-1", rating: 5, comment: "Great mouse!",})yield* db.entities.Reviews.put({ reviewId: "r-2", productId: "p-2", rating: 4, comment: "Good keyboard",})The table now has 6 items: 4 products and 2 reviews.
Step 4: Operations
Section titled “Step 4: Operations”Basic Scan
Section titled “Basic Scan”db.entities.Products.scan() returns a BoundQuery that reads the entire table. Call .collect() to execute it. Despite sharing a table with reviews, the scan only returns product items — the ORM automatically adds a FilterExpression on __edd_e__:
const allProducts = yield* db.entities.Products.scan().collect()Scan with Filter
Section titled “Scan with Filter”Scans support all BoundQuery combinators, including .filter(). Chain combinators on the BoundQuery before calling .collect(). The filter is applied server-side after items are read, reducing network transfer:
const inStockProducts = yield* db.entities.Products.scan().filter({ inStock: true }).collect()Note that filters do not reduce read capacity — DynamoDB still reads every item in the table. Use queries with partition keys when you need efficient reads.
Scan with Limit
Section titled “Scan with Limit”.limit() controls how many items DynamoDB evaluates per request. Combined with .collect(), it caps the total results:
const firstTwo = yield* db.entities.Products.scan().limit(2).collect()Scan with Consistent Read
Section titled “Scan with Consistent Read”.consistentRead() passes ConsistentRead: true to DynamoDB, ensuring you read the most recent data. This costs 2x the read capacity of eventually-consistent reads:
const consistent = yield* db.entities.Products.scan().consistentRead().collect()Entity-Type Isolation
Section titled “Entity-Type Isolation”The key feature of scans in a single-table design: each entity’s scan only returns its own items. The __edd_e__ discriminator attribute is automatically included in the FilterExpression:
const productScan = yield* db.entities.Products.scan().collect()const reviewScan = yield* db.entities.Reviews.scan().collect()Even though both scans read the same physical table, each returns only items matching its entity type.
Scan vs Query
Section titled “Scan vs Query”| Scan | Query | |
|---|---|---|
| Reads | Entire table (or index) | Single partition key |
| Cost | Proportional to table size | Proportional to items in partition |
| Use case | Analytics, admin tools, data export | Application access patterns |
| FilterExpression | Reduces network transfer, NOT read capacity | Same behavior |
Prefer queries for application access patterns. Use scans for administrative operations, data migrations, or when you genuinely need all items of a given entity type.
Running the Example
Section titled “Running the Example”The complete runnable example is at examples/scan.ts in the repository.
Start DynamoDB Local:
docker run -d -p 8000:8000 amazon/dynamodb-localRun the example:
npx tsx examples/scan.tsLayer Setup
Section titled “Layer Setup”const AppLayer = Layer.mergeAll( DynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }), MainTable.layer({ name: "scan-demo-table" }),)
const main = program.pipe(Effect.provide(AppLayer))
Effect.runPromise(main).then( () => console.log("\nDone."), (err) => console.error("\nFailed:", err),)Key Takeaways
Section titled “Key Takeaways”| Concept | How it’s used |
|---|---|
db.entities.Entity.scan() | Returns a BoundQuery — composable with all BoundQuery combinators |
.collect() / .paginate() | Terminals on BoundQuery — execute a scan (or query) |
.filter() | Server-side FilterExpression on scans — reduces network transfer |
.limit() | Caps items evaluated per DynamoDB request |
.consistentRead() | Strongly consistent reads at 2x RCU cost |
| Entity-type isolation | __edd_e__ filter ensures scans return only the matching entity type |
| Scan vs Query | Scans read the whole table; queries target a partition — prefer queries for app patterns |
What’s Next?
Section titled “What’s Next?”- Example: Conditional Writes & Filters — Conditional puts, updates, deletes, and filter operators
- Example: Projections — Selective attribute retrieval with ProjectionExpression
- Queries Guide — Index queries, pagination, sort key conditions, and collections
- Example: Human Resources — Full single-table design with 3 entities, collections, and transactions