Phase 07 Projects — Ship Something Real¶
Reading about Spring Boot is not the same as running a service at 3 a.m. when Postgres won’t accept connections. These three projects convert the theory in files 01–07 into muscle memory: one green-field build, one hardening pass, one honest engineering write-up. By the end you have a public URL that returns JSON, a Grafana dashboard that renders your own metrics, and a blog post that shows you can reason about trade-offs.
Treat these as portfolio-grade. Real repo, real README, real commits, real deployment. If a recruiter clones it and it doesn’t run with docker compose up, you’ve failed the exercise before the code review.
Project A — shortly: Production-Grade URL Shortener¶
The canonical “small enough to ship, big enough to prove you know Spring.” You’ve seen a dozen tutorials for this. Yours will be different because it will have real persistence, real auth, real observability, real tests, and a real public URL.
Functional Requirements¶
Endpoint |
Method |
Auth |
Purpose |
|---|---|---|---|
|
POST |
JWT |
Create a short link (custom slug optional) |
|
GET |
Public |
Metadata (owner, created, hits) |
|
DELETE |
JWT (owner) |
Remove a link |
|
GET |
JWT |
Paginated list of caller’s links |
|
GET |
Public |
301 redirect + async hit counter |
|
POST |
Public |
Create account |
|
POST |
Public |
Exchange credentials for JWT |
|
GET |
Public |
Liveness + readiness |
|
GET |
Internal |
Scrape endpoint |
Stack (non-negotiable)¶
Java 21, Spring Boot 3.4.x
PostgreSQL 16 (JPA + Flyway migrations)
Redis 7 (slug → URL cache, TTL 1 hour)
Spring Security 6 with self-issued JWT (RSA keypair, 15-min access + 7-day refresh)
Micrometer + Prometheus + Grafana
OpenTelemetry OTLP export to Tempo/Jaeger
Testcontainers for every integration test (NO H2)
Docker multi-stage build with Application CDS
Deployed to Fly.io (free tier) or Render
Architectural Rules¶
DTOs are records, entities are classes. Never leak
@Entitythrough the controller boundary.Constructor injection only. No
@Autowiredon fields.All errors return RFC 7807
ProblemDetail. Wire it in@RestControllerAdvice.Redirect must be fast. Cache-aside via Redis; only touch Postgres on cache miss. Increment hit counter asynchronously (via
@Asyncor a queue — do NOT block the redirect on a DB write).Rate limits from day one. Even if simple (Bucket4j in-memory), enforce 100 req/min for anonymous, 1000/min for authenticated.
Idempotent POST /links. Support
Idempotency-Keyheader (Redis TTL 24h stores the response).
Test Matrix¶
Layer |
Framework |
Slice |
|---|---|---|
Unit |
JUnit 5 + Mockito + AssertJ |
Service classes with mocked repos, |
Web slice |
|
Controllers, |
Data slice |
|
Repositories, native queries |
Full slice |
|
Happy paths only, ≤10 tests |
Load |
k6 script in |
|
Aim: ≥80% line coverage, ≥65% Pitest mutation coverage. Coverage below that and you’re gaming the number.
Observability Deliverables¶
Custom counter:
shortly_links_created_total{tier="free|paid"}Custom timer:
shortly_redirect_duration_seconds(with percentile histogram enabled)Grafana dashboard JSON checked into
/ops/grafana/Alert rules (Prometheus): error rate >1% for 5m, p95 redirect >100ms for 10m, heap >80% for 10m, HikariCP saturation
Deployment Checklist¶
Dockerfile is multi-stage (JDK build, JRE runtime, non-root user, tini as PID 1 or
JAVA_TOOL_OPTIONS)Application CDS archive baked into the image (
-XX:ArchiveClassesAtExit=/app/app.jsathen run with-XX:SharedArchiveFile=/app/app.jsa)docker-compose.ymlboots the whole world locally:shortly,postgres,redis,prometheus,grafana,tempo,lokiPublic URL live on Fly.io / Render, TLS on, custom health checks configured
README.mdhas: 1-command local start, screenshot of Grafana dashboard, curl examples,openapi.jsonlink
⚠️ What most people get wrong: they skip deployment because “it works locally.” The moment you push to a real host, you discover your app doesn’t handle
SIGTERMcleanly, HikariCP times out because Fly.io firewalled Postgres 5432, and Prometheus can’t scrape because you never exposed/actuator/prometheusin themanagement.endpoints.web.exposure.include. Deployment IS the learning.
Estimated effort: 40–60 hours over 4–5 weeks.
Project B — Harden shortly¶
Ship the naive version first. Then come back and turn it into something you’d defend in a design review. This is the difference between “I built a URL shortener” and “I ran a URL shortener in production.”
Additions¶
Feature |
Why it matters |
Implementation hint |
|---|---|---|
Distributed rate limiting |
In-memory Bucket4j resets on redeploy and doesn’t scale horizontally |
Bucket4j + |
Admin dashboard |
Real services need an ops UI, not just Postgres shell |
Simple Thymeleaf or React SPA at |
Soft delete + restore |
Real users delete things by mistake |
|
Audit log |
Every mutation must be traceable |
Separate |
Abuse detection |
Shorteners are phishing magnets |
Google Safe Browsing API check on create (or the URLhaus feed for a free option), auto-quarantine + 451 response for known-bad targets |
Password reset flow |
|
Store token hash (not plaintext), 1-hour TTL, invalidate on use |
Testing Additions¶
Contract test with WireMock for the Safe Browsing dependency (so CI doesn’t hammer Google)
Chaos test: use Testcontainers to kill Redis mid-test, assert cache-miss path still returns and no requests fail
Integration test for the audit log: mutate an entity, assert an
audit_logrow with correct actor + action
What You’ll Learn That You Can’t Get From a Tutorial¶
Distributed rate limiting is a distributed systems problem — clock skew, race conditions on the counter, and what happens when Redis is down (fail open or fail closed?)
Soft delete looks trivial until you realize every JPQL query has to know about it, and unique constraints (like
slug) now need partial indexesAudit logs balloon fast; you’ll need a rotation strategy from day one
Estimated effort: 25–35 hours over 3 weeks.
Project C — The “JPA → jOOQ” Blog Post¶
The technical portion is small. The value is in the writing and the honesty. This is a public engineering artefact — a real blog post you publish (dev.to, Hashnode, GitHub Pages, Medium, LinkedIn — pick one and commit).
Setup¶
Pick 3–5 of the heaviest reads in shortly (typically: paginated link list with owner filter, top-N by hit count, per-user analytics rollup). Reimplement each in jOOQ 3.19+ (Java 21 codegen). Keep the JPA version alongside; feature-flag which one serves.
Deliverables¶
Benchmark harness — JMH microbenchmark AND a JMeter/k6 macro test hitting real HTTP endpoints backed by both stacks. Report:
p50, p95, p99 latency for each query
Rows/sec throughput at 100 concurrent requests
JVM heap and GC pause frequency
EXPLAIN ANALYZEoutput for the SQL each layer produces
The write-up (target ~2000 words):
What jOOQ actually gives you (type-safe SQL, no N+1 by construction, better projections, cleaner batch DML)
What it costs (codegen step in the build, no free change-tracking, no cascade, more boilerplate for simple CRUD)
When the numbers matter and when they don’t (the honest answer is: for 90% of endpoints they don’t)
A recommended hybrid pattern: JPA for command-side / simple CRUD, jOOQ for query-side / reporting
Migration cost estimate for a real team
Publish it. Post the link on LinkedIn with a short summary. This becomes an study talking point for the next 12 months.
⚠️ What most people get wrong: they conclude “jOOQ is faster, use it everywhere” from a microbenchmark that measured 200µs vs 400µs on a query that runs 50 times a day. Learn to say “the difference is real but does not matter here, and here’s how I decided.” That sentence, delivered confidently in an study, is worth more than the whole benchmark.
Estimated effort: 15–25 hours over 2 weeks.
Portfolio Packaging¶
At the end of Phase 07 you should have:
One repo:
github.com/<you>/shortly— full source, one-command local start,README.mdwith screenshots and public URLOne blog post: JPA → jOOQ, with real numbers
One CV bullet you can defend line-by-line: “Built and deployed a production-grade URL shortener (Spring Boot 3.4, Postgres, Redis, JWT auth) with Prometheus/Grafana observability, OpenTelemetry tracing, Testcontainers integration tests, and rate limiting. Serves at [public URL].”
That single bullet, plus the ability to whiteboard how it works end-to-end, will pass the “can you build a real service” bar at most MNCs.
Return to README.md · Previous: 07_config_and_deployment.md · Next: ../08_distributed_systems_applied_integration/README.md