Skip to content

feat(notification): tell passengers what happened to their journey, closing US-009, US-010, US-011 and EPIC-05 - #30

Merged
GODSCAR1 merged 1 commit into
mainfrom
feat/notifications
Aug 20, 2026
Merged

GODSCAR1 merged 1 commit into
mainfrom
feat/notifications

Conversation

@GODSCAR1

Copy link
Copy Markdown
Collaborator

Summary

A passenger is now told when their booking is made, when their payment goes through, and when they have checked in. The new notification-service listens to booking.created.v1, payment.succeeded.v1 and checkin.completed.v1, composes the message, records it and hands it to a channel. Nothing calls it and it calls nobody. Two other services had to change to make that possible: booking-service gained an outbox, because a booking having been made is something only it can say, and the payment events gained a passengerId, because a payment message could not otherwise be addressed to anyone. Nobody is actually told anything, and the code says so rather than pretending: no service here holds an email address, a phone number or a name, so the channel is a port whose adapter writes a log line and every send is recorded with the moment the channel took it.

Changes

notification-service (new)

  • Three Maven modules in the ADR-006 shape, on port 8085 with a database of its own. The domain holds Notification, NotificationType and NotificationMessage, which is where the wording lives.

  • One listener per topic, each mapping its event into a command. NotifyPassengerService composes the message, NotificationRecorder claims the event id and writes the notification in one transaction, and only then is the channel called.

  • Written, sent, marked, in that order. A crash between the write and the send leaves a notification nobody received, which the partial index over sent_at IS NULL makes findable. The other order would send one and forget it had.

  • processed_events per ADR-014, which matters more here than anywhere: a booking confirmed twice is the same booking, and a passenger notified twice is a passenger who noticed.

  • No read endpoint. This is the record of a send, not an inbox, and no story asks for one.
    booking-service

  • An outbox, a relay, and booking.created.v1 carrying the passenger, the seats and the total.

  • BookingRecorder holds the write and the announcement in one transaction, and CreateBookingService delegates to it. This service had no transaction machinery at all: its use cases relied on @Transactional in the adapters, which is not enough, because a @TransactionalEventListener(BEFORE_COMMIT) published outside a transaction does not run and does not say so.

  • Announced only when the write happened. A repeated request carrying a key that is already taken gets the booking that exists, and nothing new to tell anyone about.
    payment-service

  • payment.succeeded.v1 and payment.failed.v1 carry passengerId. BookingToPay keeps the field booking-service already returned and payment discarded, PayBookingService hands it to the recorder, and PaymentSettled carries it to the listener.

  • Payment does not store it. No column, no migration. A payment is money, and whose booking it settles is booking-service's business.
    Infrastructure and docs

  • Compose gains notification-db and notification-service, and gains the broker address checkin-service was missing (see below).

  • NotificationJourneyE2ETest joins the e2e module, whose stack now starts eleven containers.

  • scripts/smoke-notifications.ps1 walks a whole journey and shows what the passenger was told at each step. scripts/README.md records what the scripts are for.

  • ARCHITECTURE.md gains ADR-017 and ADR-018, and ADR-001 is corrected.

Notes

ADR-001 was wrong and now says so. It claimed booking-service would never need an outbox, because everything it could announce another service already knew. Notifications are the case that does not fit: only booking-service can say a booking was made, and something finally needed to hear it. The record is amended rather than quietly rewritten.

Getting the passenger onto a payment notification had three candidates, argued in ADR-018. A synchronous read from inside the consumer was rejected: when booking-service is down the consumer either blocks its partition or drops the notification. Remembering the booking from booking.created.v1 was rejected for a subtler reason: they are different topics, Kafka orders within a partition and not across topics, and in production minutes pass between booking and paying while in the end-to-end test milliseconds do. A failure that only appears when things are fast is the kind that appears in a demonstration.

The contract stayed at v1. Adding a field does not break a consumer that ignores what it does not know, and booking-service's copy of the payment event declares two fields and says exactly that in its own javadoc.

