File Handling and I/O
Designing a File-Based Repository
You are viewing a free preview lesson.
Lesson Overview
এখন পর্যন্ত আমরা file system-এর individual operations শিখেছি:
Path তৈরি
File read করা
File write করা
Streams ব্যবহার
Resources close করা
Copy, move, delete করা
এবার এগুলোকে application design-এর সঙ্গে connect করব।
ধরুন LiveKlass application-এর একটি Course save করতে হবে।
Business code ideally এমন হওয়া উচিত:
repository.save(
course
);
Business service-এর জানা উচিত নয়:
File কোথায় আছে
File name কী
UTF-8 কীভাবে ব্যবহার হচ্ছে
Properties কীভাবে parse হচ্ছে
Temporary file কীভাবে replace হচ্ছে
এই implementation details repository-এর responsibility।
এই lesson-এ আমরা শিখব:
- Repository abstraction
- File-based repository
- Domain এবং storage responsibility
- File naming strategy
- Serialization এবং deserialization
- Save
- Find
- List
- Delete
- Missing entity handling
- Corrupted file handling
- Exception translation
- Atomic replacement
- File storage-এর limitations
- কখন file repository useful এবং কখন database প্রয়োজন
Learning Objectives
এই lesson শেষে আপনি পারবেন:
- Repository pattern-এর purpose explain করতে
- File storage details business logic থেকে hide করতে
- Domain objectকে text format-এ serialize করতে
- Stored data থেকে domain object reconstruct করতে
- Missing এবং corrupted data distinguish করতে
- Repository-specific exception design করতে
- Atomic replacement ব্যবহার করতে
- File repository-এর concurrency limitations explain করতে
What Is a Repository?
Repository application codeকে storage implementation থেকে isolate করে।
Business layer দেখে:
CourseRepository
Implementation হতে পারে:
FileCourseRepository
PostgresCourseRepository
InMemoryCourseRepository
Service-এর perspective থেকে contract একই থাকতে পারে।
Example:
public interface CourseRepository {
void save(
Course course
);
Course findByCode(
CourseCode courseCode
);
List<Course> findAll();
boolean deleteByCode(
CourseCode courseCode
);
}
Why Use a Repository?
Without repository:
public void publishCourse(
CourseCode code
) throws IOException {
Path path =
Path.of(
"courses",
code + ".properties"
);
Properties properties =
new Properties();
// Read file
// Parse values
// Change status
// Write file
}
Business logic এখন file-system details-এর সঙ্গে tightly coupled।
Better:
Course course =
repository.findByCode(
code
);
course.publish();
repository.save(
course
);
The service focuses on:
Business behavior
Repository focuses on:
Persistence
Repository Does Not Mean Database
Repository একটি abstraction।
Storage হতে পারে:
Memory
File
SQL database
NoSQL database
Remote service
আজ আমরা implement করব:
FileCourseRepository
Domain Model
আমাদের simplified Course persist করবে:
code
title
priceInPaisa
status
CourseStatus.java
package io.liveklass.course;
public enum CourseStatus {
DRAFT,
REVIEW,
PUBLISHED,
ARCHIVED;
public boolean canTransitionTo(
CourseStatus target
) {
if (target == null) {
return false;
}
return switch (this) {
case DRAFT ->
target == REVIEW;
case REVIEW ->
target == DRAFT
|| target == PUBLISHED;
case PUBLISHED ->
target == ARCHIVED;
case ARCHIVED ->
false;
};
}
}
CourseCode.java
package io.liveklass.course;
import java.util.Objects;
public final class CourseCode {
private final String value;
public CourseCode(
String value
) {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
String normalized =
value.strip()
.toUpperCase();
if (
!normalized.matches(
"[A-Z0-9-]+"
)
) {
throw new IllegalArgumentException(
"Course code contains unsupported characters."
);
}
this.value =
normalized;
}
public String getValue() {
return value;
}
@Override
public boolean equals(
Object other
) {
if (this == other) {
return true;
}
if (
!(other
instanceof CourseCode courseCode)
) {
return false;
}
return value.equals(
courseCode.value
);
}
@Override
public int hashCode() {
return Objects.hash(
value
);
}
@Override
public String toString() {
return value;
}
}
Restoring Persisted Domain State
Normally a new course starts as:
DRAFT
But repository may load:
PUBLISHED
We should not replay:
DRAFT → REVIEW → PUBLISHED
just to restore persisted state।
Instead provide a controlled restoration path।
Course.java
package io.liveklass.course;
public final class Course {
private final CourseCode code;
private final String title;
private final long priceInPaisa;
private CourseStatus status;
public Course(
CourseCode code,
String title,
long priceInPaisa
) {
this(
code,
title,
priceInPaisa,
CourseStatus.DRAFT
);
}
private Course(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status
) {
if (code == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (priceInPaisa < 0) {
throw new IllegalArgumentException(
"Course price cannot be negative."
);
}
if (status == null) {
throw new IllegalArgumentException(
"Course status is required."
);
}
this.code = code;
this.title = title.strip();
this.priceInPaisa = priceInPaisa;
this.status = status;
}
public static Course restore(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status
) {
return new Course(
code,
title,
priceInPaisa,
status
);
}
public void changeStatus(
CourseStatus targetStatus
) {
if (
!status.canTransitionTo(
targetStatus
)
) {
throw new IllegalStateException(
"Invalid course status transition from "
+ status
+ " to "
+ targetStatus
+ "."
);
}
status =
targetStatus;
}
public CourseCode getCode() {
return code;
}
public String getTitle() {
return title;
}
public long getPriceInPaisa() {
return priceInPaisa;
}
public CourseStatus getStatus() {
return status;
}
@Override
public String toString() {
return code
+ " — "
+ title
+ " — "
+ status;
}
}
Why Use restore()?
A repository is reconstructing an existing object।
It is not performing a new business transition।
This:
Course.restore(...)
communicates that intent explicitly।
But notice:
Constructor invariants are still enforced
The repository cannot restore:
Negative price
Blank title
Null status
as a valid domain object।
Repository Contract
CourseRepository.java
package io.liveklass.repository;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import java.util.List;
public interface CourseRepository {
void save(
Course course
);
Course findByCode(
CourseCode courseCode
);
List<Course> findAll();
boolean deleteByCode(
CourseCode courseCode
);
}
Missing Course Contract
Our contract says:
findByCode(...)
returns:
Course → when found
null → when not found
This is a deliberate design।
A higher-level method may convert absence into:
CourseNotFoundException
Example:
Course course =
repository.findByCode(
courseCode
);
if (course == null) {
throw new CourseNotFoundException(
courseCode
);
}
Missing data and storage failure remain different।
Storage Format
We will use Java:
Properties
Each course file might look like:
code=JAVA-OOP
title=Java and OOP Foundation
priceInPaisa=499000
status=PUBLISHED
This is simple enough for learning file persistence।
Why Not JSON Yet?
JSON is common in real systems, but Java does not provide a rich general-purpose JSON mapper in the standard library।
Using JSON would require an external library such as:
Jackson
Gson
That would distract from the I/O concepts of this module।
Later, a real application may use JSON, database rows, or another serialization format।
File Naming Strategy
Course:
JAVA-OOP
can map to:
java-oop.properties
Storage:
data/
└── courses/
├── java-oop.properties
├── backend.properties
└── system-design.properties
File Names Should Come from Controlled Values
Do not use arbitrary course title:
Java / Backend: Complete Course!
directly as a file name।
Use a controlled identifier such as:
CourseCode
Because it already allows only:
A-Z
0-9
-
This makes path construction safer।
Repository Exception
CourseRepositoryException.java
package io.liveklass.repository;
public final class CourseRepositoryException
extends RuntimeException {
public CourseRepositoryException(
String message
) {
super(
message
);
}
public CourseRepositoryException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Repository clients do not need to understand raw:
IOException
NumberFormatException
IllegalArgumentException from malformed storage
The repository translates storage failures into its abstraction।
File Repository Fields
public final class FileCourseRepository
implements CourseRepository {
private final Path baseDirectory;
}
Constructor:
public FileCourseRepository(
Path baseDirectory
) {
if (baseDirectory == null) {
throw new IllegalArgumentException(
"Base directory is required."
);
}
this.baseDirectory =
baseDirectory
.toAbsolutePath()
.normalize();
}
Resolve Course File
private Path resolveFile(
CourseCode courseCode
) {
if (courseCode == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
String fileName =
courseCode.getValue()
.toLowerCase()
+ ".properties";
Path target =
baseDirectory.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw new IllegalArgumentException(
"Course path escapes repository directory."
);
}
return target;
}
Because CourseCode is already controlled, traversal risk is low, but repository still protects its storage boundary।
Serialization
Serialization means:
Convert object state
→ storable representation
For our repository:
Course
becomes:
Properties
Convert Course to Properties
private Properties serialize(
Course course
) {
Properties properties =
new Properties();
properties.setProperty(
"code",
course.getCode()
.getValue()
);
properties.setProperty(
"title",
course.getTitle()
);
properties.setProperty(
"priceInPaisa",
Long.toString(
course.getPriceInPaisa()
)
);
properties.setProperty(
"status",
course.getStatus()
.name()
);
return properties;
}
Why Persist Enum name()?
We persist:
course.getStatus()
.name()
Example:
PUBLISHED
Do not persist:
ordinal()
because enum ordering can change।
Deserialization
Deserialization means:
Stored representation
→ domain object
For example:
code=JAVA-OOP
title=Java Foundation
priceInPaisa=499000
status=DRAFT
becomes:
Course
Required Property Helper
private String requireProperty(
Properties properties,
String key
) {
String value =
properties.getProperty(
key
);
if (
value == null
|| value.isBlank()
) {
throw new CourseRepositoryException(
"Stored course is missing property: "
+ key
+ "."
);
}
return value;
}
Deserialize Course
private Course deserialize(
Properties properties,
Path source
) {
try {
CourseCode courseCode =
new CourseCode(
requireProperty(
properties,
"code"
)
);
String title =
requireProperty(
properties,
"title"
);
long priceInPaisa =
Long.parseLong(
requireProperty(
properties,
"priceInPaisa"
)
);
CourseStatus status =
CourseStatus.valueOf(
requireProperty(
properties,
"status"
)
);
return Course.restore(
courseCode,
title,
priceInPaisa,
status
);
} catch (
CourseRepositoryException exception
) {
throw exception;
} catch (
RuntimeException exception
) {
throw new CourseRepositoryException(
"Stored course data is invalid: "
+ source.getFileName()
+ ".",
exception
);
}
}
Corrupted Storage Is Not Not-Found
Suppose file exists:
code=JAVA-OOP
priceInPaisa=hello
status=DRAFT
Parsing fails।
This is not:
Course not found
It is:
Repository data corrupted or invalid
Correct abstraction:
CourseRepositoryException
Loading a Properties File
private Properties loadProperties(
Path path
) throws IOException {
Properties properties =
new Properties();
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
properties.load(
reader
);
}
return properties;
}
Resource ownership is clear:
Repository opens reader
Repository closes reader
Saving Properties
private void storeProperties(
Path path,
Properties properties
) throws IOException {
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
properties.store(
writer,
null
);
}
}
Direct Save Problem
We could simply:
storeProperties(
target,
properties
);
But overwriting an important file directly is less robust।
Instead:
Write temp file
↓
Move temp file over target
Atomic Replacement Strategy
private void replaceFile(
Path target,
Properties properties
) throws IOException {
Files.createDirectories(
baseDirectory
);
Path temp =
Files.createTempFile(
baseDirectory,
"course-",
".tmp"
);
try {
storeProperties(
temp,
properties
);
try {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} catch (
AtomicMoveNotSupportedException exception
) {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
}
} finally {
Files.deleteIfExists(
temp
);
}
}
Why Use a Temp File?
Direct overwrite can expose incomplete content if something goes badly during the write।
Temp strategy means:
Existing file remains untouched
until new file has been written
Then replacement happens afterward।
Why Same Directory?
Temp file is created under:
baseDirectory
This increases the chance source and target are on the same file system, which is important for atomic move support।
Implementing save()
@Override
public void save(
Course course
) {
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
Path target =
resolveFile(
course.getCode()
);
Properties properties =
serialize(
course
);
try {
replaceFile(
target,
properties
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not save course "
+ course.getCode()
+ ".",
exception
);
}
}
Save Semantics
Our:
save(...)
means:
Create if missing
Replace if existing
This is effectively repository-level:
upsert
The interface should document this behavior।
Implementing findByCode()
@Override
public Course findByCode(
CourseCode courseCode
) {
Path path =
resolveFile(
courseCode
);
try {
Properties properties =
loadProperties(
path
);
return deserialize(
properties,
path
);
} catch (
NoSuchFileException exception
) {
return null;
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load course "
+ courseCode
+ ".",
exception
);
}
}
Why Catch NoSuchFileException Separately?
Because our repository contract defines:
Missing file → Course absent → null
Other I/O failures mean:
Repository failed
Do not collapse them together।
Do We Need Files.exists() First?
We could write:
if (
Files.notExists(
path
)
) {
return null;
}
then read।
But this adds a check-then-read race।
Simpler:
Try reading
Catch NoSuchFileException
The actual operation remains authoritative।
Implementing deleteByCode()
@Override
public boolean deleteByCode(
CourseCode courseCode
) {
Path path =
resolveFile(
courseCode
);
try {
return Files.deleteIfExists(
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not delete course "
+ courseCode
+ ".",
exception
);
}
}
Semantics:
true → Existing course file deleted
false → Course file did not exist
Listing Stored Courses
Use:
Files.list(
baseDirectory
)
Filter:
Regular files
.properties extension
Then load each course।
Implementing findAll()
@Override
public List<Course> findAll() {
try {
Files.createDirectories(
baseDirectory
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not initialize course repository.",
exception
);
}
try (
Stream<Path> paths =
Files.list(
baseDirectory
)
) {
return paths.filter(
Files::isRegularFile
)
.filter(
this::isCourseFile
)
.map(
this::loadCourseFile
)
.toList();
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not list stored courses.",
exception
);
}
}
Course File Filter
private boolean isCourseFile(
Path path
) {
return path.getFileName()
.toString()
.endsWith(
".properties"
);
}
Load One Listed File
private Course loadCourseFile(
Path path
) {
try {
Properties properties =
loadProperties(
path
);
return deserialize(
properties,
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load stored course "
+ path.getFileName()
+ ".",
exception
);
}
}
Complete FileCourseRepository
package io.liveklass.repository;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseStatus;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Properties;
import java.util.stream.Stream;
public final class FileCourseRepository
implements CourseRepository {
private final Path baseDirectory;
public FileCourseRepository(
Path baseDirectory
) {
if (baseDirectory == null) {
throw new IllegalArgumentException(
"Base directory is required."
);
}
this.baseDirectory =
baseDirectory
.toAbsolutePath()
.normalize();
}
@Override
public void save(
Course course
) {
if (course == null) {
throw new IllegalArgumentException(
"Course is required."
);
}
Path target =
resolveFile(
course.getCode()
);
try {
Files.createDirectories(
baseDirectory
);
replaceFile(
target,
serialize(
course
)
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not save course "
+ course.getCode()
+ ".",
exception
);
}
}
@Override
public Course findByCode(
CourseCode courseCode
) {
Path path =
resolveFile(
courseCode
);
try {
return deserialize(
loadProperties(
path
),
path
);
} catch (
NoSuchFileException exception
) {
return null;
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load course "
+ courseCode
+ ".",
exception
);
}
}
@Override
public List<Course> findAll() {
try {
Files.createDirectories(
baseDirectory
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not initialize course repository.",
exception
);
}
try (
Stream<Path> paths =
Files.list(
baseDirectory
)
) {
return paths.filter(
Files::isRegularFile
)
.filter(
this::isCourseFile
)
.map(
this::loadCourseFile
)
.toList();
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not list courses.",
exception
);
}
}
@Override
public boolean deleteByCode(
CourseCode courseCode
) {
Path path =
resolveFile(
courseCode
);
try {
return Files.deleteIfExists(
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not delete course "
+ courseCode
+ ".",
exception
);
}
}
private Path resolveFile(
CourseCode courseCode
) {
if (courseCode == null) {
throw new IllegalArgumentException(
"Course code is required."
);
}
String fileName =
courseCode.getValue()
.toLowerCase()
+ ".properties";
Path target =
baseDirectory.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw new IllegalArgumentException(
"Course path escapes repository directory."
);
}
return target;
}
private Properties serialize(
Course course
) {
Properties properties =
new Properties();
properties.setProperty(
"code",
course.getCode()
.getValue()
);
properties.setProperty(
"title",
course.getTitle()
);
properties.setProperty(
"priceInPaisa",
Long.toString(
course.getPriceInPaisa()
)
);
properties.setProperty(
"status",
course.getStatus()
.name()
);
return properties;
}
private Course deserialize(
Properties properties,
Path source
) {
try {
CourseCode code =
new CourseCode(
requireProperty(
properties,
"code"
)
);
String title =
requireProperty(
properties,
"title"
);
long priceInPaisa =
Long.parseLong(
requireProperty(
properties,
"priceInPaisa"
)
);
CourseStatus status =
CourseStatus.valueOf(
requireProperty(
properties,
"status"
)
);
return Course.restore(
code,
title,
priceInPaisa,
status
);
} catch (
CourseRepositoryException exception
) {
throw exception;
} catch (
RuntimeException exception
) {
throw new CourseRepositoryException(
"Stored course data is invalid: "
+ source.getFileName()
+ ".",
exception
);
}
}
private String requireProperty(
Properties properties,
String key
) {
String value =
properties.getProperty(
key
);
if (
value == null
|| value.isBlank()
) {
throw new CourseRepositoryException(
"Stored course is missing property: "
+ key
+ "."
);
}
return value;
}
private Properties loadProperties(
Path path
) throws IOException {
Properties properties =
new Properties();
try (
BufferedReader reader =
Files.newBufferedReader(
path,
StandardCharsets.UTF_8
)
) {
properties.load(
reader
);
}
return properties;
}
private void storeProperties(
Path path,
Properties properties
) throws IOException {
try (
BufferedWriter writer =
Files.newBufferedWriter(
path,
StandardCharsets.UTF_8
)
) {
properties.store(
writer,
null
);
}
}
private void replaceFile(
Path target,
Properties properties
) throws IOException {
Path temp =
Files.createTempFile(
baseDirectory,
"course-",
".tmp"
);
try {
storeProperties(
temp,
properties
);
try {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} catch (
AtomicMoveNotSupportedException exception
) {
Files.move(
temp,
target,
StandardCopyOption.REPLACE_EXISTING
);
}
} finally {
Files.deleteIfExists(
temp
);
}
}
private boolean isCourseFile(
Path path
) {
return path.getFileName()
.toString()
.endsWith(
".properties"
);
}
private Course loadCourseFile(
Path path
) {
try {
return deserialize(
loadProperties(
path
),
path
);
} catch (
IOException exception
) {
throw new CourseRepositoryException(
"Could not load stored course "
+ path.getFileName()
+ ".",
exception
);
}
}
}
Using the Repository
package io.liveklass;
import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseStatus;
import io.liveklass.repository.CourseRepository;
import io.liveklass.repository.FileCourseRepository;
import java.nio.file.Path;
public class Main {
public static void main(
String[] args
) {
CourseRepository repository =
new FileCourseRepository(
Path.of(
"data",
"courses"
)
);
Course course =
new Course(
new CourseCode(
"JAVA-OOP"
),
"Java and OOP Foundation",
499_000L
);
course.changeStatus(
CourseStatus.REVIEW
);
repository.save(
course
);
Course loaded =
repository.findByCode(
new CourseCode(
"java-oop"
)
);
System.out.println(
loaded
);
repository.findAll()
.forEach(
System.out::println
);
}
}
Expected Stored File
Conceptually:
code=JAVA-OOP
title=Java and OOP Foundation
priceInPaisa=499000
status=REVIEW
Properties.store() may also write formatting or comment metadata depending on the JDK implementation।
Your application should rely on the properties, not exact visual formatting।
Repository Responsibility
The repository should own:
- File location
- File naming
- Encoding
- Serialization
- Deserialization
- Resource lifecycle
- Atomic replacement
- I/O exception translation
It should not own:
- Whether learner can access a course
- Whether instructor may publish
- Course pricing business policy
- Authentication
- Authorization
Those belong to other layers।
Missing vs Broken Storage
These must remain different।
Missing
java-oop.properties does not exist
Result:
null
according to our repository contract।
Broken
priceInPaisa=banana
Result:
CourseRepositoryException
Never turn corrupted storage into:
Course not found
File Storage and Concurrency
Our repository has a serious limitation:
Multiple writers
Suppose:
Process A loads course
Process B loads same course
A changes title and saves
B changes status and saves
B may overwrite A's update।
This is a:
Lost update
Atomic File Replacement Does Not Solve Lost Updates
Atomic move helps with:
Partial file replacement
It does not solve:
Two clients updating stale versions
These are different problems।
Possible Version Field
You could persist:
version=7
Then update only if expected version matches।
Conceptually:
Loaded version = 7
Current stored version = 7
→ Save version 8
If current stored version is already:
8
then reject the stale update।
But implementing correct cross-process locking and compare-and-set semantics with plain files becomes increasingly complex।
This is where databases become much more attractive।
File Repository Limitations
A file repository usually lacks convenient support for:
- Concurrent updates
- Transactions
- Complex queries
- Indexes
- Relationships
- Unique constraints across multiple entities
- Row-level locking
- Efficient pagination
- High write throughput
- Multi-node coordination
When File Storage Is Appropriate
Good use cases:
Learning projects
Local developer tools
Static course content
Configuration
Small single-user applications
Exports
Imports
Caches
Simple prototypes
When a Database Is Better
Prefer a database when you need:
Many concurrent users
Frequent updates
Transactions
Relationships
Search and filtering
Pagination
Uniqueness guarantees
Large datasets
Multiple application instances
Reliable concurrent writes
For a real multi-user course platform, primary course metadata would normally belong in a database rather than one file per course।
Files Can Still Be Useful Beside a Database
Database:
Course metadata
Enrollment
Payments
User relationships
Object/file storage:
Lesson Markdown
Images
Videos
PDF attachments
Exports
Real systems often combine several storage technologies।
Repository Abstraction Makes Migration Easier
Today:
CourseRepository repository =
new FileCourseRepository(
path
);
Later:
CourseRepository repository =
new PostgresCourseRepository(
dataSource
);
Application service can continue using:
repository.save(...)
repository.findByCode(...)
provided the repository contract remains appropriate।
Do Not Abstract Everything Prematurely
Repository abstraction is useful when there is a meaningful persistence boundary।
Do not create interfaces for every tiny file operation such as:
FileNameProviderFactoryStrategy
PathResolverManager
FileReaderRepositoryAdapterFactory
Keep the design proportional to the problem।
Common Mistakes
Letting Services Build File Paths
Leaks storage implementation into business logic।
Using Course Titles as File Names
Titles may contain unsafe or unstable characters।
Persisting Enum ordinal()
Enum ordering changes can corrupt meaning।
Returning null for Every Repository Failure
Makes not-found indistinguishable from broken storage।
Exposing Raw IOException Everywhere
Leaks storage details beyond the repository abstraction।
Ignoring Corrupted Files
Bad stored data should produce a visible repository failure।
Writing Directly to Important Files Without Considering Partial Replacement
Temp file + move provides safer update semantics।
Assuming Atomic Move Solves Concurrent Updates
It does not prevent lost updates।
Treating File Storage as a Database Replacement
Files become difficult when querying, transactions, and concurrency grow।
Putting Business Rules Inside the Repository
Repository should persist state, not decide business policy।
Practice Exercises
Exercise 1: Create Repository Interface
Create:
LessonRepository
with:
save(...)
findById(...)
findAll()
deleteById(...)
Do not expose Path in the interface।
Exercise 2: Serialization
Convert:
Course
to:
Properties
including:
code
title
priceInPaisa
status
Persist enum using:
name()
Exercise 3: Missing vs Failure
Implement:
findByCode(...)
so that:
Missing file → null
Permission denied → CourseRepositoryException
Invalid stored price → CourseRepositoryException
Exercise 4: Atomic Save
Implement:
Write temporary file
Move with REPLACE_EXISTING + ATOMIC_MOVE
Fall back when atomic move is unsupported
Exercise 5: List Courses
Use:
Files.list()
and load only:
*.properties
files।
Exercise 6: Corrupted File
Create:
code=JAVA
title=Java
priceInPaisa=hello
status=DRAFT
Predict and verify the repository failure।
Exercise 7: Concurrency Analysis
Explain why atomic file replacement does not prevent this:
A loads version 1
B loads version 1
A saves changes
B saves different changes
What update is potentially lost?
Predict the Result
Question 1
Stored status:
status=PUBLISHED
Repository calls:
CourseStatus.valueOf(
"PUBLISHED"
);
What is returned?
Answer
CourseStatus.PUBLISHED
Question 2
Stored status:
status=published
What happens with:
CourseStatus.valueOf(
"published"
);
Answer
It throws:
IllegalArgumentException
because valueOf() is case-sensitive।
The repository should translate invalid persisted data into a repository failure।
Question 3
Course file does not exist।
Our findByCode() catches:
NoSuchFileException
What does it return?
Answer
null
because that is the repository contract we chose।
Question 4
The file exists but contains:
priceInPaisa=abc
Should repository return null?
Answer
No।
The stored entity exists but is invalid।
It should throw:
CourseRepositoryException
Question 5
Does atomic replacement prevent two callers from overwriting each other's logical changes?
Answer
No।
It protects replacement integrity, not application-level concurrent update coordination।
Knowledge Check
Question 1
What problem does a repository solve?
Question 2
Should business services know file names?
Question 3
What is serialization?
Question 4
What is deserialization?
Question 5
Why use a controlled identifier for file names?
Question 6
Why persist enum name() instead of ordinal()?
Question 7
Why distinguish a missing file from an unreadable file?
Question 8
Why translate IOException into CourseRepositoryException?
Question 9
Why use a temporary file during save?
Question 10
What does atomic replacement protect?
Question 11
Does it prevent lost updates?
Question 12
Why use Course.restore()?
Question 13
What responsibilities belong in the repository?
Question 14
What responsibilities should remain outside it?
Question 15
When is a database usually preferable to one-file-per-entity storage?
Knowledge Check Answers
Answer 1
It separates application/domain logic from persistence implementation details।
Answer 2
Usually no. File naming is a repository implementation concern।
Answer 3
Converting object state into a form that can be stored or transmitted।
Answer 4
Converting stored representation back into application/domain objects।
Answer 5
It provides stable, predictable, and safer storage paths।
Answer 6
ordinal() depends on enum declaration order; name() has clearer stable meaning।
Answer 7
Absence is a valid lookup result; unreadable storage means the repository failed।
Answer 8
Higher layers should depend on repository semantics rather than low-level file-system details।
Answer 9
It reduces the chance of exposing partially written target content।
Answer 10
It helps ensure target replacement occurs as one file-system operation when supported।
Answer 11
No. Concurrent stale writers can still overwrite each other।
Answer 12
To reconstruct persisted state without pretending that normal business transitions occurred again।
Answer 13
File paths, encoding, serialization, reading, writing, deletion, listing, and storage exception translation।
Answer 14
Authorization, publication policy, pricing rules, and other business decisions।
Answer 15
When concurrency, transactions, relationships, querying, pagination, or multi-instance writes become important।
Lesson Summary
এই lesson-এ আমরা শিখেছি:
- Repository persistence implementationকে business logic থেকে isolate করে
- Repository storage database হওয়া বাধ্যতামূলক নয়
- File-based repository simple persistence শেখার জন্য useful
- Stable domain identifiers file naming-এর জন্য ভালো
- Domain objectsকে serialization-এর মাধ্যমে file format-এ convert করা যায়
- Stored representationকে deserialization-এর মাধ্যমে domain object-এ restore করা যায়
- Persisted enum values
name()দিয়ে store করা safer thanordinal() - Missing entity এবং repository failure আলাদা concepts
- Corrupted storage not-found হিসেবে return করা উচিত নয়
IOExceptionrepository-specific failure-এ translate করা যায়- Resource ownership repository-এর ভিতরে রাখা যায়
- Temporary-file replacement direct overwrite-এর চেয়ে safer হতে পারে
ATOMIC_MOVEsupport platform-dependent- Atomic replacement partial write risk reduce করে
- Atomic replacement lost update prevent করে না
- File repositories concurrent write scenarios-এ limited
- Databases transactions, querying, indexing, and concurrency better handle করে
- Files এবং databases একই real system-এ different responsibilities পালন করতে পারে
- A clean repository contract future storage implementation পরিবর্তন সহজ করতে পারে
- Abstraction useful, but unnecessary architecture avoid করা উচিত
Next Lesson
পরবর্তী lesson:
Module Practice and Assessment
আমরা তৈরি করব একটি complete file-based course storage application যেখানে থাকবে:
- Safe path handling
- UTF-8 reading and writing
- Repository abstraction
- File serialization
- Atomic replacement
- Listing stored courses
- Missing-file handling
- Exception translation
- Recursive export or backup
- Final file I/O design assessment