Final Project

Building Repository Contracts and Application Services

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

আগের lesson-এ আমরা Final Project-এর domain model তৈরি করেছি:

Course
Lesson
Learner
Enrollment

এবার আমরা application layer তৈরি করব।

এই lesson-এ implement করব:

  • CourseRepository
  • LearnerRepository
  • EnrollmentRepository
  • In-memory repository implementations
  • Application exceptions
  • CourseService
  • LearnerService
  • EnrollmentService
  • Duplicate detection
  • Cross-entity enrollment rules
  • Constructor injection

এখনও file storage ব্যবহার করব না।

প্রথমে goal:

Domain + use cases

correctly কাজ করছে কিনা verify করা।

পরের lesson-এ একই repository contracts ব্যবহার করে file persistence implement করব।


Why Start with In-Memory Storage?

যদি domain logic এবং file I/O একসঙ্গে build করি, bug কোথায় হচ্ছে বোঝা কঠিন হতে পারে।

Problem হতে পারে:

Business rule
Repository lookup
Path handling
Serialization
File parsing

Better progression:

Domain
↓
Repository contracts
↓
In-memory repositories
↓
Services
↓
File repositories

এভাবে complexity ধীরে ধীরে বাড়ে।


Repository as a Contract

Repository application-কে বলে:

কোন persistence capability available?

It does not say:

Data কীভাবে stored হবে?

Example:

Course findByCode(
        CourseCode courseCode
);

Implementation হতে পারে:

HashMap
File
Database

Service-এর জানার প্রয়োজন নেই।


1. Course Repository

CourseRepository.java

package io.liveklass.course;

import java.util.List;

public interface CourseRepository {

    void save(
            Course course
    );

    Course findByCode(
            CourseCode courseCode
    );

    List<Course> findAll();
}

Current use cases-এর জন্য এটুকুই যথেষ্ট।


2. Learner Repository

Learner registration-এর সময় আমরা check করব:

Duplicate learner ID
Duplicate email

তাই repository-তে email lookup দরকার।

LearnerRepository.java

package io.liveklass.learner;

import java.util.List;

public interface LearnerRepository {

    void save(
            Learner learner
    );

    Learner findById(
            LearnerId learnerId
    );

    Learner findByEmail(
            EmailAddress email
    );

    List<Learner> findAll();
}

Why findByEmail()?

Service নিজে এমন করতে পারত:

repository.findAll()
        .stream()
        .filter(...)

কিন্তু email lookup persistence capability।

Better:

repository.findByEmail(
        email
);

Repository contract actual use case reflect করে।


3. Enrollment Repository

Enrollment creation-এর একটি important rule:

এক learner একই course-এ দ্বিতীয়বার enroll করতে পারবে না।

EnrollmentRepository.java

package io.liveklass.enrollment;

import io.liveklass.course.CourseCode;
import io.liveklass.learner.LearnerId;

import java.util.List;

public interface EnrollmentRepository {

    void save(
            Enrollment enrollment
    );

    Enrollment findById(
            EnrollmentId enrollmentId
    );

    boolean existsByLearnerAndCourse(
            LearnerId learnerId,
            CourseCode courseCode
    );

    List<Enrollment> findAll();
}

Repository Methods Should Come from Real Needs

Do not design dozens of hypothetical methods upfront।

Good:

existsByLearnerAndCourse(...)

because current business rule requires it।

Do not add:

findByStatusAndCourseAndLearnerAndDate...

until a real use case requires it।


Application Exceptions

Meaningful exception types help distinguish failure categories।

We will use:

CourseNotFoundException
DuplicateCourseException
LearnerNotFoundException
DuplicateLearnerException
EnrollmentNotFoundException
DuplicateEnrollmentException
DuplicateEnrollmentIdException
CourseNotAvailableForEnrollmentException

CourseNotFoundException.java

package io.liveklass.course;

public final class CourseNotFoundException
        extends RuntimeException {

    public CourseNotFoundException(
            CourseCode courseCode
    ) {
        super(
                "Course not found: "
                + courseCode
                + "."
        );
    }
}

DuplicateCourseException.java

package io.liveklass.course;

