Final Project

Implementing File-Based Persistence

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

এখন পর্যন্ত Final Project in-memory repositories ব্যবহার করছে।

Example:

CourseRepository repository =
        new InMemoryCourseRepository();

এবার আমরা একই repository contracts-এর file-based implementations তৈরি করব।

Goal:

Application restart হওয়ার পরও data থাকবে।

সবচেয়ে গুরুত্বপূর্ণ বিষয়:

CourseService
LearnerService
EnrollmentService

একটিও পরিবর্তন করতে হবে না।

আমরা implement করব:

  • StorageException
  • StoredDataCorruptionException
  • UTF-8 Properties
  • Safe paths
  • Atomic file replacement
  • FileCourseRepository
  • FileLearnerRepository
  • FileEnrollmentRepository
  • Serialization
  • Deserialization
  • restore()
  • Missing vs corrupted data handling

Storage Layout

আমাদের data directory:

data/
├── courses/
├── learners/
└── enrollments/

Example:

data/
├── courses/
│   └── java-oop.properties
├── learners/
│   └── 1.properties
└── enrollments/
    └── 1001.properties

Why Properties?

Java standard library provides:

java.util.Properties

Simple structured file storage-এর জন্য এটি যথেষ্ট।

Example:

code=JAVA-OOP
title=Java and OOP Foundation
priceInPaisa=499000
status=PUBLISHED

এই project-এর goal database design নয়।

আমরা practice করছি:

File I/O
Serialization
Repository abstraction
Resource management
Failure handling

Storage Exceptions

File repositories raw:

IOException

application layer-এ leak করবে না।

StorageException.java

package io.liveklass.storage;

