Skip to content

An invoice's reliability

Keyboard: ← → to move, F for full screen, O for overview.

Tan with a serious expression

Muhammet Şafak — Presentations

An invoice's reliability

Number, write, repeat and outcome: an invoice can go wrong four times on its way.

Muhammet Şafak

Section 01

Number

In e-invoicing, a number must be both free of duplicates and free of gaps.

Generate too late and you get a race; generate too early and you get a gap.

Muhammet ŞafakNumber collision in an e-invoice integration

Two constraints

Four approaches, two constraints

The database's own sequence (SEQUENCE, AUTO_INCREMENT) does not give back a value consumed on rollback; it produces gaps.
ApproachNo duplicatesNo gapsFailure mode
MAX()+1 at send timeNoYesCollision with parallel workers
Generate at enqueueYesNoFailed or cancelled → gap
DB sequenceYesNoRollback doesn't return the value → gap
JIT reservationYesYesCost: an early commit is mandatory

JIT reservation

The number, right before sending

  1. Step 1: LocklockForUpdate
  2. Step 2: Reserveif reserved_no is empty, counter++
  3. Step 3: Early commitstatus = SENDING
  4. Step 4: Integratoroutside the lock
  5. Step 5: Retrysame reserved_no

A lock is held for DB state, not for external I/O.

Muhammet ŞafakThe integrator call takes 200 ms to 2 seconds · sade.dev

Section 02

Delivery

The invoice goes to the database, the event to RabbitMQ: two systems, no single atomic step.

Dual-write

Writing to two systems

Dual-writesave() and publish() are separate
  • Publish fails → lost event
  • Rollback happens → phantom event
  • DB::transaction does not cover RabbitMQ
Transactional outboxThe write drops to a single system
  • Invoice and outbox row in the same commit
  • A separate process publishes to the broker
  • Dual-write shrinks to a single write

Repeat

Repeat delivery must be harmless

At-least-once onlyIdempotent consumer
DeliveryAt least once, sometimes twiceAt least once, sometimes twice
RecordProcessed ids are not keptprocessed_messages, unique constraint
TransactionWork and stamp are separateWork and stamp in the same transaction
OutcomeA repeat repeats the effectEffectively-once

Write the data to a single system, suppress repeats at the consumer, seal the sensitive field before it leaves.

Muhammet ŞafakOutbox, idempotent consumption and field encryption

Section 03

Outcome

The outbox guaranteed delivery; it did not guarantee the same outcome.

The real equation

outcome = f(message, hidden inputs)

Between production and consumption there can be milliseconds, twenty minutes or three days.

Hidden inputs

Six hidden inputs

Six hidden inputs encountered in an event-driven invoice flow.
InputIn practice
TimeReading the current clock for the document date
DatabasePrefix X at creation, Y at send time
ConfigurationThe same allow-list in two separate services
External system"This already exists", but what exists is unclear
EnvironmentOne framework swallows the warning, another turns it into an exception
WorkerParallel replicas, locks, repeat delivery

A noisy failure beats a silent wrong outcome

Four moves

  1. Step 1: Freeze the contextat production time
  2. Step 2: Time is an inputa retry does not change it
  3. Step 3: Name the forkwrite the reason to the log
  4. Step 4: Don't resolve silentlystop on ambiguity
Tan waving

Thank you

muhammetsafak.com.tr

What enters the decision is frozen; what doesn't is carried by reference.

Share, embed, download