Two defects surfaced during this epic, and it is worth recording which found them:

What was wrong What found it
checkin-service had no broker address in docker-compose.yml, so its outbox could not deliver anything outside the end-to-end stack The check-in smoke script. The e2e suite builds its own stack and passes each service its broker address directly, so by construction it never reads that file
Three e2e journeys budgeted 30 seconds for the payment saga, which is no longer enough now the stack runs eleven containers The suite, intermittently. The notification journey passed the same wait with a longer budget in the same run, which is what showed it was patience rather than a fault

Left out on purpose, and recorded in ADR-017 rather than papered over:

  • Nobody is actually told anything. A log line is not a notification. The day this system has contact details it needs a real adapter and a retry policy to go with it.
  • Nothing sweeps the unsent. The index exists, the job does not.
  • processed_events is never purged, the same gap booking-service already has.
  • A booking notification cannot name the flight. booking-service holds seats by id and never learns the flight number, so its message names the booking and the total. Only the check-in message can say which aeroplane.

Testing

./mvnw -B clean verify runs 402 tests with zero failures; the four skips are the e2e classes staying behind their flag. Per module against what was there before: booking-infrastructure 40 to 55, payment-infrastructure 41 to 43, and notification's two modules adding 15 and 20. ./mvnw -B verify -pl e2e-tests "-Dairline.e2e=true" runs 35 e2e tests against the real stack, up from 28.

Against compose, all five smoke paths were run: smoke-notifications.ps1 ends with three notifications for one booking, each caused by a different service, all of them sent; smoke-checkin.ps1 and its -TooEarly path, the second now printing the refusal in full, title, reason and problem type; and smoke-payment.ps1 with -Decline, whose output also shows passengerId present in a real payment.failed.v1 message.

The tests that carry the new guarantees: NotificationTest pins the wording, including that a booking message does not call an unpaid booking confirmed; NotifyPassengerSliceTest covers all three notifications, that a redelivered message does not produce a second, and that a failing channel leaves the notification behind unsent; BookingOutboxSliceTest proves the announcement shares the transaction that wrote the booking and that a repeated request announces nothing; NotificationJourneyE2ETest is the only place where what three services publish is read by the thing that consumes it.

Closes #14 (US-009)
Closes #15 (US-010)
Closes #16 (US-011)
Closes #13 (EPIC-05)

Adds notification-service, which listens to booking.created.v1, payment.succeeded.v1 and checkin.completed.v1, composes the message and hands it to a channel. Nobody is actually told anything: no service here holds an email address, a phone number or a name, so the channel is a port whose adapter writes a log line, and every send is recorded with the moment the channel took it (ADR-017).

booking-service gains an outbox and publishes booking.created.v1. ADR-001 said it would never need one, because everything it could announce another service already knew; a booking having been made is the case that broke that, since only booking-service can say it. The service had no transaction machinery at all, and a BEFORE_COMMIT listener published outside a transaction does not run and does not say so.

payment.succeeded.v1 and payment.failed.v1 now carry the passenger. The alternatives were a synchronous call from inside the consumer, or remembering the booking from another topic with no ordering between them, and both are argued in ADR-018. Payment itself stores nothing new.

Fixes docker-compose: checkin-service was never given the broker address, so its outbox could not deliver outside the end-to-end stack. The check-in smoke script found it, which the e2e suite cannot: it builds its own stack and wires each service itself.

The smoke scripts now refuse a switch they do not declare, wait for the relay before reporting, print the reason behind a refusal, and stop when the stack is down instead of announcing a flight they never created. scripts/README.md records what they are for.
@GODSCAR1
GODSCAR1 requested a review from RicardoRB August 20, 2026 07:31
@GODSCAR1 GODSCAR1 self-assigned this Aug 20, 2026
@GODSCAR1
GODSCAR1 merged commit 9e8b866 into main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[US-011] Notificar check-in completado [US-010] Notificar pago completado [US-009] Notificar reserva creada [EPIC-05] Notificaciones

1 participant