A persisted reading list separates validated in-memory membership from the file snapshot that survives a process restart.
Java reading-list project: validated titles and explicit file persistence
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
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
[Concurrency notes, Collections handbook]
[Collections handbook]
saved=2Costs 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.
