Portfolio Manifest — Prepared 2026-08-19

Terrence Daniels

Backend & Distributed Systems Engineer

An original implementation of the Distributed Saga pattern across six independent Spring Boot / gRPC microservices — a JWT-guarded API gateway in front of order placement, payment settlement, and restaurant fulfillment, coordinated through Kafka with compensating transactions instead of a shared database transaction. Built and verified one service at a time against real Postgres and Kafka infrastructure — not mocked past the point where mocking stops proving anything.

41phases shipped
107/107tests passing
6services complete
3saga chain hops
15real bugs found & fixed
2real infra, not mocked

Why this project

A whiteboard topic, built for real

Saga orchestration is a common interview-whiteboard topic and an uncommon thing to actually build end to end: compensating transactions, event ordering, idempotency, and the failure modes that only show up once services genuinely run independently. This isn't a fork of any existing project — the module boundaries and general shape of the problem (order → payment → fulfillment, with compensation on failure) are common territory for this class of system, but the code, design decisions, and tradeoffs recorded here are this repo's own, built one service at a time with the reasoning written down as it happened.

Spring Boot gRPC Kafka PostgreSQL Gradle (Kotlin DSL) JUnit 5 Docker Compose GitHub Actions CodeQL

Six services, one saga chain

Outbox pattern, compensating transactions, wired end-to-end behind a real security perimeter

api-gateway-service fronts the chain — JWT-guarded routing and a Resilience4j circuit breaker sit in front of everything below. order-service creates an order and stages an outbox record in the same transaction; a poller ships it to Kafka. payment-service consumes it, settles payment, and publishes its own event. restaurant-service consumes that, allocates inventory, and makes the saga's real decision — approve or reject — publishing back to both order-service and payment-service so a rejection compensates the payment and cancels the order. No shared database transaction anywhere in the chain.

Deliberate deviations, not defaults

spring-kafka directly instead of a Spring Cloud Stream binder — this project only ever targets Kafka, so the broker-swappability abstraction wasn't worth the config overhead. UUID ids and BigDecimal money instead of raw strings and doubles — floating point can't exactly represent decimal fractions, a real correctness concern once money's involved. Reasoning for both recorded in docs/architecture.md.

Idempotency guards, not TODOs

Every state-changing method follows the same cheap-check-before-load pattern: an existsBy… query first, then a status re-check after load, so a duplicate Kafka delivery — the normal case for at-least-once delivery, not an edge case — can't double- apply an order confirmation, a payment, or a compensation.

Real bugs, not just features

Found via actually running the thing, not reading the source twice

Every entry below was caught by a real, failing run — a test, a CI job, or a live boot against real infrastructure — not by static reading, and every fix was re-verified with another real run before being called done.

