Skip to content

Migration from ElectroDB

This guide helps ElectroDB users understand effect-dynamodb. It maps ElectroDB concepts to their effect-dynamodb equivalents with side-by-side examples.

ElectroDBeffect-dynamodbNotes
Entity definitionEntity.make()ElectroDB entities define model inline; effect-dynamodb separates model (Schema.Class) from entity binding
ServiceCollectionElectroDB’s Service groups entities; effect-dynamodb’s Collection provides multi-entity queries
Model attributesSchema.Class / Schema.StructPure domain schemas — no DynamoDB concepts
model.versionDynamoSchema.versionApplication schema versioning
model.serviceDynamoSchema.nameApplication namespace
Attribute type definitionsEffect Schema typesSchema.String, Schema.Number, Schema.Boolean, etc.
required: trueSchema-level required/optionalAll Schema fields required by default; use Schema.optional() for optional
readOnly: trueDynamoModel.configure(model, { field: { immutable: true } })Excluded from update input type
field: "dbName"DynamoModel.configure(model, { field: { field: "dbName" } })Domain-to-DynamoDB field renaming
hidden: trueDynamoModel.HiddenExcluded from asModel/asRecord decode
default: valueSchema defaultsSchema.withDefault(Schema.String, () => "default")
validate: RegExpSchema.pattern() / .check()Composable, named validation
Key templates ("USER#${userId}")composite: ["userId"]Attribute-list composition, not template strings
entity.put(data).go()yield* db.entities.E.put(data)Access via db.entities.* from DynamoClient.make({ entities, tables }), returns Effect
entity.get(key).go()yield* db.entities.E.get(key)Access via db.entities.* from DynamoClient.make({ entities, tables }), returns Effect
entity.query.<name>(key).go()db.entities.E.indexName(pk).collect()Entity index accessor on typed client, BoundQuery terminal
entity.create(data).go()yield* bound.create(data)Put with attribute_not_exists condition
entity.upsert(data).go()yield* bound.upsert(data)Create or update with if_not_exists for immutable fields
entity.patch(key).set({}).go()bound.patch(key).set(...)Update with attribute_exists — fails if item doesn’t exist
entity.remove(key).go()yield* bound.deleteIfExists(key)Delete with attribute_exists — fails if item doesn’t exist
entity.scan.go()db.entities.E.scan().collect()Scan returns BoundQuery — compose with filter, limit, etc.
.where() callback.condition() / .filter()Callback or shorthand syntax, chained on builders
.set({}).go()bound.update(key).set(updates)Fluent builder: chain combinators as methods
.remove([]).remove(fields) on BoundUpdateChainable REMOVE
.add({}).add(values) on BoundUpdateChainable ADD
.subtract({}).subtract(values) on BoundUpdateChainable SUBTRACT
.append({}).append(values) on BoundUpdateChainable APPEND
.delete({}).deleteFromSet(values) on BoundUpdateChainable DELETE from set
consistent: trueEntity.consistentRead / Query.consistentReadComposable combinator
response: "all_old"Entity.returnValues("allOld")Control DynamoDB ReturnValues on update/delete
pages: NQuery.maxPages(n)Limit total pages fetched
ignoreOwnership: trueQuery.ignoreOwnershipSkip entity type filter
.params()Query.asParamsReturn DynamoDB command input without executing
cursor paginationQuery.paginateStreamStream-based automatic pagination
ElectroError with codesTagged errors with catchTagDynamoError, ItemNotFound, etc.
client optionDynamoClient.layer()Layer-based dependency injection
table on Entity configTable.make() + table.layer()Table name injected at runtime via Layer

ElectroDB:

const User = new Entity({
model: { entity: "User", version: "1", service: "myapp" },
attributes: {
userId: { type: "string", required: true },
email: { type: "string", required: true },
displayName: { type: "string", required: true },
role: { type: ["admin", "member"], required: true },
createdBy: { type: "string", readOnly: true },
},
indexes: {
primary: {
pk: { field: "pk", composite: ["userId"] },
sk: { field: "sk", composite: [] },
},
byEmail: {
index: "gsi1",
pk: { field: "gsi1pk", composite: ["email"] },
sk: { field: "gsi1sk", composite: [] },
},
},
}, { client, table: "my-table" })

effect-dynamodb:

