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 inferences

  • Batching 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 counter model_prediction_class_total{class}

  • OpenTelemetry traces (span per phase: preprocess, infer, postprocess)

  • /actuator/health/readiness includes model-loaded check

Deployment

  • Multi-stage Dockerfile: build with maven:3.9-eclipse-temurin-21, runtime with eclipse-temurin:21-jre-alpine, non-root user

  • Model artefact bundled in the image (versioned filename)

  • Helm chart with values for image tag, resource requests/limits (2Gi memory, 1000m CPU), all three probes

  • Deploy to local kind cluster

  • Prometheus (via Helm) scrapes the service; Grafana dashboard shows p95 inference latency + prediction distribution

  • Load test with k6 at 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 = userId

  • Publishes LinkEvent to link.event.v1

consumer-svc:

  • @KafkaListener with manual ack (from file 02)

  • Deduplicates via event_id UNIQUE 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 MockProducer and consumer with @EmbeddedKafka or Testcontainers Kafka

  • Integration 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 up boots the whole stack (producer, consumer, Kafka, Zookeeper or KRaft, Schema Registry, Postgres, Prometheus, Grafana, Kafka UI)

  • make test runs unit + integration + chaos suites

  • README 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 link.hit.v1, aggregates hits per link per hour, exposes /stats/{slug}

Spring Boot + Kafka Streams

shortly-abuse

Consumes link.created.v1, calls Safe Browsing / URLhaus, marks abuse in Postgres

Spring Boot + Resilience4j

shortly-notifier

Consumes multiple topics, sends email (SES / Mailgun sandbox) and webhooks

Spring Boot

shortly-categorizer

Project A, called synchronously from shortly-api on link creation to auto-tag

Spring Boot + ONNX Runtime

The Non-Negotiables

  1. Every service:

    • Constructor injection only

    • RestControllerAdvice with RFC 7807 ProblemDetail

    • Micrometer + 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

  2. Event flow:

    • link.created.v1, link.updated.v1, link.deleted.v1, link.hit.v1 topics

    • Avro schemas in Schema Registry, BACKWARD compatibility

    • At-least-once + idempotent consumers (event_id UNIQUE)

    • DLT per topic, monitored, alerted

  3. Cache:

    • Redis cache-aside for slug → URL resolution

    • Negative caching for unknown slugs (60s TTL)

    • Cache stampede prevention via single-flight lock

  4. Security:

    • Self-issued JWT (RS256), 15-min access + 7-day refresh

    • @PreAuthorize on admin endpoints (hasAuthority('SCOPE_admin'))

    • Rate limits: 100 req/min anon, 1000 req/min authenticated, 10 req/min for POST /links

    • No secrets in Git; K8s Secrets sourced from Sealed Secrets or External Secrets Operator

  5. Deployment:

    • Local kind cluster

    • One Helm umbrella chart with subcharts per service, or separate charts + ArgoCD Application-of-Applications pattern

    • Namespace shortly-dev, shortly-staging

    • HPA on custom metric (http_server_requests_seconds_count rate) via Prometheus Adapter

    • NetworkPolicy: default-deny, explicit allowlists between services

    • GitOps: ArgoCD watches a gitops/ directory; deploys happen via merged PR

  6. 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-api is tested

  • E2E happy path: run against the deployed kind cluster, hit POST /links → assert Kafka events fire → assert analytics reflects hit → assert abuse check ran

  • Load test: k6 for POST /links (creating shortened URLs) and GET /{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 charts

  • make dev-up starts everything locally in kind + Helm

  • make load-test runs the k6 script and asserts SLOs

  • README 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.v1 event? 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 dashboards

  • One ML repo (categorizer) — training notebook + Java serving + Kubernetes deploy

  • One event pipeline repo (event-pipeline) — Kafka reference implementation with idempotent sink

  • Two-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