public final class DuplicateCourseException
        extends RuntimeException {

    public DuplicateCourseException(
            CourseCode courseCode
    ) {
        super(
                "Course already exists: "
                + courseCode
                + "."
        );
    }
}

LearnerNotFoundException.java

package io.liveklass.learner;

public final class LearnerNotFoundException
        extends RuntimeException {

    public LearnerNotFoundException(
            LearnerId learnerId
    ) {
        super(
                "Learner not found: "
                + learnerId
                + "."
        );
    }
}

DuplicateLearnerException.java

package io.liveklass.learner;

public final class DuplicateLearnerException
        extends RuntimeException {

    public DuplicateLearnerException(
            String message
    ) {
        super(
                message
        );
    }
}

EnrollmentNotFoundException.java

package io.liveklass.enrollment;

public final class EnrollmentNotFoundException
        extends RuntimeException {

    public EnrollmentNotFoundException(
            EnrollmentId enrollmentId
    ) {
        super(
                "Enrollment not found: "
                + enrollmentId
                + "."
        );
    }
}

DuplicateEnrollmentException.java

package io.liveklass.enrollment;

import io.liveklass.course.CourseCode;
import io.liveklass.learner.LearnerId;

public final class DuplicateEnrollmentException
        extends RuntimeException {

    public DuplicateEnrollmentException(
            LearnerId learnerId,
            CourseCode courseCode
    ) {
        super(
                "Learner "
                + learnerId
                + " is already enrolled in course "
                + courseCode
                + "."
        );
    }
}

DuplicateEnrollmentIdException.java

package io.liveklass.enrollment;

public final class DuplicateEnrollmentIdException
        extends RuntimeException {

    public DuplicateEnrollmentIdException(
            EnrollmentId enrollmentId
    ) {
        super(
                "Enrollment id already exists: "
                + enrollmentId
                + "."
        );
    }
}

CourseNotAvailableForEnrollmentException.java

package io.liveklass.enrollment;

import io.liveklass.course.CourseCode;

public final class CourseNotAvailableForEnrollmentException
        extends RuntimeException {

    public CourseNotAvailableForEnrollmentException(
            CourseCode courseCode
    ) {
        super(
                "Course is not available for enrollment: "
                + courseCode
                + "."
        );
    }
}

In-Memory Repositories

Now repository contracts-এর simple implementations তৈরি করি।


4. In-Memory Course Repository

InMemoryCourseRepository.java

package io.liveklass.course.storage;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseRepository;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class InMemoryCourseRepository
        implements CourseRepository {

    private final Map<CourseCode, Course> courses =
            new LinkedHashMap<>();

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

        courses.put(
                course.getCode(),
                course
        );
    }

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

        return courses.get(
                courseCode
        );
    }

    @Override
    public List<Course> findAll() {
        return new ArrayList<>(
                courses.values()
        );
    }
}

LinkedHashMap insertion order preserve করে, তাই console output predictable হবে।


5. In-Memory Learner Repository

InMemoryLearnerRepository.java

package io.liveklass.learner.storage;

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

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class InMemoryLearnerRepository
        implements LearnerRepository {

    private final Map<LearnerId, Learner> learners =
            new LinkedHashMap<>();

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

        learners.put(
                learner.id(),
                learner
        );
    }

    @Override
    public Learner findById(
            LearnerId learnerId
    ) {
        if (learnerId == null) {
            throw new IllegalArgumentException(
                    "Learner id is required."
            );
        }

        return learners.get(
                learnerId
        );
    }

    @Override
    public Learner findByEmail(
            EmailAddress email
    ) {
        if (email == null) {
            throw new IllegalArgumentException(
                    "Email is required."
            );
        }

        return learners.values()
                .stream()
                .filter(
                        learner ->
                                learner.email()
                                        .equals(
                                                email
                                        )
                )
                .findFirst()
                .orElse(
                        null
                );
    }

    @Override
    public List<Learner> findAll() {
        return new ArrayList<>(
                learners.values()
        );
    }
}

6. In-Memory Enrollment Repository

InMemoryEnrollmentRepository.java

package io.liveklass.enrollment.storage;