public class StorageException
        extends RuntimeException {

    public StorageException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

StoredDataCorruptionException.java

package io.liveklass.storage;

public final class StoredDataCorruptionException
        extends StorageException {

    public StoredDataCorruptionException(
            String message,
            Throwable cause
    ) {
        super(
                message,
                cause
        );
    }
}

Missing and Corrupted Are Different

Suppose:

java-oop.properties

does not exist।

Repository may return:

null

meaning:

Course not found

But if file exists and contains:

status=NOT-A-STATUS

that is not "not found"।

That is:

corrupted stored data

and should fail visibly।


Safe File Persistence Strategy

Every file repository should:

Normalize base directory
Create directory if missing
Use controlled filenames
Use UTF-8
Use try-with-resources
Write to temporary file
Replace target after successful write
Translate IOException
Validate deserialized data

Atomic Replacement

Instead of overwriting target directly:

Serialize
↓
Write temporary file
↓
Move temporary file over target

When supported:

StandardCopyOption.ATOMIC_MOVE

Fallback:

StandardCopyOption.REPLACE_EXISTING

Shared Move Helper

private void replaceFile(
        Path temporary,
        Path target
) throws IOException {
    try {
        Files.move(
                temporary,
                target,
                StandardCopyOption.ATOMIC_MOVE,
                StandardCopyOption.REPLACE_EXISTING
        );
    } catch (
        AtomicMoveNotSupportedException exception
    ) {
        Files.move(
                temporary,
                target,
                StandardCopyOption.REPLACE_EXISTING
        );
    }
}

ATOMIC_MOVE সব filesystem-এ supported নয়।


1. File Course Repository

Course storage needs:

CourseCode
Title
Price
Status
Lessons

Example file:

code=JAVA-OOP
title=Java and OOP Foundation
priceInPaisa=499000
status=PUBLISHED
lesson.count=2
lesson.0.id=1
lesson.0.title=Introduction to Java
lesson.0.content=Java basics
lesson.1.id=2
lesson.1.title=Classes and Objects
lesson.1.content=OOP basics

FileCourseRepository.java

package io.liveklass.storage;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseRepository;
import io.liveklass.course.CourseStatus;
import io.liveklass.course.Lesson;
import io.liveklass.course.LessonId;

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.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

public final class FileCourseRepository
        implements CourseRepository {

    private final Path baseDirectory;

    public FileCourseRepository(
            Path baseDirectory
    ) {
        if (baseDirectory == null) {
            throw new IllegalArgumentException(
                    "Course storage directory is required."
            );
        }

        this.baseDirectory =
                baseDirectory.toAbsolutePath()
                        .normalize();

        initializeDirectory();
    }

    @Override
    public void save(
            Course course
    ) {
        if (course == null) {
            throw new IllegalArgumentException(
                    "Course is required."
            );
        }

        Path target =
                pathFor(
                        course.getCode()
                );

        Path temporary =
                createTemporaryFile();

        try {
            Properties properties =
                    serialize(
                            course
                    );

            try (
                var writer =
                        Files.newBufferedWriter(
                                temporary,
                                StandardCharsets.UTF_8
                        )
            ) {
                properties.store(
                        writer,
                        null
                );
            }

            replaceFile(
                    temporary,
                    target
            );
        } catch (
            IOException exception
        ) {
            deleteQuietly(
                    temporary
            );

            throw new StorageException(
                    "Failed to save course "
                    + course.getCode()
                    + ".",
                    exception
            );
        }
    }

    @Override
    public Course findByCode(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        try {
            return readCourse(
                    pathFor(
                            courseCode
                    )
            );
        } catch (
            NoSuchFileException exception
        ) {
            return null;
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to read course "
                    + courseCode
                    + ".",
                    exception
            );
        }
    }

    @Override
    public List<Course> findAll() {
        try (
            var paths =
                    Files.list(
                            baseDirectory
                    )
        ) {
            return paths
                    .filter(
                            Files::isRegularFile
                    )
                    .filter(
                            this::isPropertiesFile
                    )
                    .sorted()
                    .map(
                            this::readUnchecked
                    )
                    .toList();
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to list courses.",
                    exception
            );
        }
    }

    private Properties serialize(
            Course course
    ) {
        Properties properties =
                new Properties();

        properties.setProperty(
                "code",
                course.getCode()
                        .value()
        );

        properties.setProperty(
                "title",
                course.getTitle()
        );

        properties.setProperty(
                "priceInPaisa",
                Long.toString(
                        course.getPriceInPaisa()
                )
        );

        properties.setProperty(
                "status",
                course.getStatus()
                        .name()
        );

        List<Lesson> lessons =
                course.getLessons();

        properties.setProperty(
                "lesson.count",
                Integer.toString(
                        lessons.size()
                )
        );

        for (
                int index = 0;
                index < lessons.size();
                index++
        ) {
            Lesson lesson =
                    lessons.get(
                            index
                    );

            String prefix =
                    "lesson."
                    + index
                    + ".";

            properties.setProperty(
                    prefix + "id",
                    Long.toString(
                            lesson.id()
                                    .value()
                    )
            );

            properties.setProperty(
                    prefix + "title",
                    lesson.title()
            );

            properties.setProperty(
                    prefix + "content",
                    lesson.content()
            );
        }

        return properties;
    }

    private Course readCourse(
            Path path
    ) throws IOException {
        Properties properties =
                load(
                        path
                );

        try {
            CourseCode code =
                    new CourseCode(
                            required(
                                    properties,
                                    "code"
                            )
                    );

            String title =
                    required(
                            properties,
                            "title"
                    );

            long price =
                    Long.parseLong(
                            required(
                                    properties,
                                    "priceInPaisa"
                            )
                    );

            CourseStatus status =
                    CourseStatus.valueOf(
                            required(
                                    properties,
                                    "status"
                            )
                    );

            int lessonCount =
                    Integer.parseInt(
                            required(
                                    properties,
                                    "lesson.count"
                            )
                    );

            if (lessonCount < 0) {
                throw new IllegalArgumentException(
                        "Lesson count cannot be negative."
                );
            }

            List<Lesson> lessons =
                    new ArrayList<>();

            for (
                    int index = 0;
                    index < lessonCount;
                    index++
            ) {
                String prefix =
                        "lesson."
                        + index
                        + ".";

                lessons.add(
                        new Lesson(
                                new LessonId(
                                        Long.parseLong(
                                                required(
                                                        properties,
                                                        prefix + "id"
                                                )
                                        )
                                ),
                                required(
                                        properties,
                                        prefix + "title"
                                ),
                                required(
                                        properties,
                                        prefix + "content"
                                )
                        )
                );
            }

            return Course.restore(
                    code,
                    title,
                    price,
                    status,
                    lessons
            );
        } catch (
            IllegalArgumentException exception
        ) {
            throw new StoredDataCorruptionException(
                    "Invalid course data in "
                    + path.getFileName()
                    + ".",
                    exception
            );
        }
    }

    private Course readUnchecked(
            Path path
    ) {
        try {
            return readCourse(
                    path
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to read course file "
                    + path.getFileName()
                    + ".",
                    exception
            );
        }
    }

    private Properties load(
            Path path
    ) throws IOException {
        Properties properties =
                new Properties();

        try (
            var reader =
                    Files.newBufferedReader(
                            path,
                            StandardCharsets.UTF_8
                    )
        ) {
            properties.load(
                    reader
            );
        }

        return properties;
    }

    private String required(
            Properties properties,
            String key
    ) {
        String value =
                properties.getProperty(
                        key
                );

        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Missing property: "
                    + key
                    + "."
            );
        }

        return value;
    }

    private Path pathFor(
            CourseCode courseCode
    ) {
        String fileName =
                courseCode.value()
                        .toLowerCase(
                                Locale.ROOT
                        )
                + ".properties";

        Path path =
                baseDirectory.resolve(
                        fileName
                )
                .normalize();

        if (
                !path.startsWith(
                        baseDirectory
                )
        ) {
            throw new IllegalArgumentException(
                    "Invalid course storage path."
            );
        }

        return path;
    }

    private Path createTemporaryFile() {
        try {
            return Files.createTempFile(
                    baseDirectory,
                    "course-",
                    ".tmp"
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to create temporary course file.",
                    exception
            );
        }
    }

    private void replaceFile(
            Path temporary,
            Path target
    ) throws IOException {
        try {
            Files.move(
                    temporary,
                    target,
                    StandardCopyOption.ATOMIC_MOVE,
                    StandardCopyOption.REPLACE_EXISTING
            );
        } catch (
            AtomicMoveNotSupportedException exception
        ) {
            Files.move(
                    temporary,
                    target,
                    StandardCopyOption.REPLACE_EXISTING
            );
        }
    }

    private void initializeDirectory() {
        try {
            Files.createDirectories(
                    baseDirectory
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to initialize course storage.",
                    exception
            );
        }
    }

    private boolean isPropertiesFile(
            Path path
    ) {
        return path.getFileName()
                .toString()
                .endsWith(
                        ".properties"
                );
    }

    private void deleteQuietly(
            Path path
    ) {
        try {
            Files.deleteIfExists(
                    path
            );
        } catch (
            IOException ignored
        ) {
        }
    }
}