// 1. Model — pure domain schema (no DynamoDB concepts)
class User extends Schema.Class<User>("User")({
userId: Schema.String,
email: Schema.String,
displayName: Schema.NonEmptyString,
role: Schema.Literals(["admin", "member"]),
createdBy: Schema.String,
}) {}
const UserModel = DynamoModel.configure(User, {
createdBy: { immutable: true },
})
// 2. Schema (equivalent to service config)
const AppSchema = DynamoSchema.make({ name: "myapp", version: 1 })
// 3. Entity definition
const UserEntity = Entity.make({
model: UserModel,
entityType: "User",
primaryKey: {
pk: { field: "pk", composite: ["userId"] },
sk: { field: "sk", composite: [] },
},
indexes: {
byEmail: {
name: "gsi1",
pk: { field: "gsi1pk", composite: ["email"] },
sk: { field: "gsi1sk", composite: [] },
},
},
timestamps: true,
})
// 4. Table — register entities (equivalent to table config)
const MainTable = Table.make({ schema: AppSchema, entities: { UserEntity } })

ElectroDB:

// Create
const user = await User.put({ userId: "u-1", email: "a@b.com", ... }).go()
// Get
const { data } = await User.get({ userId: "u-1" }).go()
// Update
await User.update({ userId: "u-1" }).set({ email: "new@b.com" }).go()
// Delete
await User.delete({ userId: "u-1" }).go()

effect-dynamodb:

const program = Effect.gen(function* () {
// Get typed client with executable operations
const db = yield* DynamoClient.make({
entities: { UserEntity },
tables: { MainTable },
})
const users = db.entities.UserEntity
// Create
const user = yield* users.put({
userId: "u-1", email: "a@b.com", ...
})
// Get
const found = yield* users.get({ userId: "u-1" })
// Update (fluent builder on bound client)
yield* users.update({ userId: "u-1" }).set({ email: "new@b.com" })
// Delete
yield* users.delete({ userId: "u-1" })
})
// Provide dependencies via Layer (instead of constructor options)
program.pipe(
Effect.provide(
Layer.mergeAll(
DynamoClient.layer({ region: "us-east-1" }),
MainTable.layer({ name: "my-table" }),
)
)
)

ElectroDB:

// Query by index
const { data } = await User.query.byEmail({ email: "a@b.com" }).go()
// With sort key condition and filter
const { data } = await Task.query
.byProject({ projectId: "p-1" })
.where(({ status }, { eq }) => eq(status, "active"))
.go({ limit: 25 })
// Pagination with cursor
let cursor = null
do {
const result = await Task.query.byProject({ projectId: "p-1" }).go({ cursor })
processItems(result.data)
cursor = result.cursor
} while (cursor)

effect-dynamodb:

const db = yield* DynamoClient.make({
entities: { UserEntity, TaskEntity },
tables: { MainTable },
})
// Query by index — entity accessor returns BoundQuery
const userResults = yield* db.entities.UserEntity.byEmail({ email: "a@b.com" }).collect()
// With filter and limit
const taskResults = yield* db.entities.TaskEntity
.byProject({ projectId: "p-1" })
.filter((t, { eq }) => eq(t.status, "active"))
.limit(25)
.collect()
// Stream-based pagination (automatic)
const stream = db.entities.TaskEntity
.byProject({ projectId: "p-1" })
.paginate()
yield* Stream.runForEach(stream, (task) => processItem(task))

ElectroDB:

await User.put(data)
.where(({ email }, { attributeNotExists }) => attributeNotExists(email))
.go()
await User.update({ userId: "u-1" })
.set({ status: "active" })
.where(({ status }, { eq }) => eq(status, "pending"))
.go()

effect-dynamodb:

const db = yield* DynamoClient.make({
entities: { UserEntity },
tables: { MainTable },
})
const users = db.entities.UserEntity
yield* users.put(data).condition((t, { notExists }) => notExists(t.email))
yield* users.update({ userId: "u-1" })
.set({ status: "active" })
.condition((t, { eq }) => eq(t.status, "pending"))

ElectroDB:

await Product.update({ productId: "p-1" })
.set({ name: "New Name" })
.add({ viewCount: 1 })
.remove(["description"])
.append({ tags: ["new-tag"] })
.subtract({ stock: 3 })
.go()

effect-dynamodb:

const db = yield* DynamoClient.make({
entities: { ProductEntity },
tables: { MainTable },
})
const products = db.entities.ProductEntity
yield* products.update({ productId: "p-1" })
.set({ name: "New Name" })
.add({ viewCount: 1 })
.remove(["description"])
.append({ tags: ["new-tag"] })
.subtract({ stock: 3 })