import io.liveklass.course.CourseCode;
import io.liveklass.enrollment.Enrollment;
import io.liveklass.enrollment.EnrollmentId;
import io.liveklass.enrollment.EnrollmentRepository;
import io.liveklass.learner.LearnerId;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public final class InMemoryEnrollmentRepository
        implements EnrollmentRepository {

    private final Map<EnrollmentId, Enrollment> enrollments =
            new LinkedHashMap<>();

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

        enrollments.put(
                enrollment.getId(),
                enrollment
        );
    }

    @Override
    public Enrollment findById(
            EnrollmentId enrollmentId
    ) {
        if (enrollmentId == null) {
            throw new IllegalArgumentException(
                    "Enrollment id is required."
            );
        }

        return enrollments.get(
                enrollmentId
        );
    }

    @Override
    public boolean existsByLearnerAndCourse(
            LearnerId learnerId,
            CourseCode courseCode
    ) {
        if (
                learnerId == null
                || courseCode == null
        ) {
            throw new IllegalArgumentException(
                    "Learner id and course code are required."
            );
        }

        return enrollments.values()
                .stream()
                .anyMatch(
                        enrollment ->
                                enrollment.getLearnerId()
                                        .equals(
                                                learnerId
                                        )
                                && enrollment.getCourseCode()
                                        .equals(
                                                courseCode
                                        )
                );
    }

    @Override
    public List<Enrollment> findAll() {
        return new ArrayList<>(
                enrollments.values()
        );
    }
}

What Repositories Should Not Do

Repositories should not decide:

Can a course publish?
Can an enrollment complete?
Can a learner enroll in a draft course?

They provide persistence operations।

Domain objects and application services own business behavior।


7. Course Service

Use cases:

Create course
Add lesson
Publish
Archive
Find
List

CourseService.java

package io.liveklass.course;

import java.util.List;

public final class CourseService {

    private final CourseRepository repository;

    public CourseService(
            CourseRepository repository
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Course repository is required."
            );
        }

        this.repository =
                repository;
    }

    public void createCourse(
            CourseCode courseCode,
            String title,
            long priceInPaisa
    ) {
        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        if (
                repository.findByCode(
                        courseCode
                )
                != null
        ) {
            throw new DuplicateCourseException(
                    courseCode
            );
        }

        repository.save(
                new Course(
                        courseCode,
                        title,
                        priceInPaisa
                )
        );
    }

    public void addLesson(
            CourseCode courseCode,
            Lesson lesson
    ) {
        Course course =
                requireCourse(
                        courseCode
                );

        course.addLesson(
                lesson
        );

        repository.save(
                course
        );
    }

    public void publishCourse(
            CourseCode courseCode
    ) {
        Course course =
                requireCourse(
                        courseCode
                );

        course.publish();

        repository.save(
                course
        );
    }

    public void archiveCourse(
            CourseCode courseCode
    ) {
        Course course =
                requireCourse(
                        courseCode
                );

        course.archive();

        repository.save(
                course
        );
    }

    public CourseSummary findCourse(
            CourseCode courseCode
    ) {
        return requireCourse(
                courseCode
        )
        .summary();
    }

    public List<CourseSummary> findAllCourses() {
        return repository.findAll()
                .stream()
                .map(
                        Course::summary
                )
                .toList();
    }

    private Course requireCourse(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        Course course =
                repository.findByCode(
                        courseCode
                );

        if (course == null) {
            throw new CourseNotFoundException(
                    courseCode
            );
        }

        return course;
    }
}

Service Does Not Duplicate Domain Rules

Notice:

publishCourse(...)

does not manually check:

Course status
Lesson count

It simply calls:

course.publish();

Course owns those invariants।

Service owns:

Find
Coordinate
Persist

8. Learner Service

Rules:

Learner ID unique
Email unique

LearnerService.java

package io.liveklass.learner;

import java.util.List;

public final class LearnerService {

    private final LearnerRepository repository;

    public LearnerService(
            LearnerRepository repository
    ) {
        if (repository == null) {
            throw new IllegalArgumentException(
                    "Learner repository is required."
            );
        }

        this.repository =
                repository;
    }

