Skip to content
SparkTime, home
Java From Zero to Job-Ready
Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
12 · Interviews and Readiness
Lesson 39 of 41

Lesson 39 · Secure, Tested, Production-Ready

Project 3: The Job Application Tracker API, Assembled and Reviewed

Put the whole backend together: the final architecture, the requirements checklist, how each article contributed, how to build, run and test it, an honest list of limitations, and the questions an interviewer is likely to ask about it.

On this page

Key takeaways

  • The final application is a layered Spring Boot API with controllers, services and Spring Data repositories, PostgreSQL schema management with Flyway, JPA relationships, JWT login with bcrypt passwords, per-user data access, consistent problem-details errors, health checks and a Dockerfile.
  • It has 125 automated tests (unit, slice and integration, most database tests on a real PostgreSQL) with 96% line coverage, and the main protections were checked with a mutation experiment.
  • A project is portfolio-ready when you can build and run it from a clean clone, explain every design decision, and state its limits honestly.
  • Interviewers ask about the decisions: why DTOs, why the owner is in every query, why optimistic locking, how N+1 was found, how secrets are handled and what you would improve.

Over the last articles you built one application a piece at a time. This article assembles it, checks it against its requirements, and prepares you to explain it. It is the third project of the course, after the Expense Tracker CLI and the PostgreSQL version.

What you will learn

#
  • The final architecture and where each concept lives.
  • How to build, run and test the project from scratch.
  • A requirements checklist, an honest limitations list, and extension ideas.
  • How to present the project in an interview.

What it does

#

The Job Application Tracker API lets a registered user keep track of the jobs they are pursuing: create applications, move them through the stages (WISHLIST, APPLIED, INTERVIEWING, OFFER, REJECTED, WITHDRAWN), record interviews, filter and page the list, and edit safely when several clients are involved. Every user sees only their own data.

Architecture

#
Code
HTTP request   |   vSpring Security filter chain   JWT verification, deny by default, CORS, security headers   |   vControllers                    AuthController, JobApplicationController (HTTP only, DTO records)   |   vServices                       AuthService, TokenService, JobApplicationService (rules, transactions)   |   vSpring Data repositories       JobApplicationRepository, AppUserRepository (every query scoped to the owner)   |   vHibernate / JPA entities       JobApplication, Interview, AppUser   (Flyway owns the schema)   |   vPostgreSQL

Layer or concern

Main classes

Introduced in

Application and configuration

JobTrackerApplication, TrackerSettings, SecuritySettings, ClockConfig

32, 33

Domain

JobApplication, Interview, ApplicationStatus, AppUser

33 to 36

Persistence

JobApplicationRepository, AppUserRepository, migrations V1 to V3

34, 35

Business rules

JobApplicationService, AuthService

33, 36

HTTP API and DTOs

JobApplicationController, AuthController, request and response records, PageResponse

30, 33

Error handling

ApiExceptionHandler and the domain exceptions

33

Security

SecurityConfig, JwtConfig, TokenService

36

Operations

Actuator, Dockerfile, compose.yaml

38

Tests

125 tests in 16 files

25, 37

The project has 35 main source and resource files and 16 test files. The complete code is in the article that introduced it (the diffs show each change), and the last snapshot is the sum of articles 33 to 38.

The API

#

Endpoint

Purpose

POST /api/auth/register, POST /api/auth/login

Create an account, and get an access token

POST /api/applications

Create an application (201 with Location)

GET /api/applications

List with status, company, page, size

GET, PUT, DELETE /api/applications/{id}

Read, replace (with the optimistic-locking version), delete

POST /api/applications/{id}/status

Move to another stage (409 if not allowed)

POST, GET /api/applications/{id}/interviews, DELETE .../{interviewId}

Manage interviews

GET /actuator/health

Liveness and readiness

Build, run and test

#