Why Course.restore()?

A stored course may already be:

PUBLISHED
ARCHIVED

Repository should reconstruct that state directly।

It should not replay:

course.publish();
course.archive();

during loading।

But restore must still enforce invariants।

Corrupted state must remain invalid।


2. File Learner Repository

Learner file:

id=1
name=Sakib
email=sakib@example.com

The complete logic is simpler because Learner has no nested collection।

Core Implementation

package io.liveklass.storage;

import io.liveklass.learner.EmailAddress;
import io.liveklass.learner.Learner;
import io.liveklass.learner.LearnerId;
import io.liveklass.learner.LearnerRepository;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;

public final class FileLearnerRepository
        implements LearnerRepository {

    private final Path baseDirectory;

    public FileLearnerRepository(
            Path baseDirectory
    ) {
        if (baseDirectory == null) {
            throw new IllegalArgumentException(
                    "Learner storage directory is required."
            );
        }

        this.baseDirectory =
                baseDirectory.toAbsolutePath()
                        .normalize();

        try {
            Files.createDirectories(
                    this.baseDirectory
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to initialize learner storage.",
                    exception
            );
        }
    }

    @Override
    public void save(
            Learner learner
    ) {
        Properties properties =
                new Properties();

        properties.setProperty(
                "id",
                Long.toString(
                        learner.id()
                                .value()
                )
        );

        properties.setProperty(
                "name",
                learner.name()
        );

        properties.setProperty(
                "email",
                learner.email()
                        .value()
        );

        Path target =
                pathFor(
                        learner.id()
                );

        Path temporary;

        try {
            temporary =
                    Files.createTempFile(
                            baseDirectory,
                            "learner-",
                            ".tmp"
                    );

            try (
                var writer =
                        Files.newBufferedWriter(
                                temporary,
                                StandardCharsets.UTF_8
                        )
            ) {
                properties.store(
                        writer,
                        null
                );
            }

            replaceFile(
                    temporary,
                    target
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to save learner "
                    + learner.id()
                    + ".",
                    exception
            );
        }
    }

    @Override
    public Learner findById(
            LearnerId learnerId
    ) {
        try {
            return read(
                    pathFor(
                            learnerId
                    )
            );
        } catch (
            NoSuchFileException exception
        ) {
            return null;
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to read learner "
                    + learnerId
                    + ".",
                    exception
            );
        }
    }

    @Override
    public Learner findByEmail(
            EmailAddress email
    ) {
        return findAll()
                .stream()
                .filter(
                        learner ->
                                learner.email()
                                        .equals(
                                                email
                                        )
                )
                .findFirst()
                .orElse(
                        null
                );
    }

    @Override
    public List<Learner> findAll() {
        try (
            var paths =
                    Files.list(
                            baseDirectory
                    )
        ) {
            return paths
                    .filter(
                            path ->
                                    Files.isRegularFile(
                                            path
                                    )
                                    && path.getFileName()
                                            .toString()
                                            .endsWith(
                                                    ".properties"
                                            )
                    )
                    .sorted()
                    .map(
                            this::readUnchecked
                    )
                    .toList();
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to list learners.",
                    exception
            );
        }
    }

    private Learner read(
            Path path
    ) throws IOException {
        Properties properties =
                new Properties();

        try (
            var reader =
                    Files.newBufferedReader(
                            path,
                            StandardCharsets.UTF_8
                    )
        ) {
            properties.load(
                    reader
            );
        }

        try {
            return new Learner(
                    new LearnerId(
                            Long.parseLong(
                                    required(
                                            properties,
                                            "id"
                                    )
                            )
                    ),
                    required(
                            properties,
                            "name"
                    ),
                    new EmailAddress(
                            required(
                                    properties,
                                    "email"
                            )
                    )
            );
        } catch (
            IllegalArgumentException exception
        ) {
            throw new StoredDataCorruptionException(
                    "Invalid learner data in "
                    + path.getFileName()
                    + ".",
                    exception
            );
        }
    }

    private Learner readUnchecked(
            Path path
    ) {
        try {
            return read(
                    path
            );
        } catch (
            IOException exception
        ) {
            throw new StorageException(
                    "Failed to read learner file.",
                    exception
            );
        }
    }

    private Path pathFor(
            LearnerId learnerId
    ) {
        return baseDirectory.resolve(
                learnerId.value()
                + ".properties"
        )
        .normalize();
    }

    private String required(
            Properties properties,
            String key
    ) {
        String value =
                properties.getProperty(
                        key
                );

        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Missing property: "
                    + key
                    + "."
            );
        }

        return value;
    }

    private void replaceFile(
            Path temporary,
            Path target
    ) throws IOException {
        try {
            Files.move(
                    temporary,
                    target,
                    java.nio.file.StandardCopyOption.ATOMIC_MOVE,
                    java.nio.file.StandardCopyOption.REPLACE_EXISTING
            );
        } catch (
            java.nio.file.AtomicMoveNotSupportedException exception
        ) {
            Files.move(
                    temporary,
                    target,
                    java.nio.file.StandardCopyOption.REPLACE_EXISTING
            );
        }
    }
}