    public void registerLearner(
            LearnerId learnerId,
            String name,
            EmailAddress email
    ) {
        if (
                learnerId == null
                || email == null
        ) {
            throw new IllegalArgumentException(
                    "Learner id and email are required."
            );
        }

        if (
                repository.findById(
                        learnerId
                )
                != null
        ) {
            throw new DuplicateLearnerException(
                    "Learner id already exists: "
                    + learnerId
                    + "."
            );
        }

        if (
                repository.findByEmail(
                        email
                )
                != null
        ) {
            throw new DuplicateLearnerException(
                    "Learner email already exists: "
                    + email
                    + "."
            );
        }

        repository.save(
                new Learner(
                        learnerId,
                        name,
                        email
                )
        );
    }

    public Learner findLearner(
            LearnerId learnerId
    ) {
        Learner learner =
                repository.findById(
                        learnerId
                );

        if (learner == null) {
            throw new LearnerNotFoundException(
                    learnerId
            );
        }

        return learner;
    }

    public List<Learner> findAllLearners() {
        return List.copyOf(
                repository.findAll()
        );
    }
}

Learner immutable হওয়ায় service থেকে সরাসরি return করা safe।


9. Enrollment Service

এই service cross-entity rules coordinate করবে।

Enrollment creation-এর আগে:

  1. Enrollment ID unique হতে হবে
  2. Learner exist করতে হবে
  3. Course exist করতে হবে
  4. Course published হতে হবে
  5. Same learner-course enrollment already থাকা যাবে না

EnrollmentService.java

package io.liveklass.enrollment;

import io.liveklass.course.Course;
import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseNotFoundException;
import io.liveklass.course.CourseRepository;
import io.liveklass.learner.Learner;
import io.liveklass.learner.LearnerId;
import io.liveklass.learner.LearnerNotFoundException;
import io.liveklass.learner.LearnerRepository;

import java.util.List;

public final class EnrollmentService {

    private final CourseRepository courseRepository;
    private final LearnerRepository learnerRepository;
    private final EnrollmentRepository enrollmentRepository;

    public EnrollmentService(
            CourseRepository courseRepository,
            LearnerRepository learnerRepository,
            EnrollmentRepository enrollmentRepository
    ) {
        if (
                courseRepository == null
                || learnerRepository == null
                || enrollmentRepository == null
        ) {
            throw new IllegalArgumentException(
                    "Enrollment service repositories are required."
            );
        }

        this.courseRepository =
                courseRepository;

        this.learnerRepository =
                learnerRepository;

        this.enrollmentRepository =
                enrollmentRepository;
    }

    public void enroll(
            EnrollmentId enrollmentId,
            LearnerId learnerId,
            CourseCode courseCode
    ) {
        if (
                enrollmentId == null
                || learnerId == null
                || courseCode == null
        ) {
            throw new IllegalArgumentException(
                    "Enrollment id, learner id, and course code are required."
            );
        }

        if (
                enrollmentRepository.findById(
                        enrollmentId
                )
                != null
        ) {
            throw new DuplicateEnrollmentIdException(
                    enrollmentId
            );
        }

        requireLearner(
                learnerId
        );

        Course course =
                requireCourse(
                        courseCode
                );

        if (
                !course.isPublished()
        ) {
            throw new CourseNotAvailableForEnrollmentException(
                    courseCode
            );
        }

        if (
                enrollmentRepository
                        .existsByLearnerAndCourse(
                                learnerId,
                                courseCode
                        )
        ) {
            throw new DuplicateEnrollmentException(
                    learnerId,
                    courseCode
            );
        }

        enrollmentRepository.save(
                new Enrollment(
                        enrollmentId,
                        learnerId,
                        courseCode
                )
        );
    }

    public void completeEnrollment(
            EnrollmentId enrollmentId
    ) {
        Enrollment enrollment =
                requireEnrollment(
                        enrollmentId
                );

        enrollment.complete();

        enrollmentRepository.save(
                enrollment
        );
    }

    public void cancelEnrollment(
            EnrollmentId enrollmentId
    ) {
        Enrollment enrollment =
                requireEnrollment(
                        enrollmentId
                );

        enrollment.cancel();

        enrollmentRepository.save(
                enrollment
        );
    }

    public EnrollmentSummary findEnrollment(
            EnrollmentId enrollmentId
    ) {
        return requireEnrollment(
                enrollmentId
        )
        .summary();
    }

