Backends
The Store interface, PostgreSQL, MongoDB, and DynamoDB.
Every backend implements one shared interface — dbswitch.Store — so the same CRUD code
runs against PostgreSQL, MongoDB, or DynamoDB.
The Store interface
type Store interface {
CreateTable(ctx context.Context, t Table) error
Insert(ctx context.Context, table string, data map[string]any) error
FindOne(ctx context.Context, table string, where map[string]any) (map[string]any, error)
Find(ctx context.Context, table string, where map[string]any) ([]map[string]any, error)
List(ctx context.Context, table string, opts ListOptions) ([]map[string]any, error)
Count(ctx context.Context, table string, filter map[string]any) (int64, error)
Update(ctx context.Context, table string, set, where map[string]any) (int64, error)
Delete(ctx context.Context, table string, where map[string]any) (int64, error)
Close()
}*postgres.DB, *mongo.Store, and *dynamodb.Store all satisfy it (enforced at compile
time with var _ dbswitch.Store = (*DB)(nil)), so you can write backend-agnostic code:
// Works with *postgres.DB, *mongo.Store, or *dynamodb.Store.
func seed(ctx context.Context, db dbswitch.Store) error {
return db.Insert(ctx, "users", map[string]any{"email": "a@b.com"})
}PostgreSQL
db, err := postgres.Open(ctx, os.Getenv("DATABASE_URL"))
defer db.Close()The SQL backend: CreateTable runs CREATE TABLE IF NOT EXISTS, conditions compile to
parameterized WHERE col = $n AND …, and driver errors map to ErrNotFound /
ErrDuplicate (the constraint name comes from pgconn.PgError). See
Schema and CRUD.
MongoDB
Mongo needs the database name separately from the connection URI (a collection is
reached as db(name).Collection(table)):
db, err := mongo.Open(ctx, os.Getenv("MONGO_URI"), "myapp")
defer db.Close()Mongo is schemaless, so the abstractions map like this:
| dbswitch concept | MongoDB |
|---|---|
| Table | Collection |
CreateTable | No DDL — creates a unique index per Unique column (collections are created on first write) |
PrimaryKey column | Mapped to Mongo's _id (already uniquely indexed) |
"id" field on insert/read | Mapped to/from _id automatically |
| Duplicate key | *dbswitch.DuplicateError — Constraint is the violated index name (e.g. "email_1") |
| Not found | dbswitch.ErrNotFound |
Column types and defaults (TypeText, DefaultCurrentTime, …) are SQL concepts —
Mongo ignores them since documents are schemaless. Only Unique (→ unique index) and the
primary key (→ _id) affect the Mongo backend.
DynamoDB
db, err := dynamodb.Open(ctx) // uses the default AWS config chain (env vars, IAM role, etc.)
defer db.Close()For DynamoDB Local or any custom endpoint, pass a client option:
db, err := dynamodb.Open(ctx, func(o *dynamodbsdk.Options) {
o.BaseEndpoint = aws.String("http://localhost:8000")
})DynamoDB is schemaless like Mongo, but has no query planner or secondary index by default, which changes the abstractions more than Mongo does:
| dbswitch concept | DynamoDB |
|---|---|
| Table | Table (billed PAY_PER_REQUEST — no throughput to tune) |
PrimaryKey column | Must be named "id" — becomes the table's partition key |
CreateTable | Real CreateTable DDL (unlike Mongo's implicit collections); idempotent |
Unique on "id" | Enforced via a conditional PutItem |
Unique on other columns | Not supported — CreateTable returns an error instead of ignoring it |
FindOne / Find / List / Count by {"id": v} | Direct GetItem — cheap |
| Same, by any other field | Full-table Scan + FilterExpression — correct, not free at scale |
Update / Delete by {"id": v} | Direct UpdateItem / DeleteItem |
| Same, by any other field | Scan for matches, then one call per match |
ListOptions.SortBy / After | Emulated in memory after a Scan — no sort key/GSI support yet |
| Duplicate key | *dbswitch.DuplicateError — Constraint is "<table>.id" |
| Not found | dbswitch.ErrNotFound |
Composite (partition + sort key) tables aren't supported yet — only a single "id"
partition key. For filters or sorting on hot paths, add a Global Secondary Index and query
it with the AWS SDK directly; the Scan-based fallback here is for convenience and
small-to-medium tables, not a replacement for indexed access patterns.
Because errors are unified, errors.Is(err, dbswitch.ErrDuplicate) and
errors.Is(err, dbswitch.ErrNotFound) work identically across all three backends —
only the Constraint string differs by driver. See Errors.