Intermediate Java projects connect collections, files, structured classes, and tests into an application that preserves useful state. A task tracker, a text-frequency report, and an appointment planner are manageable choices. The challenge is not adding every possible feature; it is making storage, validation, and domain behavior agree. Each prototype below implements a real operation, and the surrounding acceptance criteria explain how to grow it into a complete project without turning it into one untestable main method.
Project one: a persistent task tracker
Represent tasks with an identifier, title, and completion state. Begin with in-memory add, complete, and list operations. Require unique IDs, reject blank titles, and define what completing an unknown task means. Add file persistence only after those operations are tested. Acceptance criteria include saving and reloading equivalent task state, preserving titles containing supported delimiters, and reporting a malformed storage record instead of silently creating a different task.
Treat storage as a separate boundary
The file prototype writes UTF-8 text and reads it back through Path and Files. Its simple format is suitable only for constrained demonstration labels. A complete tracker should choose CSV with proper quoting, JSON with a declared library, or another documented format. Keep the repository responsible for reading and writing while a service owns task rules. Handle missing files, invalid records, and write failures explicitly; a failed save must not be announced as successful.
Project two: create a word-frequency report
Normalize text under a documented rule, split it into tokens, and count each token in a map. The example uses lowercase with Locale.ROOT and whitespace splitting. That is a deliberately limited tokenizer: punctuation remains attached unless you add a rule for it. Acceptance tests should cover repeated words, case differences, empty input, and punctuation according to your selected policy. Print a deterministic report by sorting keys or choosing a documented order.
Project three: plan nonoverlapping appointments
Represent a slot with LocalTime start and end and require end after start. Two half-open slots overlap when each starts before the other ends. Under this convention, one slot ending exactly when another starts is allowed. Write acceptance tests for separated, adjacent, partially overlapping, and containing slots. A real planner also needs a date, time zone policy, and a decision about overnight appointments; do not pretend LocalTime alone identifies a worldwide event.
Design small classes around clear contracts
Use a record for an immutable data carrier when its components fit that role, a service for operations, and an interface for storage if multiple implementations are useful. Do not introduce interfaces solely to inflate the file count. Keep constructors responsible for valid starting state. Collection-returning methods should document whether callers may modify the result. Tests can then exercise the domain without actual disk access except in focused storage integration tests.
Deliver a reproducible project
Use Maven for dependencies and build commands, JUnit for behavior checks, and Git for reviewable changes. Include sample data without personal information and explain the storage format. A complete intermediate project should recover predictably from an invalid command and preserve valid data across restart. Demonstrate those requirements in the README rather than presenting only the easiest happy-path screenshot.
Practice tasks and checks
Choose one project and write a short data contract before coding extensions. For the task tracker, define identifiers, supported titles, completion behavior, and malformed-file handling. For the word report, define normalization, punctuation, and output ordering. For the planner, define interval boundaries and overnight restrictions. Add at least one test for a successful operation, one boundary, and one invalid input. Then place persistence or parsing behind a dedicated boundary so the core rule can be tested without a disk. Finally, demonstrate restart or repeat processing with the same input and compare the expected state. A project is ready when its contracts survive those realistic cases, not merely when its first demonstration prints correctly.
Java Summary
- Define data formats, normalization rules, and interval boundaries explicitly.
- Test persistence as a state round trip, not only file existence.
- Separate domain behavior from storage and presentation.
- Continue with advanced projects when these examples are clear.
Java Examples
Each example shows the code, the output it produces and what each part does.
Write and reload constrained task text
Save this program as TaskFilePrototype.java. Compile it with javac TaskFilePrototype.java and run it with java TaskFilePrototype.
Expected output:
The temporary file isolates the demonstration from a user's real task data. Writing two lines and reading them back yields two records. The finally block removes the fixture even if reading fails. This is a storage round-trip prototype, not a complete parser: titles containing the delimiter need an escaping or structured-format policy before you use this representation for arbitrary input.
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class TaskFilePrototype {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("tasks-", ".txt");
try {
Files.write(file, List.of("T1|Review notes", "T2|Pack books"), StandardCharsets.UTF_8);
System.out.println(Files.readAllLines(file, StandardCharsets.UTF_8).size());
} finally {
Files.deleteIfExists(file);
}
}
}2Count normalized words
Save this program as WordReportPrototype.java. Compile it with javac WordReportPrototype.java and run it with java WordReportPrototype.
Expected output:
Lowercasing under Locale.ROOT gives a predictable language-neutral normalization for these tokens. The whitespace pattern separates three words, and merge creates or increments each count. Selected-key output avoids depending on HashMap iteration order. A complete report should handle empty normalized input before splitting and define punctuation rules. Reading a large file may call for line-by-line processing instead of loading all text at once.
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class WordReportPrototype {
public static void main(String[] args) {
String text = "River road river";
Map<String, Integer> counts = new HashMap<>();
for (String word : text.toLowerCase(Locale.ROOT).split("\\s+")) {
counts.merge(word, 1, Integer::sum);
}
System.out.println(counts.get("river"));
System.out.println(counts.get("road"));
}
}2
1Detect overlapping appointment slots
Save this program as SlotPlannerPrototype.java. Compile it with javac SlotPlannerPrototype.java and run it with java SlotPlannerPrototype.
Expected output:
The first comparison is adjacent rather than overlapping because the second slot begins exactly at the first end. The strict before comparisons implement half-open intervals. The second comparison shares time from nine-thirty to ten, so it returns true. Constructor validation prevents reversed or zero-length slots. This prototype does not support overnight spans, an intentional restriction that should be stated in the complete project's input contract.
import java.time.LocalTime;
public class SlotPlannerPrototype {
record Slot(LocalTime start, LocalTime end) {
Slot {
if (!end.isAfter(start)) throw new IllegalArgumentException("Invalid slot");
}
boolean overlaps(Slot other) {
return start.isBefore(other.end) && other.start.isBefore(end);
}
}
public static void main(String[] args) {
Slot first = new Slot(LocalTime.of(9, 0), LocalTime.of(10, 0));
System.out.println(first.overlaps(new Slot(LocalTime.of(10, 0), LocalTime.of(11, 0))));
System.out.println(first.overlaps(new Slot(LocalTime.of(9, 30), LocalTime.of(10, 30))));
}
}false
trueJava FAQ
Should I add a database immediately?
Not unless the project's requirements need it. A clear file format and repository boundary can teach persistence without introducing connection setup. Add a database when you are ready to learn its transactions, queries, and failure modes deliberately.
How many classes should an intermediate project have?
There is no useful fixed count. Separate meaningful responsibilities such as domain rules, storage, and presentation. Avoid splitting every statement into a class or placing unrelated responsibilities in one giant method.
What makes a Java project intermediate rather than beginner?
Several cooperating classes with clear responsibilities, data that survives between runs, automated tests and a build tool. A beginner project shows you can write correct logic; an intermediate one shows you can organise it, persist it and prove it works.
How can a project store data without a database?
Use files: plain text or CSV with Files.readAllLines and Files.write, or JSON with a library such as Jackson for structured objects. Put all file access behind one storage class, so you could later swap in a database without changing the rest of the program.
Which tests should an intermediate project include?
Unit tests for the domain rules, such as overlapping appointments or word counting, including boundary cases. Add a test that writes data to a temporary folder and reads it back, which catches format mistakes in the storage code. JUnit's @TempDir provides a clean folder for each test.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Why does the file prototype use finally?
Why use Locale.ROOT in the word report?
Java Quiz
2 questions to check what you learned.
Do adjacent half-open slots overlap?
- No
- Always
- Only if their names match
What should a storage round-trip test check?
- Reloaded state matches the supported original state
- Only that a file exists
- Only the source line count