Phase 08 Projects — The M13 Capstone¶
Three projects. The first proves you can serve a real ML model from Java. The second proves you understand event-driven systems end-to-end. The third — the capstone — proves you can wire a full microservice system that a large MNC would actually recognize as production code. This is the anchor of your 13-month portfolio.
Every project below assumes you’ve completed Phase 07 and are comfortable with Spring Boot 3.4, Testcontainers, Prometheus, and Docker. If you’re not, go back. You cannot bluff Phase 08.
Project A — categorizer: ML-Powered Text Classification Service¶
Load a real ONNX classifier into a Spring Boot service, expose /classify over REST, deploy to a local Kubernetes cluster (kind or minikube), and instrument it so you can prove it works under load.
The Model¶
Train it yourself in Python (this is the only Python you need for this project):
# train_intent.py — pick your poison: sklearn / distilbert / a small CNN
# Suggested: fine-tune distilbert on the CLINC150 intent dataset (small, public, real)
# Export to ONNX:
torch.onnx.export(model, dummy_input, "intent-v1.onnx",
input_names=["input_ids", "attention_mask"],
output_names=["logits"],
dynamic_axes={"input_ids": {0: "batch"}, "attention_mask": {0: "batch"}})
Or use a pre-trained ONNX classifier from the Hugging Face ONNX Model Zoo if you want to skip training. The point is serving, not training.
The Service¶
Spring Boot 3.4 + Java 21 + virtual threads
ONNX Runtime Java (direct — no DJL for this one; keep deps minimal)
Endpoint:
POST /api/v1/classify { "text": "..." }→{ "label": "book_flight", "confidence": 0.94 }Load model at startup, warmup 20 requests before flipping readiness
Backpressure via
Semaphore(50)on concurrent inferencesBatching drain-loop (from file 05) — measure with and without batching
Prometheus metrics:
model_inference_duration_seconds{model, version},model_inference_total,model_inference_errors_total, custom drift countermodel_prediction_class_total{class}OpenTelemetry traces (span per phase: preprocess, infer, postprocess)
/actuator/health/readinessincludes model-loaded check
Deployment¶
Multi-stage Dockerfile: build with
maven:3.9-eclipse-temurin-21, runtime witheclipse-temurin:21-jre-alpine, non-root userModel artefact bundled in the image (versioned filename)
Helm chart with values for image tag, resource requests/limits (
2Gimemory,1000mCPU), all three probesDeploy to local
kindclusterPrometheus (via Helm) scrapes the service; Grafana dashboard shows p95 inference latency + prediction distribution
Load test with
k6at 100 → 500 → 1000 RPS; document where p95 breaks
Deliverables¶
Public GitHub repo with README including a live load-test screenshot
Grafana dashboard JSON checked in
Blog post or README section: “training in Python, serving in Java — why and how”
The ONE hard number: p95 inference latency at 500 concurrent requests, measured, defended, and matching your SLO
Estimated effort: 25–35 hours over 3 weeks.
Project B — event-pipeline: Kafka Producer/Consumer with DLQ and Idempotent Sink¶
Build a small event-driven system that demonstrates every Kafka production pattern from file 02. Two services + Kafka + Postgres, all in Docker Compose, all with tests.
Architecture¶
[producer-svc] --publish--> [Kafka topic: link.event.v1] --consume--> [consumer-svc] --write--> [Postgres]
|
+--fail--> [link.event.v1.retry] --> [link.event.v1.DLT]
The Services¶
producer-svc:
Spring Boot REST:
POST /events { "type": "created", "linkId": "...", "userId": "..." }Serializes to Avro (with Confluent Schema Registry in the compose stack) OR Protobuf; NOT JSON
Kafka producer config:
acks=all,enable.idempotence=true, key =userIdPublishes
LinkEventtolink.event.v1
consumer-svc:
@KafkaListenerwith manual ack (from file 02)Deduplicates via
event_idUNIQUE constraint in Postgres (idempotency at the DB layer)On transient failure → retry topic with exponential backoff
On poison pill (bad Avro, business validation failure) → DLT
Postgres sink table:
link_event(event_id UUID PK, type TEXT, link_id TEXT, user_id TEXT, event_time TIMESTAMPTZ, received_at TIMESTAMPTZ DEFAULT now())
Test Matrix¶
Unit tests for producer with
MockProducerand consumer with@EmbeddedKafkaor Testcontainers KafkaIntegration test: fire 10,000 messages, assert exactly 10,000 rows in Postgres (idempotency proof)
Poison-pill test: send a message with a broken schema, assert DLT has 1 message, main table unchanged
Chaos test: bring Kafka down mid-run with
docker compose stop kafka; bring it back; assert no messages lost, no duplicates
Observability¶
Kafka UI (Confluent Kafka UI, Kafdrop, or Redpanda Console) in compose
Prometheus + Grafana with:
Consumer lag per topic per group
DLT depth (alert threshold: > 0)
Producer send success/failure rate
Consumer processing duration p95
Deliverables¶
docker compose upboots the whole stack (producer, consumer, Kafka, Zookeeper or KRaft, Schema Registry, Postgres, Prometheus, Grafana, Kafka UI)make testruns unit + integration + chaos suitesREADME explains: partition key choice, delivery semantics chosen and why, DLT alerting strategy
Screenshot: consumer lag graph during the 10k-message load test
Estimated effort: 20–30 hours over 3 weeks.
Project C — CAPSTONE: shortly-platform — The Full Microservice System¶
This is the M13 portfolio anchor. Extend the shortly URL shortener from Phase 07 into a multi-service platform on Kubernetes. This is the project you point at in studies for the next five years.
Architecture¶
┌─── shortly-analytics (consumes link.hit.v1, aggregates)
│
[gateway] --> [shortly-api] --> [link.event.v1 (Kafka)]
│ │
├── Postgres ├──> shortly-abuse (consumes new links, safe-browsing check)
├── Redis (cache) │
├── JWT (self-issued) └──> shortly-notifier (email/webhook on events)
│
└── [shortly-categorizer] (calls Project A's ONNX classifier for auto-tagging)
Five services, one event backbone, one cache, one relational DB, one ML model.
The Services¶
Service |
Purpose |
Stack |
|---|---|---|
gateway |
Spring Cloud Gateway; TLS, JWT validation, rate limit, routing |
Spring Cloud Gateway 4.x |
shortly-api |
The original Phase 07 service, now emitting Kafka events instead of doing everything itself |
Spring Boot 3.4 |
shortly-analytics |
Consumes |
Spring Boot + Kafka Streams |
shortly-abuse |
Consumes |
Spring Boot + Resilience4j |
shortly-notifier |
Consumes multiple topics, sends email (SES / Mailgun sandbox) and webhooks |
Spring Boot |
shortly-categorizer |
Project A, called synchronously from |
Spring Boot + ONNX Runtime |
The Non-Negotiables¶
Every service:
Constructor injection only
RestControllerAdvicewith RFC 7807ProblemDetailMicrometer + Prometheus scrape endpoint
OpenTelemetry OTLP export
Correlation ID filter (X-Request-Id)
Testcontainers integration tests (no H2, no mocked Kafka in the happy path)
Multi-stage Dockerfile with Application CDS
Event flow:
link.created.v1,link.updated.v1,link.deleted.v1,link.hit.v1topicsAvro schemas in Schema Registry, BACKWARD compatibility
At-least-once + idempotent consumers (event_id UNIQUE)
DLT per topic, monitored, alerted
Cache:
Redis cache-aside for slug → URL resolution
Negative caching for unknown slugs (60s TTL)
Cache stampede prevention via single-flight lock
Security:
Self-issued JWT (RS256), 15-min access + 7-day refresh
@PreAuthorizeon admin endpoints (hasAuthority('SCOPE_admin'))Rate limits: 100 req/min anon, 1000 req/min authenticated, 10 req/min for
POST /linksNo secrets in Git; K8s Secrets sourced from Sealed Secrets or External Secrets Operator
Deployment:
Local
kindclusterOne Helm umbrella chart with subcharts per service, or separate charts + ArgoCD Application-of-Applications pattern
Namespace
shortly-dev,shortly-stagingHPA on custom metric (
http_server_requests_seconds_countrate) via Prometheus AdapterNetworkPolicy: default-deny, explicit allowlists between services
GitOps: ArgoCD watches a
gitops/directory; deploys happen via merged PR
Observability:
Grafana dashboards per service + one platform overview dashboard
Alerts: 5xx rate > 1% for 5m, p95 > SLO for 10m, DLT depth > 0, Kafka consumer lag > 10k
Distributed tracing across all six services visible in Tempo / Jaeger — a single trace spans gateway → api → categorizer → Kafka publish
Test Matrix¶
Per service: unit + slice + integration (Testcontainers)
Cross-service contract tests: WireMock stubs for the ML classifier when
shortly-apiis testedE2E happy path: run against the deployed
kindcluster, hitPOST /links→ assert Kafka events fire → assert analytics reflects hit → assert abuse check ranLoad test:
k6forPOST /links(creating shortened URLs) andGET /{slug}(redirects) — measure system-wide p95 with all services in the trace
Deliverables¶
One GitHub org / monorepo (
shortly-platform/) with all six services and the Helm chartsmake dev-upstarts everything locally in kind + Helmmake load-testruns the k6 script and asserts SLOsREADME with: architecture diagram, event flow diagram, screenshot of a distributed trace spanning six services, screenshot of the platform Grafana dashboard, curl examples end-to-end
Public URL of at least ONE service exposed via ngrok / Cloudflare Tunnel or similar (real, running, callable)
A 5-minute video walkthrough for study partners: what you built, why the seams are where they are, what you learned
The study Story¶
By the end of the capstone you should be able to deliver, without notes, a coherent 10-minute answer to any of these:
“Walk me through a request from browser to redirect for
shortly.”“How do you handle a spike of 100k slug creations in one minute?”
“What happens when Kafka is down? Redis? The ML classifier?”
“How does a new engineer add a
link.expired.v1event? Walk me through the PR.”“How do you roll back the categorizer to v1 without a code change?”
“Show me what happens when the URL of a well-known slug changes.”
Practice this out loud. Record yourself. If any answer is longer than 5 minutes or shorter than 90 seconds, iterate.
Estimated effort: 60–100 hours over 6–8 weeks.
Portfolio Packaging — What Recruiters Will See¶
By month 13:
One repo (
shortly-platform) — 6 services, ~15k lines of Java, comprehensive tests, Helm charts, ArgoCD manifests, Grafana dashboardsOne ML repo (
categorizer) — training notebook + Java serving + Kubernetes deployOne event pipeline repo (
event-pipeline) — Kafka reference implementation with idempotent sinkTwo-to-three blog posts — JPA vs jOOQ (Phase 07), Spring AI vs LangChain4j hands-on comparison, “how I deployed a Java ML service to Kubernetes”
A 5-minute video — recruiters will watch this if it exists and skip your README if it doesn’t
CV bullet:
“Designed and deployed a 6-service microservice platform (Spring Boot 3.4, Kafka, Redis, Postgres, ONNX Runtime, Kubernetes) with JWT auth, idempotent event processing, RFC 7807 error handling, distributed tracing, and GitOps deploys. Auto-tags URLs via an in-process ONNX classifier trained in PyTorch. p95 redirect latency 25ms, p95 classification 60ms, tested under 1000 RPS. [github link] [live demo link].”
That bullet, backed by the artefact, is the M13 pitch made real. You can debug the 15-year-old monolith. You can refactor toward modern patterns. You can ship the new service that survives the next 10 years. From OutOfMemoryError to deployment — you’ve done all of it, and there’s a URL to prove it.
Return to README.md · Previous: 07_case_studies_and_war_stories.md · Next: end of roadmap — begin your job search