01
user-service — protobuf-java silently downgraded
spring-grpc's BOM was silently pulling protobuf-java below what user-contract's generated code needs, on user-service's actual runtime classpath — not just tests — since the module's first phase. Undetected until a test finally constructed a generated message at runtime. Fixed with an explicit version pin.
high · production
02
order-service — Spring Boot 3.4.1 can't boot under JDK 25
The module's first test (contextLoads) failed outright — Spring Framework's own bundled ASM can't parse JDK 25 class files during component scanning at all, deeper than an earlier Gradle-plugin-only version of the same issue. Checking user-service showed it had the identical latent risk, never caught because no test there had ever booted a real ApplicationContext. Fixed by bumping both modules to Boot 3.5.16.
high · infra
03
Consolidated test report — two report bugs
The custom root task merging every module's JUnit XML into one HTML file had two real bugs: a failing test silently blocked the report from generating at all, and — separately — the task had no declared inputs, so Gradle kept serving a stale report instead of regenerating it. Both fixed; the report is now a genuine CI artifact on every run.
medium · tooling
04
CI — gradlew committed without its executable bit
The first real CI run failed outright (Permission denied, exit 126) — gradlew had been committed from Windows without its executable bit, so git stored mode 100644 instead of 100755. Fixed with git update-index --chmod=+x, confirmed on a second real run.
medium · CI
05
api-gateway-service — a ported test that could never pass
Porting JwtPerimeterGuardGatewayFilterFactoryTest and actually running it — never assumed — showed two of its three cases asserting an HTTP response status the filter itself never sets; that translation only happens through GlobalExceptionHandler, a layer absent from an isolated filter unit test. Failed immediately with onError instead of the expected onComplete. Rewritten to assert the filter's real, provable contract instead.
medium · test correctness
06
order-service — log injection (CWE-117)
GitHub CodeQL flagged OrderController logging a whole request record whose itemCode field is free-form client input with no character restriction — \r/\n bytes could forge fake log lines. Fixed by logging fields individually and stripping control characters from the one field that actually needed it.
medium · security
07
user-service — BCryptPasswordEncoder.encode() never exercised
No registration RPC exists in this repo, and every test mocked PasswordEncoder with a literal placeholder hash — so the real bcrypt encode() path had never run once, in production code or tests. Added a real encode/matches round-trip test.
low · test coverage
08
user-service — login enumeration (CWE-203)
Login returned three distinguishable gRPC statuses depending on unknown-email vs. wrong-password vs. inactive-account, letting a caller enumerate registered emails — and their active status — without ever guessing a password. Collapsed to one generic UNAUTHENTICATED response after weighing the UX tradeoff.
medium · security
09
user-service — login timing side-channel (CWE-208)
Even after closing the status-code leak above, unknown-email and inactive-account logins skipped the deliberately-slow BCrypt comparison entirely, responding measurably faster than a real login attempt. Restructured so every path pays the identical real BCrypt cost, verified by reverting the fix and confirming new tests fail against the old code first.
medium · security
10
user-service — JWTVerifier rebuilt on every call
The sibling code in api-gateway-service already caches its JWTVerifier once at construction for performance; this file rebuilt one on every single verifyToken() call. Matched the established pattern — zero behavior change, existing tests passed unchanged as proof.
low · maintainability
11
user-service — zero repository test coverage
Unlike every other module's repositories, user-service had no @DataJpaTest coverage at all — findByEmail and the email column's unique constraint had never been verified against a real database. Added tests including a real proof the unique constraint is genuinely enforced.
low · test coverage
12
order-service — Kafka poison-pill risk, no error handler configured
Neither Kafka listener caught anything — an event referencing an unrecognized order ID or a malformed payload fell through to Spring Boot's autoconfigured default with nothing in the code announcing that. Verifying that default for real turned up a second bug: an earlier write-up had already mischaracterized it as retrying indefinitely, when the actual behavior is 10 rapid retries then a silent drop. Fixed with an explicit, bounded error handler — and the inaccurate claim corrected in the same PR.
medium · reliability
13
order-service — a second log injection, three services away from the first
The same client-controlled itemCode field CodeQL had already flagged once (item 06) turned out to have a second, cross-service path to an unsanitized log line: order-servicepayment-servicerestaurant-service's raw string-concatenated rejection reason → back into order-service's own cancelOrder. Traced end-to-end before being fixed with the same sanitizing pattern already established for the first instance.
medium · security
14
order-service — customerId and ticketId deserialized, never read
Two inbound Kafka events carried customerId (and one carried ticketId) all the way into confirmOrder/cancelOrder without either method ever checking them — a real asymmetry against this repo's own established pattern of validating every field on an inbound event. Fixed by cross-checking customerId against the order already on record, catching a corrupted or mismatched message on the shared topic instead of trusting it blindly.
medium · reliability
15
order-service — outbox publisher blocked on Kafka with no timeout
The transactional outbox's Kafka send blocked on an unbounded Future.get(), implicitly relying on Kafka's own undocumented 120-second client default while holding a database row lock open the whole time — a degraded broker could have held that lock for up to 20 minutes across a full batch. Fixed with an explicit, bounded timeout.
low · reliability

Verifying it actually works

Real Postgres, real Kafka, real partition assignments — not assumed

Every Kafka-wired service was booted for real against Docker Compose's Postgres and Kafka containers, not just unit-tested in isolation: a genuine HikariPoolPgConnection connection, and each service's consumer groups actually joining and getting real partition assignments against the live broker — the thing every prior test run under a mocked or absent broker couldn't prove.

107/107 tests, real infra behind them

Repository tests run against embedded H2 with real Hibernate DDL, not mocks; Kafka/Postgres wiring is separately verified against the actual Docker containers before being called done.

Live, committed test report

A consolidated test report, regenerated and committed back to main by CI after every real test run — checkable against the live repository, not a static screenshot.

Judgment calls

Where the interesting decisions actually happened

GitHub Pages was briefly switched to an Actions-based deployment to try matching this portfolio's own live-rendered-site pattern, then deliberately reverted back to a simpler committed-static-file approach once it became clear the fancier setup would replace the README-rendered repo homepage for no real benefit at this stage — matching a pattern isn't a reason to add complexity a project doesn't need yet. Separately, building out payment-service before restaurant-service wasn't the obvious reading of the original four-service plan — it came from actually checking which service consumes which event, not from the plan's stated order. restaurant-service depends on payment-service's output, not the reverse, so building it first would have meant building against nothing. Both calls, and the reasoning behind them, are recorded in todo.md as they happened, not reconstructed afterward.