You need JDK 21 or newer, Maven 3.9 and a PostgreSQL 18 server with an empty database (the Docker route is described in article 38 and was not verified here). Create the database, then supply the configuration through the environment, never through a file you commit:

Shell
export DB_URL=jdbc:postgresql://localhost:5432/jobtrackerexport DB_USER=jobtrackerexport DB_PASSWORD='your-database-password'export JWT_SECRET="$(head -c 32 /dev/urandom | base64)"mvn clean verifyjava -jar target/jobtracker-0.0.1-SNAPSHOT.jar

mvn clean verify runs the 125 tests (they start their own PostgreSQL through the embedded test server, so they do not need yours) and builds the jar. Flyway creates the tables when the application starts. If you forget JWT_SECRET, the application refuses to start and tells you which variable to set.

What was verified for this course: the full test suite passes on JDK 21 and JDK 25, and the HTTP sessions in articles 33 to 38 were produced by the packaged jar on both JDKs (identical output) against a real PostgreSQL 18.6.

Requirements checklist

#

Requirement

Evidence

Users register and log in; passwords are hashed

AuthService, SecurityIntegrationTest.thePasswordIsStoredOnlyAsABcryptHash

Every protected endpoint needs a valid token; deny by default

SecurityRulesTest

A user can reach only their own applications

findByIdAndOwnerId, SecurityIntegrationTest.aUserCannotSeeAnotherUsers..., the mutation experiment

Stage changes follow the rules

ApplicationStatusTest (all 36 moves), 409 in the controller tests

Input is validated; errors are consistent problem-details JSON

Bean Validation on the DTOs, ApiExceptionHandler, controller tests

Lists are filtered and paged in the database, with a maximum page size

search query, JobApplicationRepositoryTest

Interviews belong to applications and are removed with them

RelationshipTest, JobApplicationServiceIntegrationTest

No N+1 on the list endpoint

@BatchSize, RelationshipTest statement counts

Concurrent edits cannot overwrite each other silently

@Version, StaleVersionException, TransactionTest, integration test

The schema is managed by migrations

Flyway V1 to V3; Hibernate set to validate

Secrets are not in the code; weak or missing secrets stop start-up

JwtConfig, TokenServiceTest, the failures shown in article 36

Health checks without leaking details

Actuator configuration, output in article 38

Automated tests at several levels

125 tests, 96% line coverage

Honest limitations

#

Limitation

What a production system adds

No refresh tokens, no token revocation, no login rate limiting

Refresh tokens, a deny-list or short lifetimes, throttling

No password reset, email verification or MFA

Those features, each with its own security review

Offset paging

Cursor paging for very large lists

list nests interviews

A count in lists and a separate call for the details

Cascading delete issues one statement per interview

A bulk delete for large sets

The Dockerfile and Compose file were not run here

Build and test them, scan the image, pin the versions

The migration V3 assumes an empty job_applications table

Add the column nullable, backfill, then make it NOT NULL

One application instance was tested

Testing with several instances, and a shared rate limiter

No end-to-end browser tests, no performance tests

Load tests and a front-end suite

Extension ideas

#
  1. A GET /api/applications/summary endpoint with counts per stage, computed in SQL.
  2. Soft delete and an audit trail of status changes (a new table and a migration).
  3. Notes and attachments per application.
  4. Login throttling and a refresh-token flow.
  5. A scheduled reminder for interviews coming up, with a test using the injected Clock.
  6. A Testcontainers version of the database tests, if you have Docker.

Presenting it in an interview

#

Be ready to explain, in your own words and with the code open:

  • Why DTOs and not entities in the controllers (mass assignment, stable contracts).
  • Where each rule lives and why: validation at the edge, business rules in the service, constraints in the database.
  • How ownership is enforced and why other users get 404.
  • How you found and fixed the lower(bytea) bug, the N+1 problem and the lagging-version bug, because real problems with real fixes are the strongest evidence of experience.
  • What @Transactional does, and the self-invocation and checked-exception traps.
  • Why optimistic locking, and what the client must send.
  • How passwords and secrets are handled, and what a JWT does and does not protect.
  • How you tested it, what the coverage number means, and what the mutation experiment showed.
  • What you would do next, from the limitations table.

