-
-
Notifications
You must be signed in to change notification settings - Fork 0
FAQ
Common questions about eventferry — what it is, what it isn't, how it compares to alternatives, and recurring "should I…" judgment calls.
The dual-write problem. You want to change business state in a database and emit an event about that change. Doing both naively (commit; publish or publish; commit) has a real failure mode: one side succeeds, the other doesn't, and your system becomes inconsistent.
eventferry implements the transactional outbox pattern: the event row commits with the business change inside the same transaction, and a separate process publishes from the outbox table to the broker. Either both rows commit, or neither does. The broker eventually hears about every committed change exactly once (idempotent producer) or exactly-once with transactional EOS.
You're a good fit if any of these are true:
- You're publishing domain events to Kafka / Redpanda from a service backed by Postgres / MySQL.
- You're already running into the dual-write problem (lost events on crash, duplicate events on retry).
- You want strict per-aggregate ordering without sharding or partition assignment glue.
- You want type-safe payloads without dragging Confluent Schema Registry into a same-monorepo setup.
- You want a publisher you own — no Connect cluster, no Debezium, no separate operational surface.
You're NOT a good fit if:
- Your sink is something other than Kafka / Redpanda (no MQ-style publishers shipped today).
- You're at Debezium scale (millions of events / second per topic) — Debezium + Kafka Connect is the right answer at that volume.
- You want consumer-side framework features (consumer groups, exactly-once consume) — eventferry is publisher-only.
Debezium reads the database's binlog/WAL with no application coupling — your code doesn't know an event was emitted. eventferry's enqueue is an explicit call in your business handler — you choose what becomes an event and shape its payload.
| eventferry | Debezium | |
|---|---|---|
| Coupling to your code | Explicit enqueue call |
None (reads the binlog) |
| Payload shape | Whatever you write | Whatever the row looks like |
| Schema evolution | Your code controls | Tied to table schema |
| Operational surface | Your app process | Kafka Connect cluster |
| Scaling unit | Run more relays | Run more Connect tasks |
| Schema Registry | Optional | Required-ish |
Use Debezium when you want CDC of an existing schema unchanged. Use eventferry when you want to design domain events as a first-class concept.
Yes — the store's enqueue takes any object that exposes a query method (Postgres) or execute (MySQL). All four ORMs let you grab the underlying pg.Client / mysql2 connection mid-transaction:
// Prisma
await prisma.$transaction(async (tx) => {
await tx.order.create({ data: { ... } });
await store.enqueue(
(tx as any).$queryRawUnsafe.bind(tx), // grab the raw client
{ topic: "orders.created", ... },
);
});This is loosely typed but works. See Postgres Adapter for the canonical pattern (raw pg client).
A typed Prisma / Drizzle / TypeORM adapter package is on the roadmap — open an issue with your stack if you want it sooner.
That's the dual-write problem in one sentence. Commit succeeds, your process dies before the publish, broker never hears about the change. Or publish succeeds, commit fails on a constraint violation, broker hears about a change that didn't happen.
The whole point of the outbox pattern is to make the event durable inside the same transaction as the business change so this failure mode is impossible.
eventferry's default is at-least-once to the broker: every committed change reaches Kafka, possibly with duplicates on retry. Combined with idempotent producer (default on), duplicates are deduplicated on the wire, so the broker only commits each record once per partition.
For end-to-end exactly-once (producer-broker-consumer), opt into the transactional producer (transactional: true, transactionalId: "...") and configure consumers with isolation.level=read_committed. See Transactions and EOS.
The outbox layer dedups on retry too — once a row is done, it stays done. So a relay restart that re-claims a row mid-publish doesn't produce a duplicate at the outbox layer.
Yes. Pass any table name to createMigrationSql and PostgresStore({ table }):
const ordersStore = new PostgresStore({ pool, table: "outbox_orders" });
const usersStore = new PostgresStore({ pool, table: "outbox_users" });Each gets its own relay, its own publisher, its own DLQ topics. Common pattern for high-volume systems → see Operations Guide.
Either works. In-process is simpler — your service starts, the relay starts, lifecycle is shared. Separate process isolates the publish load and lets you scale relays independently.
Rule of thumb:
- Single-replica service → in-process.
- Multi-replica service → in-process; every replica runs a relay,
SKIP LOCKEDpartitions the work. - Massive publish load (> 1000 events/sec) → separate relay deployment, fewer replicas, larger batches.
To match what you actually use. If you're on Postgres + kafkajs, you don't need @eventferry/mysql's schema, @confluentinc/kafka-javascript's native build, or @eventferry/kafka-iam's AWS SDK chain. Optional peer dependencies + independent SemVer let you install only what you reach for.
For prototyping, use @eventferry/all — it pulls everything in one shot. Move to individual packages when your install footprint matters.
Yes to all. Each has its config quirks:
-
Aurora / Cloud SQL — set
wal_level=logicalvia the parameter group if you wantPostgresStreamingRelay. -
RDS MySQL / Aurora MySQL — enable binlog via the parameter group for
MysqlBinlogRelay. -
MSK — IAM auth via AWS MSK IAM. Topic auto-create is OFF by default; use
validateTopicsOnConnector Terraform-provisioned topics. -
Confluent Cloud — SASL/PLAIN with the cluster API key. Schemas registered via the Cloud UI or Confluent CLI; use
autoRegister: falsein production.
The eventferry-emitted outbox table schema is versioned with the package itself. Migrations and Upgrades lists schema diffs per minor.
Your event payload schemas (the things in defineOutbox(registry)) are versioned by you — eventferry doesn't enforce schema evolution rules. For runtime-enforced compatibility, use Schema Registry → Schema Registry.
Tradition. The outbox pattern's name predates eventferry — outbox is the conventional table name in literature (Pat Helland's "Life Beyond Distributed Transactions", Chris Richardson's Microservices Patterns, etc.). You can name it whatever you want — pass { table: "your_name" } everywhere.
Each package ships its own CHANGELOG.md inside the npm tarball:
npm view @eventferry/kafka@latest dist
# look at CHANGELOG.md inside the extracted tarball
# or:
npm view @eventferry/kafka changelogFor the source-of-truth, the .changeset/ directory in the repo accumulates pending changes; merging the Version Packages PR cuts a release and rolls CHANGELOG.md per package.
The packages are TypeScript-first. The compiled output is plain ES modules + CJS — JS consumers work fine, you just lose the type checking that catches schema mismatches at compile time.
Cross-language consumers (Go, Java, Python) are the explicit use case for Schema Registry → Schema Registry.
The integration test suite (packages/integration/test/) is the canonical example. Every supported configuration is exercised against real Postgres / MySQL / Redpanda / Schema Registry containers — clone the repo, read the tests, lift what fits.
git clone https://github.com/SametGoktepe/eventferry
cd eventferry
pnpm install
pnpm test:integration # requires DockerThe setup harness at packages/integration/test/setup/containers.ts shows the canonical container config (wal_level=logical, binlog flags, MSK-style brokers, etc.).
Repository · Issues · npm: @eventferry/all · MIT
Get going
Adapters
Type & schema
Security
Operational
- Transactions and EOS
- Admin Operations
- Observability
- Consuming Events
- Dead-Letter Queue
- Reliability and Error Handling
Operations
Reference