Final Project
Project Overview
You are viewing a free preview lesson.
Project Overview
এই course-এর Final Project-এ আমরা একটি complete console-based Java application তৈরি করব।
Project-এর নাম:
LiveKlass Course Enrollment System
Applicationটি support করবে:
- Course create করা
- Course-এ lesson add করা
- Course publish করা
- Learner create করা
- Learner-কে published course-এ enroll করা
- Enrollment complete করা
- Course এবং enrollment list করা
- Application data file-এ persist করা
- Application restart-এর পর stored data load করা
- Invalid operations meaningful exceptions দিয়ে reject করা
এই project-এর উদ্দেশ্য শুধু feature তৈরি করা নয়।
আমরা পুরো course-এর concepts একসঙ্গে apply করব:
Java fundamentals
Object-oriented programming
Encapsulation
Composition
Interfaces
Generics
Collections
Enums
Exceptions
File I/O
Repository abstraction
Immutability
Value objects
Records
Equality
Clean methods
Dependency injection
Package organization
What We Are Not Building
এই project intentionally একটি:
Console application
আমরা এখানে ব্যবহার করব না:
Spring Boot
REST API
PostgreSQL
ORM
Docker
Kafka
Microservices
Web frontend
কারণ এই course-এর goal:
Strong Java and object-oriented programming foundation
Backend framework শেখা হবে next stage-এর topic।
Final Project Goal
Project শেষে student-এর এমন codebase থাকা উচিত যেখানে:
Domain rules entities protect করে
Dependencies explicit
Storage details repositories-এর ভিতরে
Collections safely owned
Identifiers strong types
Failures meaningful
Packages organized
Application flow readable
Final code শুধু "works" করলেই হবে না।
এটি demonstrate করবে:
How to design a maintainable Java application.
The Domain
আমাদের application-এর main concepts:
Course
Lesson
Learner
Enrollment
Relationships:
Course
└── contains Lessons
Learner
└── can have Enrollments
Enrollment
├── belongs to one Learner
└── references one Course
High-Level Model
Course
├── CourseCode
├── title
├── price
├── CourseStatus
└── List<Lesson>
Lesson
├── LessonId
├── title
└── content
Learner
├── LearnerId
├── name
└── EmailAddress
Enrollment
├── EnrollmentId
├── LearnerId
├── CourseCode
└── EnrollmentStatus
Course Lifecycle
A new course starts:
DRAFT
Then:
DRAFT
↓
PUBLISHED
↓
ARCHIVED
For this project:
DRAFT → PUBLISHED
PUBLISHED → ARCHIVED
No reverse transition।
Course Rules
A course:
- Must have a valid
CourseCode - Must have a non-blank title
- Cannot have a negative price
- Starts as
DRAFT - Cannot contain duplicate lesson IDs
- Can add lessons only while
DRAFT - Must contain at least one lesson before publication
- Cannot publish twice
- Cannot add lessons after publication
- Can only archive a published course
These rules belong mainly inside:
Course
Lesson Rules
A lesson:
- Must have positive
LessonId - Must have non-blank title
- Must have non-blank content
- Is immutable after creation
A lesson is therefore a strong candidate for an immutable class or record.
For this project, we will use:
record Lesson
because it represents immutable lesson data।
Learner Rules
A learner:
- Has a positive
LearnerId - Has a non-blank name
- Has a valid normalized email address
- Does not expose mutable state
A learner does not need lifecycle mutation in this project।
So it can be modeled as an immutable class or record।
We will use a record।
Enrollment Lifecycle
New enrollment:
ACTIVE
Then:
ACTIVE
├──→ COMPLETED
└──→ CANCELLED
Once:
COMPLETED
it cannot be cancelled।
Once:
CANCELLED
it cannot be completed।
Enrollment Rules
An enrollment:
- Must have valid IDs
- Starts as
ACTIVE - Can complete only while
ACTIVE - Can cancel only while
ACTIVE - Cannot transition after completion/cancellation
These rules belong inside:
Enrollment
Application-Level Enrollment Rules
Some rules require more than one entity।
Example:
Can a learner enroll in this course?
To answer this we need:
Course
Learner
Existing enrollments
Rules:
- Course must exist
- Learner must exist
- Course must be
PUBLISHED - Learner cannot enroll in the same course twice
These rules belong in an application service such as:
EnrollmentService
because one Enrollment object alone does not know all required external state।
Entity vs Value Object
This project deliberately uses both।
Value Objects
Examples:
CourseCode
LessonId
LearnerId
EnrollmentId
EmailAddress
Properties:
Immutable
Validated at creation
Value-based equality
Stable hashCode
Records are strong candidates।
Entities
Examples:
Course
Enrollment
They have:
Identity
Lifecycle
Controlled mutation
Domain behavior
Normal classes fit better।
Read Models
Console output should not need mutable entities everywhere।
We will create immutable summary records:
CourseSummary
EnrollmentSummary
Example:
public record CourseSummary(
CourseCode code,
String title,
long priceInPaisa,
CourseStatus status,
int lessonCount
) {
}
Repository Boundaries
We will have three persistence contracts:
CourseRepository
LearnerRepository
EnrollmentRepository
CourseRepository
Conceptual contract:
public interface CourseRepository {
void save(
Course course
);
Course findByCode(
CourseCode courseCode
);
List<Course> findAll();
}
LearnerRepository
public interface LearnerRepository {
void save(
Learner learner
);
Learner findById(
LearnerId learnerId
);
List<Learner> findAll();
}
EnrollmentRepository
public interface EnrollmentRepository {
void save(
Enrollment enrollment
);
Enrollment findById(
EnrollmentId enrollmentId
);
List<Enrollment> findAll();
}
Later we may add a focused query such as:
boolean existsByLearnerAndCourse(
LearnerId learnerId,
CourseCode courseCode
);
if it genuinely simplifies enrollment rules।
Why Separate Repositories?
Weak design:
public interface Repository {
void saveCourse(...);
void saveLearner(...);
void saveEnrollment(...);
void findCourse(...);
void findLearner(...);
void findEnrollment(...);
}
This mixes unrelated persistence capabilities।
Better:
CourseRepository
LearnerRepository
EnrollmentRepository
Each contract remains cohesive।
File-Based Storage
We will persist data under:
data/
├── courses/
├── learners/
└── enrollments/
Example:
data/
├── courses/
│ ├── java-oop.properties
│ └── backend.properties
│
├── learners/
│ ├── 1.properties
│ └── 2.properties
│
└── enrollments/
├── 1001.properties
└── 1002.properties
Why One File Per Entity?
This design is not intended for a high-scale production system।
It is useful here because it lets us practice:
Path handling
UTF-8
Serialization
Deserialization
Atomic replacement
Directory traversal
Repository abstraction
Exception translation
without introducing database technology।
Course Storage Example
Conceptually:
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 is...
lesson.1.id=2
lesson.1.title=Classes and Objects
lesson.1.content=A class...
We will define serialization carefully during implementation।
Learner Storage Example
id=1
name=Sakib
email=sakib@example.com
Enrollment Storage Example
id=1001
learnerId=1
courseCode=JAVA-OOP
status=ACTIVE
Why Not Store Raw Object Serialization?
Java has built-in object serialization mechanisms, but we will not use them here।
We want:
Readable storage
Explicit serialization logic
Clear file I/O practice
Controlled persistence format
Our repository should understand exactly what gets stored।
Application Services
We will use two main services:
CourseService
EnrollmentService
Learner creation can either get its own small service or be coordinated by LearnerService.
For clarity, we will use:
LearnerService
So final application services:
CourseService
LearnerService
EnrollmentService
CourseService Responsibilities
Use cases:
Create course
Add lesson
Publish course
Archive course
Find course
List courses
The service should coordinate repositories and domain objects।
It should not duplicate rules already owned by Course।
LearnerService Responsibilities
Use cases:
Register learner
Find learner
List learners
It may enforce email uniqueness if we include that repository query।
EnrollmentService Responsibilities
Use cases:
Enroll learner
Complete enrollment
Cancel enrollment
Find enrollment
List enrollments
It coordinates:
CourseRepository
LearnerRepository
EnrollmentRepository
because enrollment creation needs data from all three areas।
Example Enrollment Flow
Console command:
Enroll learner 1 into JAVA-OOP
Application flow:
EnrollmentService
↓
Find learner
↓
Find course
↓
Verify course is published
↓
Check duplicate enrollment
↓
Create Enrollment
↓
Save Enrollment
Where Should the Published Check Live?
Interesting design question।
Course knows whether it is published:
course.isPublished()
But deciding:
Enrollment requires published course
is an enrollment use-case rule involving another entity।
Therefore:
EnrollmentService
can check:
if (
!course.isPublished()
) {
throw new CourseNotAvailableForEnrollmentException(
course.getCode()
);
}
This does not duplicate Course.publish() logic।
It consumes course state for another use case।
ID Strategy
For simplicity, we will create IDs in application memory।
Example:
LearnerId → supplied by user or generated by service
EnrollmentId → generated sequentially
But global mutable static counters are undesirable।
Instead, we can introduce:
IdGenerator
only where it creates meaningful value।
Generic ID Generator
We could create:
public interface LongIdGenerator {
long nextId();
}
Then:
SequentialLongIdGenerator
However, creating one abstraction for every ID type would add complexity।
For this project, we will keep ID creation simple and explicit in the application service or composition root।
The key goal is domain design, not distributed ID generation।
Exception Design
We will create meaningful exceptions for application failures।
Examples:
CourseNotFoundException
DuplicateCourseException
LearnerNotFoundException
DuplicateLearnerException
EnrollmentNotFoundException
DuplicateEnrollmentException
CourseNotAvailableForEnrollmentException
StorageException
StoredDataCorruptionException
Not every failure needs a new class, but important failure categories should be distinguishable।
Storage Exceptions
Repositories should translate:
IOException
into application-facing repository/storage exceptions।
For example:
public class StorageException
extends RuntimeException {
public StorageException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
}
Corrupted Stored Data
Suppose:
status=BROKEN
Repository should not pretend entity is missing।
It should fail with something like:
StoredDataCorruptionException
Missing and corrupted are different conditions।
Missing Entity Handling
Repository methods will use:
null
for simple not-found semantics in this project।
Example:
Course findByCode(
CourseCode code
);
Then services translate absence:
Course course =
repository.findByCode(
code
);
if (course == null) {
throw new CourseNotFoundException(
code
);
}
This keeps repository absence separate from use-case exception semantics।
Package Structure
Our final structure will grow approximately like this:
src/main/java/
└── io/liveklass/
├── Main.java
│
├── course/
│ ├── Course.java
│ ├── CourseCode.java
│ ├── CourseStatus.java
│ ├── CourseSummary.java
│ ├── Lesson.java
│ ├── LessonId.java
│ ├── CourseRepository.java
│ ├── CourseService.java
│ ├── CourseNotFoundException.java
│ └── DuplicateCourseException.java
│
├── learner/
│ ├── Learner.java
│ ├── LearnerId.java
│ ├── EmailAddress.java
│ ├── LearnerRepository.java
│ ├── LearnerService.java
│ └── LearnerNotFoundException.java
│
├── enrollment/
│ ├── Enrollment.java
│ ├── EnrollmentId.java
│ ├── EnrollmentStatus.java
│ ├── EnrollmentSummary.java
│ ├── EnrollmentRepository.java
│ ├── EnrollmentService.java
│ └── EnrollmentNotFoundException.java
│
└── storage/
├── StorageException.java
├── StoredDataCorruptionException.java
├── FileCourseRepository.java
├── FileLearnerRepository.java
└── FileEnrollmentRepository.java
Why Storage Implementations Have Their Own Package
Feature contracts stay near their domain:
course.CourseRepository
learner.LearnerRepository
File-specific implementations live under:
storage
This communicates:
Domain/application code knows the contract
Infrastructure knows file mechanics
Alternative Package Structure
A larger project might use:
course/storage/FileCourseRepository
learner/storage/FileLearnerRepository
That would also be reasonable।
There is no single perfect folder tree।
For this project, one shared:
storage
package keeps infrastructure easy to locate।
Console Layer
We do not want domain or services filled with:
System.out.println(...)
Instead, Main or a dedicated console class will handle interaction।
Conceptually:
Console
↓
Services
↓
Domain + repositories
Console Menu
Final application may show:
=== LiveKlass Course Enrollment System ===
1. Create course
2. Add lesson
3. Publish course
4. List courses
5. Register learner
6. List learners
7. Enroll learner
8. Complete enrollment
9. Cancel enrollment
10. List enrollments
0. Exit
Input Handling
Console layer will:
- Read strings
- Parse numbers
- Build strong domain values
- Call services
- Catch expected application exceptions
- Display user-friendly messages
It should not contain:
Course publication rules
Duplicate enrollment logic
Repository file operations
Example Console Flow
Choose option: 1
Course code: java-oop
Title: Java and OOP Foundation
Price in paisa: 499000
Course created successfully.
Behind the scenes:
CourseCode code =
new CourseCode(
inputCode
);
courseService.createCourse(
code,
title,
price
);
Invalid Input Flow
Input:
Course code:
blank।
CourseCode rejects it:
IllegalArgumentException
Console boundary catches it and displays:
Error: Course code is required.
The domain object never enters invalid state।
Application Layer vs Domain Layer
This project will intentionally distinguish these two ideas।
Domain
Owns:
Course state
Enrollment state
Value validation
State transitions
Invariants
Application Services
Own:
Use-case coordination
Repository lookups
Cross-entity rules
Persistence after changes
Infrastructure
Owns:
Files
Paths
Properties
UTF-8
Serialization
Atomic replacement
Directory creation
IOException translation
Presentation
Owns:
Console input
Console output
Menu
Parsing user interaction
Dependency Direction
Conceptually:
Console
↓
Services
↓
Domain contracts
↑
File repositories
The service sees:
CourseRepository
not:
FileCourseRepository
Composition Root
Main will construct the application:
CourseRepository courseRepository =
new FileCourseRepository(
dataRoot.resolve(
"courses"
)
);
LearnerRepository learnerRepository =
new FileLearnerRepository(
dataRoot.resolve(
"learners"
)
);
EnrollmentRepository enrollmentRepository =
new FileEnrollmentRepository(
dataRoot.resolve(
"enrollments"
)
);
Then:
CourseService courseService =
new CourseService(
courseRepository
);
LearnerService learnerService =
new LearnerService(
learnerRepository
);
EnrollmentService enrollmentService =
new EnrollmentService(
courseRepository,
learnerRepository,
enrollmentRepository
);
This is plain Java dependency injection।
Why Repositories Are Shared
Notice:
CourseRepository
is used by:
CourseService
EnrollmentService
Both receive the same repository instance from Main।
Neither constructs its own repository।
This keeps storage state and configuration consistent।
File Repository Requirements
Each file repository must:
- Validate base directory
- Normalize paths
- Prevent path traversal
- Use UTF-8
- Create required directories
- Use try-with-resources
- Translate
IOException - Distinguish missing entity from storage failure
- Reject corrupted data
- Write through a temporary file
- Use atomic move when supported
- Fall back safely when atomic move is unavailable
Why Atomic Save Matters
Conceptual save:
Serialize object
↓
Write temp file
↓
Move temp over target
instead of:
Overwrite target directly
This reduces partial replacement risk।
Concurrency Limitation
Even with atomic file replacement:
Two processes can still overwrite each other's logical updates.
This project does not attempt to solve distributed concurrency।
That limitation should be explicitly understood।
Persistence Is Not the Domain
Important principle:
Course should not have saveToFile()
Enrollment should not know Path
Learner should not parse Properties
Domain objects should remain independent from file-system mechanics।
Final Project Use Cases
We will implement these use cases.
Use Case 1: Create Course
Input:
Course code
Title
Price
Flow:
Validate CourseCode
Check duplicate
Create Course
Save Course
Use Case 2: Add Lesson
Input:
Course code
Lesson ID
Lesson title
Lesson content
Flow:
Find Course
Create Lesson
Course.addLesson()
Save Course
Use Case 3: Publish Course
Flow:
Find Course
Course.publish()
Save Course
Use Case 4: Archive Course
Flow:
Find Course
Course.archive()
Save Course
Use Case 5: List Courses
Flow:
Repository findAll()
Map Course → CourseSummary
Return immutable list
Use Case 6: Register Learner
Input:
Learner ID
Name
Email
Flow:
Validate ID
Validate EmailAddress
Check duplicate
Create Learner
Save Learner
Use Case 7: Enroll Learner
Input:
Enrollment ID
Learner ID
Course code
Flow:
Find Learner
Find Course
Require PUBLISHED course
Check duplicate learner-course enrollment
Create Enrollment
Save Enrollment
Use Case 8: Complete Enrollment
Flow:
Find Enrollment
Enrollment.complete()
Save Enrollment
Use Case 9: Cancel Enrollment
Flow:
Find Enrollment
Enrollment.cancel()
Save Enrollment
Use Case 10: List Enrollments
Flow:
Find all enrollments
Map to EnrollmentSummary
Return immutable results
Domain Behavior Example
A good Course API should look like:
course.addLesson(
lesson
);
course.publish();
course.archive();
Not:
course.setLessons(...);
course.setStatus(...);
Enrollment API
Good:
enrollment.complete();
enrollment.cancel();
Avoid:
enrollment.setStatus(
EnrollmentStatus.COMPLETED
);
because setters allow bypassing lifecycle rules।
Value Object Example
public record EnrollmentId(
long value
) {
public EnrollmentId {
if (value <= 0) {
throw new IllegalArgumentException(
"Enrollment id must be positive."
);
}
}
}
Once created:
EnrollmentId is always valid.
Equality Strategy
Value objects:
CourseCode
LessonId
LearnerId
EnrollmentId
EmailAddress
will use value equality।
Records make this natural।
Entities:
Course
Enrollment
do not need to participate in hash collections by all mutable fields।
We will avoid blindly generating entity equality over lifecycle state।
Collection Ownership
Course owns:
List<Lesson>
Internal:
new ArrayList<>()
External:
List.copyOf(
lessons
)
Caller cannot mutate internal course structure directly।
Lesson Immutability
Possible definition:
public record Lesson(
LessonId id,
String title,
String content
) {
public Lesson {
if (id == null) {
throw new IllegalArgumentException(
"Lesson id is required."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Lesson title is required."
);
}
if (
content == null
|| content.isBlank()
) {
throw new IllegalArgumentException(
"Lesson content is required."
);
}
title =
title.strip();
content =
content.strip();
}
}
This is a strong immutable component of Course।
Email Value Object
Conceptually:
public record EmailAddress(
String value
) {
}
It will:
- Reject blank values
- Normalize surrounding whitespace
- Apply a deliberately simple validity rule
We will not attempt to reproduce the full email RFC grammar।
The lesson is about domain modeling, not perfect global email validation।
Why Avoid Over-Validation?
Email syntax is more complex than:
value.contains(
"@"
);
but a full standards-compliant validator can also become unnecessary complexity for this learning project।
We will define a clear, limited application rule and document it।
Data Loading
Each repository loads objects when requested।
For:
findAll()
repository:
Lists entity files
Reads each file
Deserializes each entity
Returns domain objects
If one stored file is corrupted, we will fail the operation rather than silently omit it।
Silent data loss is a dangerous default।
Application Startup
On startup:
Repositories initialize directories
No manual preload is required
Entities are read from files when repository methods run।
This keeps startup simple।
Application Shutdown
Because repository operations close their own short-lived resources using try-with-resources:
No long-lived file handles need explicit application shutdown.
Console scanner will still need proper lifecycle handling।
Main Implementation Stages
We will build the final project in stages.
Stage 1: Domain Model
Implement:
CourseCode
LessonId
Lesson
CourseStatus
Course
LearnerId
EmailAddress
Learner
EnrollmentId
EnrollmentStatus
Enrollment
Summary records
Goal:
Valid domain behavior without persistence.
Stage 2: Repository Contracts and Services
Implement:
CourseRepository
LearnerRepository
EnrollmentRepository
CourseService
LearnerService
EnrollmentService
Initially use in-memory repository implementations if useful।
Goal:
Complete use cases independent of file storage.
Stage 3: File Persistence
Replace in-memory implementations with:
FileCourseRepository
FileLearnerRepository
FileEnrollmentRepository
Goal:
Persistence without changing services.
Stage 4: Console Application
Implement:
Menu
Input parsing
Service calls
Error presentation
Goal:
Interactive complete application.
Stage 5: Final Review
Review:
Encapsulation
Immutability
Equality
Exceptions
Dependency design
Storage safety
Package organization
Code smells
Why Build Domain First?
If we begin with:
Files
Menu
Input parsing
business rules quickly become scattered।
Starting with domain objects lets us answer:
What is valid?
What behavior belongs where?
before infrastructure distracts us।
Why In-Memory Before File Storage?
Because we can verify:
Use cases
Domain transitions
Service design
without debugging file serialization at the same time।
Then we replace:
InMemoryCourseRepository
with:
FileCourseRepository
without rewriting application logic।
That demonstrates the value of repository boundaries।
Project Success Criteria
The final application should satisfy all of these:
- Invalid value objects cannot be created
- Course controls lesson mutation
- Course controls publication lifecycle
- Enrollment controls its own status transitions
- Duplicate courses are rejected
- Duplicate learners are rejected
- Duplicate enrollment is rejected
- Only published courses accept enrollment
- Repositories hide storage mechanics
- Services receive repositories through constructors
- File resources are closed correctly
- UTF-8 is explicit
- Stored data survives application restart
- Corrupted storage does not become fake not-found
- Internal mutable collections are not exposed
- Console layer does not contain domain rules
- Domain layer does not contain file-system code
Design Questions Before Coding
Before writing implementation, answer:
Question 1
Should CourseCode be a normal mutable class?
Answer
No।
It represents one validated stable value, so an immutable record is a strong choice।
Question 2
Should Course be a record?
Answer
No।
It owns lifecycle and controlled mutation।
Question 3
Should Lesson be mutable?
Answer
Not for this project।
Lesson is created with complete content and remains immutable।
Question 4
Who should decide if a course can publish?
Answer
Course, because it owns the relevant state and invariant।
Question 5
Who should decide whether a learner may enroll in a particular course?
Answer
EnrollmentService, because the decision requires learner existence, course state, and existing enrollment information।
Question 6
Should CourseService know file names?
Answer
No।
File names belong to the file repository implementation।
Question 7
Should FileCourseRepository decide whether a course may publish?
Answer
No।
That is a domain rule।
Question 8
Should console code directly mutate course status?
Answer
No।
It should call:
courseService.publishCourse(
code
);
Common Final Project Mistakes
Building One Huge Application Class
This recreates the god-class problem।
Passing Raw Strings Everywhere
Important domain concepts should use strong types।
Adding Setters for Convenience
Setters can bypass domain rules।
Putting Validation Only in Console Input
Objects should protect their own fundamental invariants।
Making Repositories Responsible for Business Rules
Repositories persist; they do not decide domain policy।
Making Services Responsible for Every Entity Rule
Services coordinate; entities should protect their own local state transitions।
Returning Mutable Collections
Leaks internal state।
Catching Every Exception and Returning null
Destroys failure meaning।
Using IOException Throughout the Application
Leaks storage implementation details।
Creating Interfaces for Every Tiny Helper
Adds architecture without value।
Practice Before Implementation
Design these method signatures without looking ahead.
Course
What methods should it expose?
Suggested:
void addLesson(
Lesson lesson
);
void publish();
void archive();
boolean isPublished();
List<Lesson> getLessons();
CourseSummary summary();
Enrollment
Suggested:
void complete();
void cancel();
boolean isActive();
EnrollmentSummary summary();
CourseService
Suggested:
void createCourse(...);
void addLesson(...);
void publishCourse(...);
void archiveCourse(...);
CourseSummary findCourse(...);
List<CourseSummary> findAllCourses();
EnrollmentService
Suggested:
void enroll(...);
void completeEnrollment(...);
void cancelEnrollment(...);
EnrollmentSummary findEnrollment(...);
List<EnrollmentSummary> findAllEnrollments();
Architecture Sketch
Final application conceptually:
┌─────────────────┐
│ Console │
└────────┬────────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
CourseService LearnerService EnrollmentService
│ │ │
│ │ ┌────┴────┐
│ │ │ │
▼ ▼ ▼ ▼
CourseRepo LearnerRepo CourseRepo EnrollmentRepo
│ │ │
▼ ▼ ▼
File Course File Learner File Enrollment
Storage Storage Storage
Domain objects sit behind these use cases and enforce their own invariants।
Final Project Mindset
While implementing, repeatedly ask:
Who owns this rule?
Who owns this state?
Who owns this resource?
Who should know this dependency?
Can invalid state be constructed?
Can a caller bypass this rule?
Is this abstraction solving a real problem?
These questions matter more than simply producing more classes।
Lesson Summary
এই lesson-এ আমরা Final Project design করেছি।
আমরা established করেছি:
- Project হবে console-based course enrollment application
- Main entities are
Course,Learner, andEnrollment Lessonbelongs toCourse- Value objects will represent domain identifiers
- Records will model immutable values and summaries
- Normal classes will model lifecycle-heavy entities
- Course owns its own publication lifecycle
- Enrollment owns completion/cancellation transitions
- Cross-entity enrollment rules belong in
EnrollmentService - Repository interfaces hide persistence mechanics
- File repositories handle UTF-8, paths, serialization, and exception translation
- Services receive repositories through constructor injection
- Console remains a presentation boundary
Mainacts as composition root- Development will proceed domain-first, then services, persistence, and console
- The project intentionally avoids Spring and databases so Java/OOP design remains the focus
The central architecture is:
Console
→ Application Services
→ Domain + Repository Contracts
→ File Storage Implementations
The most important success criterion is not:
How many features did we build?
It is:
Can the code clearly express
who owns each responsibility?
Next Lesson
পরবর্তী lesson:
Building the Domain Model
আমরা complete domain layer implement করব:
CourseCodeLessonIdLessonCourseStatusCourseCourseSummaryLearnerIdEmailAddressLearnerEnrollmentIdEnrollmentStatusEnrollmentEnrollmentSummary
Focus থাকবে:
Validation
Immutability
Encapsulation
Equality
State transitions
Collection ownership