Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java reading-list project: validated titles and explicit file persistence

Last updated: 29 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A persisted reading list separates validated in-memory membership from the file snapshot that survives a process restart.

Download Java source kit

Java 8+. This is a complete program using JDK classes.

Define the file format and save boundary

This project stores one nonempty UTF-8 title per line. Titles cannot contain line breaks, and the list is capped at ten thousand entries. The format deliberately has no quoted multiline field grammar. Changing that requirement needs a new parser and compatibility decision.

Adding or removing a title changes only the live list. save creates a staged file and then replaces the named snapshot. Loading parses private list state before returning the restored object, so an invalid file does not expose a half-restored list to callers.

An ordinary move does not claim a crash-durable transaction. A production store needs a tested replacement policy, filesystem capability checks, durability requirements and concurrent-writer coordination. This program is a local single-writer project with no synchronization between processes.

Run the acceptance scenario

The fixture saves two titles, loads a new object, removes one title from the new object and loads the file again. The last count stays two because removal alone is not a save. The temporary directory is cleaned up after the scenario. To retain data in a real CLI, give the application an explicitly owned persistent path instead.

Working program

Java
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
public class ReadingListPersistence {
    final List<String> titles=new ArrayList<>();
    void add(String title){
        if(title==null||title.trim().isEmpty()||title.indexOf('\n')>=0||title.indexOf('\r')>=0)throw new IllegalArgumentException("Single nonempty title required");
        if(titles.size()==10000)throw new IllegalArgumentException("Title budget exhausted");
        titles.add(title.trim());
    }
    List<String> snapshot(){return Collections.unmodifiableList(new ArrayList<>(titles));}
    void remove(int index){titles.remove(index);}
    void save(Path destination) throws Exception {
        Path absolute=destination.toAbsolutePath();Path staged=Files.createTempFile(absolute.getParent(),"reading-stage-",".txt");
        try{
            byte[] snapshot=String.join("\n",titles).getBytes(StandardCharsets.UTF_8);
            if(snapshot.length>2_000_000)throw new IllegalArgumentException("Snapshot budget exhausted");
            Files.write(staged,snapshot);
            Files.move(staged,absolute,StandardCopyOption.REPLACE_EXISTING);
        }finally{Files.deleteIfExists(staged);}
    }
    static ReadingListPersistence load(Path source) throws Exception {
        if(Files.size(source)>2_000_000)throw new IllegalArgumentException("File budget exhausted");
        ReadingListPersistence restored=new ReadingListPersistence();
        for(String title:Files.readAllLines(source,StandardCharsets.UTF_8))restored.add(title);
        return restored;
    }
    public static void main(String[] args) throws Exception {
        Path directory=Files.createTempDirectory("aitrove-reading-");Path file=directory.resolve("titles.txt");
        try{
            ReadingListPersistence original=new ReadingListPersistence();
            original.add("Concurrency notes");original.add("Collections handbook");original.save(file);
            ReadingListPersistence restored=load(file);System.out.println(restored.snapshot());
            restored.remove(0);System.out.println(restored.snapshot());System.out.println("saved="+load(file).snapshot().size());
        }finally{Files.deleteIfExists(file);Files.deleteIfExists(directory);}
    }
}

Output

Output
[Concurrency notes, Collections handbook]
[Collections handbook]
saved=2

Costs and boundaries

Loading visits O(b) input bytes and stores O(n) titles. The size check is suitable for this owned, unchanged fixture; an adversarial concurrently growing file needs a streaming byte limit during reading. Saving writes O(b) content and needs staged-file storage. Concurrent updates and crash recovery remain outside this project’s contract.

Common Mistakes

  • Removing a title does not implicitly persist it.
  • A pre-read file size check is not a streaming limit under concurrent modification.
  • The staged replacement is not advertised as crash durability.

Read next

File operations, Private batch state.

java
reading-list-project
Storage details