Go architectures · 3 MIN READ

Event-driven services: delivery and idempotency

Treat duplicate delivery as normal and define the durable boundary.

Treat duplicate delivery as normal and define the durable boundary. Event-driven systems decouple a producer from consumers through notifications or durable messages. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.

Architecture · A MAP OF THE IDEA

  1. Producer commits an event or outbox record
  2. Broker may redeliver
  3. Consumer atomically records its effect

Useful when

  • Independent reactions should not block a request
  • Bursty work benefits from durable buffering

Watch out

  • Delivery duplicates and reordering need policies
  • Debugging spans queues and consumers

Implementation & operation cost: High

Fit depends on the task

Small tool or CLI1/5
Domain-rich application3/5
Independent distributed services5/5

Editorial suitability, not benchmarks or a universal ranking. Styles describe different axes and can be combined.

01

The design decision

Event-driven systems decouple a producer from consumers through notifications or durable messages. Consumers should know what happened and decide their own reaction. Delivery can repeat, so the design needs event identities, ordering rules and a durable acknowledgment strategy.

02

A complete Go example

Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.

go
package main

import (
	"fmt"
)

type Consumer struct {
	seen  map[string]bool
	count int
}

func (c *Consumer) Handle(id string) {
	if c.seen[id] {
		return
	}
	c.seen[id] = true
	c.count++
}

func main() {
	c := Consumer{seen: make(map[string]bool)}
	c.Handle("event-1")
	c.Handle("event-1")
	c.Handle("event-2")
	fmt.Println(c.count)
}

// Output:
// 2
Read the code

Two deliveries of event-1 create one effect. This single-goroutine memory map demonstrates deduplication only. Production deduplication must commit the event identity and business effect atomically; otherwise a crash can lose work or repeat the effect.

03

Tradeoffs and the simpler alternative

Buffering can isolate timing, but it does not guarantee business completion. Define dead-letter handling, retry limits and observability. A transactional outbox can bridge a database commit and later publication; a plain goroutine cannot replace durable messaging.

04

Specify the delivery contract

A producer commits a business change and an outbox record in one transaction. A relay publishes the record and may publish it again after a crash. The consumer therefore needs a stable event identifier and an idempotent effect. Acknowledge only after the required durable work. At-least-once delivery is not exactly-once business processing: a payment gateway, email API and local store do not share one magical commit.

05

Choose ordering, retries and a poison-message path

Order events only where the business needs it, often per aggregate rather than globally. Bound retries with backoff and jitter, and route repeated failures to a monitored dead-letter path with a replay procedure. Distinguish invalid events from temporary infrastructure outages. A permanently malformed message should not block unrelated work forever. Version event schemas compatibly and include correlation identifiers without leaking tokens or personal payloads into logs.

06

Exercise the crash windows

Test a crash after database commit but before publish, after publish but before marking the outbox sent, and after the consumer effect but before acknowledgement. Verify duplicates do not repeat the effect and that lag is visible in metrics. Monitor oldest unprocessed age, backlog and failure count, not only whether the worker process is alive. Use events for meaningful decoupling; a synchronous local call is clearer when a caller needs an immediate answer and no durable handoff.

TAKE THIS WITH YOU

Repeating a message does not repeat its business effect. Buffering can isolate timing, but it does not guarantee business completion. Define dead-letter handling, retry limits and observability. A transactional outbox can bridge a database commit and later publication; a plain goroutine cannot replace durable messaging.

Check your understanding

Which statement best describes Event-driven services: delivery and idempotency?

Go to the source

Progress stays in this browser. No account needed.