ElectroDB:

const myService = new Service({
user: User,
order: Order,
})
const { data } = await myService.collections.byTenant({ tenantId: "t-1" }).go()
// data.user: User[], data.order: Order[]

effect-dynamodb:

// Collections are auto-discovered from entity indexes sharing the same
// `collection` property. No explicit Collection.make() needed.
// On UserEntity and OrderEntity, add: collection: "tenantMembers" to the index
const db = yield* DynamoClient.make({
entities: { UserEntity, OrderEntity },
tables: { MainTable },
})
const data = yield* db.collections.tenantMembers({ tenantId: "t-1" }).collect()
// { UserEntity: User[], OrderEntity: Order[] }

ElectroDB:

await myService.transaction.write(({ user, order }) => [
user.put(newUser).commit(),
order.delete({ orderId: "o-1" }).commit(),
user.check({ userId: "u-1" }).where(({ email }, { attributeExists }) =>
attributeExists(email)
).commit(),
]).go()

effect-dynamodb:

yield* Transaction.transactWrite(
UserEntity.put(newUser),
OrderEntity.delete({ orderId: "o-1" }),
Transaction.check(
UserEntity.get({ userId: "u-1" }),
{ attributeExists: "email" },
),
)

ElectroDB:

try {
await User.get({ userId: "u-1" }).go()
} catch (err) {
if (err.code === 1) { /* configuration error */ }
if (err.code === 2) { /* invalid identifier */ }
// ... numeric error codes
}

effect-dynamodb:

const db = yield* DynamoClient.make({
entities: { UserEntity },
tables: { MainTable },
})
const users = db.entities.UserEntity
yield* users.get({ userId: "u-1" }).pipe(
Effect.catchTag("ItemNotFound", () => Effect.succeed(null)),
Effect.catchTag("ValidationError", (e) => Effect.die(`Bad data: ${e.message}`)),
Effect.catchTag("DynamoError", (e) => Effect.die(`AWS error in ${e.operation}`)),
)

Features that effect-dynamodb provides that ElectroDB does not:

FeatureDescription
Built-in versioningversioned: true auto-increments version on every write. versioned: { retain: true } keeps version snapshots.
Built-in soft deletesoftDelete: true marks items as deleted instead of physical deletion. Includes restore() and purge().
Built-in unique constraintsunique: { email: ["email"] } enforces field-level uniqueness via sentinel items in atomic transactions.
Built-in optimistic locking.expectedVersion(n) on BoundUpdate fails the write if the current version doesn’t match.
7 derived typesModel, Record, Input, Update, Key, Item, Marshalled — all auto-derived from one entity declaration.
Stream paginationQuery.paginate returns an Effect Stream for lazy, composable pagination.
Layer-based DITable names and DynamoDB client injected at runtime via Effect Layers — no hardcoded config.
Config-based setupDynamoClient.layerConfig() reads from environment variables via Effect Config.
Table definition exportTable.definition() generates CreateTableCommandInput for CloudFormation/CDK/testing.
4 decode modesasModel (clean domain), asRecord (with system fields), asItem (with DynamoDB keys), asNative (raw AttributeValue).
Dual APIsAll public functions support both data-first and data-last (pipeable) calling conventions.
DynamoDB Streams decodeEntity.itemSchema() and Entity.decodeMarshalledItem() for typed stream record processing.

Features in ElectroDB that work differently or are not available in effect-dynamodb:

FeatureElectroDBeffect-dynamodb
Find/MatchAuto-selects index from attributesNot supported — use explicit entity.query.<indexName>() (more predictable)
Getter/setter hooksget/set callbacks per attributeUse Effect Schema.transform for equivalent functionality
Calculated/virtual attributesVia watch + set/getCompute at application layer or with Schema.transform
Key templates"USER#${userId}" template stringsAttribute-list composition (composite: ["userId"]) — more predictable
Attribute paddingpadding: { length, char }Automatic zero-padding for numeric composites in sort keys
Fluent chainingentity.query.byEmail(pk).where(...).go()Fluent BoundQuery: db.entities.E.byEmail(pk).where(...).collect()
Async/PromiseReturns PromisesReturns Effects — run via Effect.runPromise at the edge

See the ElectroDB Comparison for a full feature-by-feature comparison.