Article 41 turns these into prepared answers.

Self-review checklist

#
  • Can someone build and run it from a clean clone using only your README?
  • Is anything secret in the repository or its history?
  • Does every endpoint have a test for success, invalid input and a missing resource?
  • Do you know the generated SQL for each endpoint and the number of statements?
  • Can you explain every annotation in the entities and the security configuration?
  • Is the README honest about what is missing?

Common mistakes in portfolio projects

#
  • A project nobody can run, because the README is missing or wrong.
  • Committed secrets or a .env file.
  • No tests, or tests that only check mocks.
  • Entities exposed as the API.
  • Claims you cannot defend: "secure", "scalable", "production-ready", without evidence.
  • Copying a tutorial without understanding it, which is obvious in the first follow-up question.

Exercises

#

Exercise 1

Write the README

#

List the sections of a README for this project that would let a stranger run it in ten minutes.

Exercise 2: Add an endpoint

#

Outline, without code, the files you would change to add GET /api/applications/summary (counts per status for the caller), including the tests.

Exercise 3: Defend a decision

#

An interviewer asks: "Why return 404 and not 403 for another user's application?" Answer in three sentences.

Exercise 4: Find the weakest part

#

Pick the weakest part of this project from the limitations table and describe how you would fix it first, and why.

Exercise 5: What would break?

#

You deploy two instances behind a load balancer. Which parts of the design still work, and which need attention?

Show solutionHide solution

What the project is, requirements (JDK, Maven, PostgreSQL), how to configure it (the environment variables, with how to generate the secret), how to build and test (mvn clean verify), how to run it, a short API table with example requests, the architecture in a few lines, the test strategy and results, the known limitations, and the licence and contact. Keep the commands copy-pasteable and test them on a clean machine.

Exercise 2

Show solutionHide solution

A repository method with a GROUP BY status query scoped to the owner (a projection record for the result), a service method that calls it and fills in zero for missing statuses, a controller method @GetMapping("/summary") taking the token, a response record, and tests at each level: a repository test on the real database, a service unit test, a controller test for the shape, and an integration test that another user's data is not counted. Check that the literal path /summary is not captured by /{id}.

Exercise 3

Show solutionHide solution

A 403 would confirm that the application exists, which leaks information and lets someone enumerate ids. 404 gives the same answer whether it does not exist or belongs to someone else, so nothing is revealed. The invariant I protect is that another user's data is never returned, and this response keeps it.

Exercise 4

Show solutionHide solution

A good first choice is login throttling plus refresh tokens, because authentication endpoints are the most attacked surface and the current tokens cannot be revoked. I would add per-account and per-address rate limiting with a test, then a short access token with a refresh token stored hashed in the database, so a stolen token has a small window. The reasoning is risk: it protects every user, and the other limitations are about scale or convenience.

Exercise 5

Show solutionHide solution

Works unchanged: the JWT approach is stateless, so any instance can verify any token, and the database handles concurrent writes with transactions and optimistic locking. Needs attention: Flyway running on every instance start (it takes a lock, but you may prefer a separate migration step), rate limiting that must be shared (an in-memory counter is per instance), the connection pool size multiplied by the number of instances against the database limit, and secrets that must be identical across instances (the signing secret).

Where to go next

#

Sources & further reading

  1. docs.spring.io
  2. docs.spring.io
  3. docs.spring.io

Connected lessons

Linked from 1 guide

Other pages in the library that point here.

  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Technology

    Dynamic Programming Practice: From 1D to 2D

  • Course · Technology

    DSA From Zero to Job-Ready: A Practical Guide with Java