You've spent eight chapters learning the parts. This chapter is where the parts come together. The capstone is a real, runnable Java application — not a toy snippet — that exercises classes, inheritance, interfaces, collections, file I/O, JSON parsing, exception handling, lambdas, and streams in one codebase. By the end you'll have something you can show to a hiring manager, paste on a CV, or hand to a junior who joins the team. This first lesson sets the scope and the structure; lesson 2 walks you through the implementation; lesson 3 is your self-review.
What you're building
QA DataManager — a command-line utility that manages test data fixtures for a QA team. The kind of utility every test framework needs once you scale beyond a handful of tests:
- Reads JSON fixture files (
users.json,products.json) and deserialises them into typed Java objects. - Processes the data — list all, filter by field, sort by field, search by ID.
- Generates new randomised entries (with realistic-looking names and emails) for tests that need fresh data.
- Writes the processed or generated data back out as JSON or CSV.
- Logs every operation with a timestamp to an
operations.logfile. - Handles errors gracefully — missing files become a friendly error, malformed JSON triggers a domain-specific exception, invalid input is rejected loudly at the boundary.
This isn't fictional homework. Real QA teams build exactly this kind of tool. Test fixture management, test data generation for parameterised runs, sanitising prod-like data into fixture files — every shop has a handful of these scripts in some half-maintained corner of their repo. Yours will be cleaner.
Why this scope
The brief is deliberately chosen to force every concept from the course to do real work:
- Classes & objects (Ch 4):
TestUser,Product, plus aTestDatainterface and aDataManagerabstract base class. - Inheritance & polymorphism (Ch 4 & 5):
UserManagerandProductManagerboth extendDataManager. TheMainclass holds them asDataManagerreferences and dispatches polymorphically. - Interfaces (Ch 5):
TestDatais the cross-cutting capability "this thing has an ID and a one-line summary." - Collections (Ch 6):
List<TestUser>for fixtures,Map<String, String>for config,HashSet<String>to dedupe generated emails. - Exceptions & file I/O (Ch 7):
try-with-resourcesfor files, a customTestDataExceptionfor fixture problems,IllegalArgumentExceptionfor input validation. - JSON (Ch 7): Jackson
ObjectMapperfor round-tripping JSON. - Strings, regex, streams (Ch 8):
String.formatfor log lines, regex for email validation, stream pipelines for filtering and sorting.
If you can build the brief end-to-end, you've used every Java skill QA hiring managers ask about. That's the actual goal.
What success looks like
A working Main you can run from the command line:
$ mvn package
$ java -jar target/qa-datamanager-1.0.jar
QA DataManager — pick an operation:
1. List users
2. Filter users by role
3. Generate N random users
4. Sort users by name
5. Find user by id
6. Export users to CSV
7. List products
8. Filter products by category
9. Quit
> 1
[2026-05-06 09:30:01] LIST users (n=4)
1: Alice (alice@test.com) [admin]
2: Bob (bob@y.com) [member]
3: Carol (carol@z.com) [admin]
4: Dave (dave@x.com) [guest]Every menu choice exercises a different code path. The output is human-readable. Errors don't crash — they print a clear message and re-show the menu. The JSON fixtures and the operations log live in data/ and output/ next to the jar.
Project structure
A standard Maven layout. Put each class in the package that matches its role:
qa-datamanager/
├── pom.xml # Maven build, Jackson dep
├── data/
│ ├── users.json # input fixture
│ └── products.json # input fixture
├── output/
│ ├── operations.log # written by the Logger
│ ├── users.cleaned.json # generated/exported data
│ └── users.csv # CSV export
└── src/
└── main/
└── java/
├── Main.java # menu loop, wires managers together
├── model/
│ ├── TestData.java # interface — getId(), toSummary()
│ ├── TestUser.java # implements TestData
│ └── Product.java # implements TestData
├── manager/
│ ├── DataManager.java # abstract base
│ ├── UserManager.java # extends DataManager
│ └── ProductManager.java # extends DataManager
├── generator/
│ └── DataGenerator.java # random users / products
├── util/
│ ├── FileHelper.java # read/write JSON, ensure dirs
│ └── Logger.java # timestamped operations.log
└── exception/
└── TestDataException.java # custom unchecked exception
Don't get hung up on the package depth — flatten it if you want, or keep Main at the root. The point is to separate models, managers, utilities, and infrastructure code so each file has one clear job.
Sample fixtures
Start with these. Save them in data/ and check in any time you make changes you want to keep.
data/users.json:
[
{ "id": "u1", "name": "Alice", "email": "alice@test.com", "role": "admin", "active": true },
{ "id": "u2", "name": "Bob", "email": "bob@y.com", "role": "member", "active": true },
{ "id": "u3", "name": "Carol", "email": "carol@z.com", "role": "admin", "active": false },
{ "id": "u4", "name": "Dave", "email": "dave@x.com", "role": "guest", "active": true }
]data/products.json:
[
{ "id": "p1", "name": "Pro Plan", "category": "subscription", "priceCents": 4900 },
{ "id": "p2", "name": "Team Add-on", "category": "addon", "priceCents": 1500 },
{ "id": "p3", "name": "Free Plan", "category": "subscription", "priceCents": 0 },
{ "id": "p4", "name": "Support Pack", "category": "service", "priceCents": 9900 }
]The structures are intentionally simple — enough fields to make filtering and sorting interesting, not so many that you'll spend half the project on getter/setter ceremony.
Operations to support
A non-exhaustive list of the operations your menu should drive. Build the easy ones first; some of these turn into stretch goals in lesson 3.
| # | Operation | Skills exercised |
|---|---|---|
| 1 | List users / products | basic iteration, polymorphism via DataManager.listAll() |
| 2 | Filter by role / category | Predicate<T>, Stream .filter |
| 3 | Sort by name / price | Comparator, Stream .sorted |
| 4 | Find by ID | Stream .findFirst(), Optional |
| 5 | Generate N random users | DataGenerator, HashSet for unique emails |
| 6 | Export to CSV | StringBuilder, String.join, FileWriter |
| 7 | Save filtered fixture as JSON | ObjectMapper.writerWithDefaultPrettyPrinter() |
| 8 | Show operation log tail | BufferedReader line streaming |
Each row maps to one or two lessons from earlier in the course. If you find yourself stuck on row N, that's the chapter to revisit before lesson 2 of this capstone.
The end-to-end flow
That's the whole utility on one page. Read the arrows left to right: input JSON in, manager loads it, stream pipeline transforms it, output writer drops the result on disk, logger records every operation. Nothing more, nothing less.
Setup checklist before you start lesson 2
Before you open lesson 2, get the project skeleton ready. Don't write any logic yet — that's lesson 2's job. Just lay the rails:
- Create the folder.
mkdir qa-datamanager && cd qa-datamanager. Inside, createdata/andoutput/folders, plus the standardsrc/main/java/...Maven layout. - Add a
pom.xmlwith agroupId,artifactId(qa-datamanager),version(1.0), Java 17 or 21 in<maven.compiler.source>and<target>, and the Jacksonjackson-databinddependency from chapter 7 lesson 4. - Open the project in IntelliJ. File → Open →
pom.xml. Confirm IntelliJ resolves Jackson — the import in a stub file should turn green. - Drop in the sample fixtures. Paste
users.jsonandproducts.jsonfrom above into thedata/folder. - Stub out empty classes in the right packages — even just
class TestUser {}. This lets you see the structure before you fill in the bodies and gives IntelliJ something to reason about for autocompletion. - Confirm
mvn packagebuilds an empty jar. It will say nothing was compiled, which is fine — you've validated the pipeline runs.
The setup itself is a useful 30-minute exercise. If mvn package fails, that's a Maven or JDK problem to fix before you start writing logic. Lesson 2 assumes the build works.
Project work
Spend 30-45 minutes laying the foundation:
- Set up the Maven project as above (folder layout,
pom.xmlwith Jackson). - Drop in both
users.jsonandproducts.jsonexactly as shown. - Create empty stubs for every file in the structure tree —
TestUser.java,TestData.java,DataManager.java, etc. Each can be a one-linepublic class X {}for now. - Verify the project compiles:
mvn package(or build via IntelliJ). Fix any path or pom issues now, with stubs, before you've written real logic. - Open
users.jsonin the JSON Formatter on qa.codes to confirm it's valid. Do the same forproducts.json. Treat valid JSON as a precondition; debugging Jackson errors with malformed input is much harder than fixing the JSON first. - Sketch the
DataManagerabstraction on paper or in a comment inDataManager.java: which methods should be abstract? Which concrete? Which should befinal? Lesson 2 will give you the answer; trying it yourself first makes the answer stick. - Open the empty
Main.javaand write — as a comment, not code — the menu items you'll support. Your menu doesn't have to match the table above exactly; it has to make sense to you.
When the empty project compiles, the JSON validates, and your design notes feel right, you're ready for lesson 2 — the implementation walkthrough.