    public List<EnrollmentSummary> findAllEnrollments() {
        return enrollmentRepository.findAll()
                .stream()
                .map(
                        Enrollment::summary
                )
                .toList();
    }

    private Learner requireLearner(
            LearnerId learnerId
    ) {
        Learner learner =
                learnerRepository.findById(
                        learnerId
                );

        if (learner == null) {
            throw new LearnerNotFoundException(
                    learnerId
            );
        }

        return learner;
    }

    private Course requireCourse(
            CourseCode courseCode
    ) {
        Course course =
                courseRepository.findByCode(
                        courseCode
                );

        if (course == null) {
            throw new CourseNotFoundException(
                    courseCode
            );
        }

        return course;
    }

    private Enrollment requireEnrollment(
            EnrollmentId enrollmentId
    ) {
        if (enrollmentId == null) {
            throw new IllegalArgumentException(
                    "Enrollment id is required."
            );
        }

        Enrollment enrollment =
                enrollmentRepository.findById(
                        enrollmentId
                );

        if (enrollment == null) {
            throw new EnrollmentNotFoundException(
                    enrollmentId
            );
        }

        return enrollment;
    }
}

Why Enrollment Needs Three Repositories

The enrollment use case needs to answer:

Does learner exist?
Does course exist?
Is course published?
Is learner already enrolled?

No single Enrollment object owns all this information।

Therefore:

EnrollmentService

coordinates the required collaborators।


Domain Rule vs Cross-Entity Rule

Useful rule:

Rule depends only on one entity's state
→ entity usually owns it

Rule requires multiple entities/repositories
→ application service usually coordinates it

Examples:

ACTIVE → COMPLETED

belongs to:

Enrollment.complete()

But:

Only published courses accept enrollment

belongs to enrollment use-case coordination।


End-to-End Example

Now we can run the application entirely in memory।

CourseRepository courseRepository =
        new InMemoryCourseRepository();

LearnerRepository learnerRepository =
        new InMemoryLearnerRepository();

EnrollmentRepository enrollmentRepository =
        new InMemoryEnrollmentRepository();

CourseService courseService =
        new CourseService(
                courseRepository
        );

LearnerService learnerService =
        new LearnerService(
                learnerRepository
        );

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

Create and Publish Course

CourseCode courseCode =
        new CourseCode(
                "java-oop"
        );

courseService.createCourse(
        courseCode,
        "Java and OOP Foundation",
        499_000L
);

courseService.addLesson(
        courseCode,
        new Lesson(
                new LessonId(
                        1L
                ),
                "Introduction to Java",
                "Java is a general-purpose programming language."
        )
);

courseService.publishCourse(
        courseCode
);

Register Learner

LearnerId learnerId =
        new LearnerId(
                1L
        );

learnerService.registerLearner(
        learnerId,
        "Sakib",
        new EmailAddress(
                "sakib@example.com"
        )
);

Create Enrollment

EnrollmentId enrollmentId =
        new EnrollmentId(
                1001L
        );

enrollmentService.enroll(
        enrollmentId,
        learnerId,
        courseCode
);

Complete Enrollment

enrollmentService.completeEnrollment(
        enrollmentId
);

Result:

COMPLETED

What Happens with a Draft Course?

If course has not been published:

enrollmentService.enroll(...)

throws:

CourseNotAvailableForEnrollmentException

What Happens with Missing Learner?

Throws:

LearnerNotFoundException

What Happens with Duplicate Enrollment?

If same learner tries another enrollment for the same course:

DuplicateEnrollmentException

is thrown।

Our current rule treats even a previous cancelled enrollment as an existing relationship।

If requirements later allow re-enrollment after cancellation, repository query semantics must change।


Constructor Injection

Notice:

new CourseService(
        courseRepository
);

and:

new EnrollmentService(
        courseRepository,
        learnerRepository,
        enrollmentRepository
);

Services do not construct their own repositories।

This is plain Java dependency injection।

No framework is required।


Why This Design Is Ready for File Storage

Currently:

CourseRepository courseRepository =
        new InMemoryCourseRepository();

Next lesson we can use:

CourseRepository courseRepository =
        new FileCourseRepository(
                path
        );

CourseService will remain unchanged।

Same for learner and enrollment services।