Email Lookup Cost

Current:

findByEmail()

loads and scans all learner files।

For this learning project, acceptable।

At scale, database index would be more appropriate।

The important thing is that application code still calls:

findByEmail(...)

regardless of implementation।


3. File Enrollment Repository

Enrollment file:

id=1001
learnerId=1
courseCode=JAVA-OOP
status=ACTIVE

Core deserialization:

return Enrollment.restore(
        new EnrollmentId(
                Long.parseLong(
                        required(
                                properties,
                                "id"
                        )
                )
        ),
        new LearnerId(
                Long.parseLong(
                        required(
                                properties,
                                "learnerId"
                        )
                )
        ),
        new CourseCode(
                required(
                        properties,
                        "courseCode"
                )
        ),
        EnrollmentStatus.valueOf(
                required(
                        properties,
                        "status"
                )
        )
);

Serialization stores:

properties.setProperty(
        "id",
        Long.toString(
                enrollment.getId()
                        .value()
        )
);

properties.setProperty(
        "learnerId",
        Long.toString(
                enrollment.getLearnerId()
                        .value()
        )
);

properties.setProperty(
        "courseCode",
        enrollment.getCourseCode()
                .value()
);

properties.setProperty(
        "status",
        enrollment.getStatus()
                .name()
);

The rest follows the same pattern as learner storage:

Safe path
Temporary file
UTF-8 writer
Atomic replacement
Missing file → null
Invalid stored data → StoredDataCorruptionException
findAll() → list .properties files

existsByLearnerAndCourse()

For our small file repository:

@Override
public boolean existsByLearnerAndCourse(
        LearnerId learnerId,
        CourseCode courseCode
) {
    return findAll()
            .stream()
            .anyMatch(
                    enrollment ->
                            enrollment.getLearnerId()
                                    .equals(
                                            learnerId
                                    )
                            && enrollment.getCourseCode()
                                    .equals(
                                            courseCode
                                    )
            );
}

This is simple but not highly scalable।

That is acceptable for this project।


Wiring File Repositories

Before:

CourseRepository courseRepository =
        new InMemoryCourseRepository();

LearnerRepository learnerRepository =
        new InMemoryLearnerRepository();

EnrollmentRepository enrollmentRepository =
        new InMemoryEnrollmentRepository();

After:

Path dataDirectory =
        Path.of(
                "data"
        );

CourseRepository courseRepository =
        new FileCourseRepository(
                dataDirectory.resolve(
                        "courses"
                )
        );

LearnerRepository learnerRepository =
        new FileLearnerRepository(
                dataDirectory.resolve(
                        "learners"
                )
        );

EnrollmentRepository enrollmentRepository =
        new FileEnrollmentRepository(
                dataDirectory.resolve(
                        "enrollments"
                )
        );

Services remain:

CourseService courseService =
        new CourseService(
                courseRepository
        );

LearnerService learnerService =
        new LearnerService(
                learnerRepository
        );

EnrollmentService enrollmentService =
        new EnrollmentService(
                courseRepository,
                learnerRepository,
                enrollmentRepository
        );

No service changes।


Why This Is Important

CourseService depends on:

CourseRepository

not:

FileCourseRepository

Therefore storage mechanism can change independently।

This is the practical benefit of:

Dependency inversion through a useful boundary

without needing a framework।


Application Restart

First run:

Create course
Register learner
Create enrollment

Files are saved।

Application stops।

Second run:

courseService.findCourse(
        new CourseCode(
                "JAVA-OOP"
        )
);

repository reads the stored file and reconstructs the domain object।


Corrupted Data Example

Suppose:

status=UNKNOWN

Then:

CourseStatus.valueOf(
        "UNKNOWN"
);

fails।

Repository translates that failure into:

StoredDataCorruptionException

Correct behavior is not:

return null;

because the course file exists।


UTF-8

Always use:

StandardCharsets.UTF_8

Example:

Files.newBufferedWriter(
        path,
        StandardCharsets.UTF_8
);

This allows Bengali and other Unicode content to behave consistently across environments।


Resource Ownership

Repository opens:

Reader
Writer
Files.list() stream

Therefore repository closes them।

Use:

try (
    var reader =
            Files.newBufferedReader(...)
) {
}

