Module 6 · Exercise · ~45 min · Real Go code

Idempotent Signed Submitter

The Module 6 capstone: a Submitter that signs commands with Ed25519, dedups by command_id, retries transient failures with backoff + ctx cancellation, and stays correct under concurrent use. The exact shape you'll wrap around real Canton gRPC calls.

Where
cd module-06-canton-patterns/exercises/exercise-01-signed-submitter
go test -v -race ./...

What gets tested

  1. Happy path — submit succeeds, signature verifies on the backend side.
  2. Empty ID — returns wrapped ErrPermanent.
  3. Idempotency — same command_id twice → one backend call, same Receipt returned both times.
  4. Transient retry — backend fails twice then succeeds; submitter makes exactly 3 attempts.
  5. Retry exhaustion — maxAttempts reached → ErrTransient returned, last attempt's error wrapped.
  6. No retry on permanent — ErrPermanent → exactly 1 backend call.
  7. Ctx cancellation — long backoff + short ctx timeout → returns quickly with ctx error.
  8. Concurrent safety — 50 goroutines, 5 unique IDs → exactly 5 backend calls (dedup honored even under concurrency).

What this combines

Once you can write this, the capstone is mostly wiring it to real gRPC.

One non-obvious thing

The dedup check should briefly hold a mutex, find no entry, release the mutex, then do the I/O, then re-lock to write the result. Never hold a mutex across the I/O call — if Submit blocks for a second, every other caller is blocked too, and contention destroys throughput.

This is one of the most common Go bugs in production: the cache lookup and the cache populate happen under the same lock. Don't.