That is the practical value of depending on repository contracts।


Common Mistakes

Service Constructs File Repository

Bad:

private final FileCourseRepository repository =
        new FileCourseRepository(...);

This couples application behavior to file storage।


Service Reimplements Entity Rules

Bad:

if (
        enrollment.getStatus()
        == ACTIVE
) {
    enrollment.setStatus(
            COMPLETED
    );
}

Better:

enrollment.complete();

Entity Calls Repository

Bad:

enrollment.complete(
        repository
);

Entity should change its state; service coordinates persistence।


Repository Makes Business Decisions

Repository should not decide whether a course may publish।


Generic Repository for Everything

Avoid:

Object find(
        String type,
        String id
);

Focused repository contracts preserve type safety and meaning।


Practice Exercises

Exercise 1

Create the same course twice using:

java
JAVA

What should happen?

Answer

Second creation throws:

DuplicateCourseException

because CourseCode normalizes both to the same value।


Exercise 2

Register two learners using:

subu@example.com
SUBU@EXAMPLE.COM

What happens?

Answer

Second registration fails under our email normalization policy।


Exercise 3

Try enrolling a learner before publishing the course।

Answer

CourseNotAvailableForEnrollmentException

Exercise 4

Complete the same enrollment twice।

Answer

Second call fails because Enrollment.complete() only accepts ACTIVE state।


Exercise 5

Could FileCourseRepository replace InMemoryCourseRepository without changing CourseService?

Answer

Yes, provided it correctly implements:

CourseRepository

Knowledge Check

Question 1

What does a repository interface represent?

Question 2

Why use in-memory repositories first?

Question 3

Why does learner repository support email lookup?

Question 4

Why does enrollment repository expose existsByLearnerAndCourse()?

Question 5

Who owns course publication rules?

Question 6

Who checks whether a course is published before enrollment?

Question 7

Who owns ACTIVE → COMPLETED transition?

Question 8

Why do services save after domain mutation?

Question 9

Why should services depend on repository interfaces?

Question 10

Does constructor injection require Spring?


Answers

Answer 1

It describes persistence capabilities required by application code without exposing the storage implementation।

Answer 2

To verify domain and application behavior before adding file-system complexity।

Answer 3

Because learner registration must enforce email uniqueness।

Answer 4

Because duplicate learner-course enrollment is a real use-case rule।

Answer 5

Course

Answer 6

EnrollmentService, because the rule spans the enrollment use case and course state।

Answer 7

Enrollment

Answer 8

The entity changes in memory; the application service coordinates persistence of the resulting state।

Answer 9

So storage implementations can change without rewriting use-case logic।

Answer 10

No. Constructor injection is plain Java।


Lesson Summary

এই lesson-এ আমরা implement করেছি:

CourseRepository
LearnerRepository
EnrollmentRepository

InMemoryCourseRepository
InMemoryLearnerRepository
InMemoryEnrollmentRepository

CourseService
LearnerService
EnrollmentService

এবং meaningful application failures।

আমরা শিখেছি:

  • Repository contracts storage mechanism hide করে
  • Repository methods real application needs থেকে derive করা উচিত
  • In-memory implementations business logic test করা সহজ করে
  • Application services use-case orchestration করে
  • Domain entities নিজের local state transitions protect করে
  • Cross-entity rules services coordinate করে
  • Constructor injection dependencies explicit করে
  • Services concrete storage implementation-এর ওপর depend করে না
  • Duplicate entity ID এবং duplicate relationship different failure হতে পারে
  • Repository implementation change করলেও service design unchanged থাকতে পারে

Core application flow:

Find required entities
→ enforce cross-entity rules
→ invoke domain behavior
→ persist resulting state

Next Lesson

পরবর্তী lesson:

Implementing File-Based Persistence

আমরা implement করব:

  • FileCourseRepository
  • FileLearnerRepository
  • FileEnrollmentRepository
  • Safe paths
  • UTF-8
  • Properties
  • Serialization and deserialization
  • restore()
  • Missing-file handling
  • Corrupted-data handling
  • Atomic replacement
  • findAll()
  • Repository-specific queries

এবং সবচেয়ে গুরুত্বপূর্ণ:

CourseService
LearnerService
EnrollmentService

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