This protects both success and failure paths।


File Persistence Limitations

Our implementation is suitable for learning and small local applications।

It does not provide:

Database transactions
Indexed queries
Concurrent write protection
Multi-process locking
Optimistic versioning

Atomic Save Does Not Solve Lost Updates

Imagine:

Process A reads Course v1
Process B reads Course v1

A changes and saves v2
B changes and saves its own v2

B may overwrite A।

Atomic replacement only helps ensure:

A single file replacement is not partially visible.

It does not solve application-level concurrency।


Do We Need an Abstract Base Repository?

We repeated helpers such as:

required()
replaceFile()
load properties

We could extract them later।

But do not immediately create:

AbstractFileRepository

just to remove a few repeated lines।

A shared abstraction should represent a real concept, not only reduce line count।


Practice Exercises

Exercise 1

Course file contains:

priceInPaisa=hello

What should happen?

Answer

Parsing fails and repository should surface:

StoredDataCorruptionException

Exercise 2

Requested learner file does not exist।

Answer

Repository returns:

null

and service can convert that to:

LearnerNotFoundException

Exercise 3

Why not return null for every storage error?

Answer

Because these are different:

Not found
Permission failure
Disk failure
Corrupted data

Hiding them behind null destroys useful failure information।


Exercise 4

Why create temp files inside the same base directory?

Answer

It increases the chance that temp and target are on the same filesystem, which helps atomic move support।


Exercise 5

Why does Course.restore() still validate?

Answer

Stored data is external input and can be corrupted।

Persistence must not create invalid domain objects।


Knowledge Check

Question 1

Why do file repositories implement existing repository interfaces?

Question 2

Why translate IOException?

Question 3

What is the difference between missing data and corrupted data?

Question 4

Why use explicit UTF-8?

Question 5

Why use try-with-resources?

Question 6

Why write to a temporary file before replacement?

Question 7

Is ATOMIC_MOVE always supported?

Question 8

Why use Course.restore() and Enrollment.restore()?

Question 9

Does atomic move solve concurrent lost updates?

Question 10

Why can services remain unchanged when storage changes?


Knowledge Check Answers

Answer 1

They provide the same persistence capabilities while changing only the underlying storage mechanism।

Answer 2

So low-level file-system details do not leak into application and domain code।

Answer 3

Missing means no stored entity exists; corrupted means stored data exists but cannot represent a valid entity।

Answer 4

To ensure deterministic text encoding across operating systems and environments।

Answer 5

To reliably close resources on both successful and failing execution paths।

Answer 6

To reduce the chance of leaving the target partially written if serialization fails।

Answer 7

No. A fallback move is required।

Answer 8

To reconstruct persisted lifecycle state without replaying business operations।

Answer 9

No. It only protects individual file replacement।

Answer 10

They depend on repository contracts rather than concrete in-memory or file implementations।


Lesson Summary

এই lesson-এ আমরা file-based persistence introduce করেছি।

Main concepts:

  • StorageException low-level I/O failures translate করে
  • StoredDataCorruptionException invalid stored data represent করে
  • .properties simple explicit serialization provide করে
  • UTF-8 explicitly use করা উচিত
  • Safe paths repository boundary protect করে
  • Try-with-resources file resources reliably close করে
  • Course.restore() এবং Enrollment.restore() stored lifecycle reconstruct করে
  • Restore validation bypass করে না
  • Missing file এবং corrupted file আলাদা failure
  • Temporary file + move partial overwrite risk reduce করে
  • ATOMIC_MOVE useful but not guaranteed
  • File persistence indexed database-এর replacement নয়
  • Atomic replacement concurrency control নয়
  • Most importantly:
Application services remain unchanged
when repository implementation changes.

That proves our repository abstraction is useful.


Next Lesson

পরবর্তী lesson:

Building the Console Application

আমরা implement করব:

  • Interactive menu
  • Scanner
  • Input parsing
  • Course operations
  • Learner operations
  • Enrollment operations
  • User-friendly error handling
  • Console boundary
  • Complete dependency wiring in Main