Final Project
Final Project Review and Hardening
You are viewing a free preview lesson.
Lesson Overview
Final Project এখন functionally complete।
আমাদের application পারে:
- Course create করতে
- Lesson add করতে
- Course publish/archive করতে
- Learner register করতে
- Learner enroll করতে
- Enrollment complete/cancel করতে
- Data file-এ persist করতে
- Application restart-এর পর data reload করতে
- Console interface দিয়ে operations চালাতে
এখন final step হলো:
Review
Harden
Refactor
Verify
Working software এবং maintainable software এক জিনিস নয়।
এই lesson-এ আমরা পুরো projectকে professional engineering perspective থেকে review করব।
Focus থাকবে:
- Domain invariants
- Immutability
- Equality
- Collection ownership
- Package boundaries
- Repository contracts
- File safety
- Exception semantics
- Dependency design
- Console boundary
- Common bugs
- Refactoring opportunities
- Final verification checklist
Final Architecture
Current application:
Console
↓
Application Services
↓
Domain + Repository Contracts
↓
File Repository Implementations
Main components:
Course
Learner
Enrollment
Lesson
CourseService
LearnerService
EnrollmentService
CourseRepository
LearnerRepository
EnrollmentRepository
FileCourseRepository
FileLearnerRepository
FileEnrollmentRepository
Main acts as:
Composition Root
Review Principle 1: Can Invalid State Exist?
Strong domain design tries to make invalid objects difficult to create।
Example:
new CourseCode(
""
);
must fail।
Similarly:
new LessonId(
-10
);
must fail।
And:
new EnrollmentId(
0
);
must fail।
These validations already belong inside value objects।
Review CourseCode
Expected guarantees:
Not null
Not blank
Normalized
Uppercase
Only supported characters
Stable equality
Stable hashCode
Once constructed:
CourseCode code
downstream code should be able to trust it।
Do not repeatedly validate:
code.value()
.isBlank();
in every service।
That defeats the purpose of a strong value type।
Review Course
Course owns:
Title
Price
Status
Lessons
Key invariants:
Price >= 0
Lesson IDs unique
Only DRAFT accepts lessons
Only DRAFT can publish
Published course must contain lessons
Only PUBLISHED can archive
The important question:
Can callers bypass these rules?
Avoid Public Setters
Bad:
course.setStatus(
CourseStatus.PUBLISHED
);
This allows publication without checking lesson count।
Better:
course.publish();
The method expresses intent and protects invariants।
Review Collection Ownership
Internal:
private final List<Lesson> lessons =
new ArrayList<>();
External:
public List<Lesson> getLessons() {
return List.copyOf(
lessons
);
}
This prevents:
course.getLessons()
.clear();
from mutating internal state।
Is List.copyOf() Enough?
For this project:
Lesson
is immutable।
Therefore:
Unmodifiable list
+
Immutable elements
gives us strong protection।
If Lesson were mutable, List.copyOf() would only protect collection structure, not element mutation।
Review Enrollment
Lifecycle:
ACTIVE
├──→ COMPLETED
└──→ CANCELLED
Invalid transitions:
COMPLETED → CANCELLED
CANCELLED → COMPLETED
COMPLETED → COMPLETED
CANCELLED → CANCELLED
are rejected by:
complete()
cancel()
This is correct entity ownership।
Review Learner
Learner is immutable in this project।
That works because current requirements do not include:
Change name
Change email
Suspend learner
If requirements later introduce lifecycle behavior, reconsider whether record Learner is still the best representation।
Design follows behavior, not fashion।
Review Equality
Value objects:
CourseCode
LessonId
LearnerId
EnrollmentId
EmailAddress
use record value equality।
This makes them excellent candidates for:
Map keys
Set elements
Lookup values
because their equality-related state is immutable।
Entity Equality
We intentionally did not create custom:
Course.equals()
Enrollment.equals()
using all fields।
Why?
Because they contain mutable lifecycle state।
If equality included:
status
lessons
title
then mutation could change equality and hash behavior।
Instead, repositories use stable identity objects:
CourseCode
EnrollmentId
Review Repository Contracts
Our contracts are focused।
Course
save()
findByCode()
findAll()
Learner
save()
findById()
findByEmail()
findAll()
Enrollment
save()
findById()
existsByLearnerAndCourse()
findAll()
Each method exists because a real use case needs it।
Avoid Generic Repository Design
Do not replace these with:
public interface Repository<T, ID> {
...
}
just because generics exist।
A generic base can sometimes reduce duplication, but it can also hide domain-specific capabilities such as:
findByEmail()
existsByLearnerAndCourse()
For this project, focused repositories are clearer।
Review Service Responsibilities
CourseService
Should coordinate:
Lookup
Creation
Domain behavior
Persistence
It should not implement internal Course rules।
Good:
course.publish();
repository.save(
course
);
Bad:
if (
course.getLessons()
.size()
> 0
) {
course.setStatus(
PUBLISHED
);
}
LearnerService
Owns registration-level rules:
Unique ID
Unique email
These rules require repository state, so they belong outside immutable Learner itself।
EnrollmentService
Owns cross-entity coordination:
Learner exists
Course exists
Course is published
No duplicate enrollment
Then creates:
Enrollment
The entity itself owns subsequent lifecycle changes।
Review Duplicate Enrollment Semantics
Current repository method:
existsByLearnerAndCourse(
learnerId,
courseCode
);
means:
Any previous enrollment blocks another one.
That includes:
ACTIVE
COMPLETED
CANCELLED
This is a deliberate business rule।
If requirement changes to:
Cancelled enrollment may enroll again
then method semantics should change explicitly।
Example:
existsActiveOrCompletedByLearnerAndCourse(...)
Do not silently change implementation while leaving an inaccurate method name।
Review File Storage
File repositories are responsible for:
Directory creation
Path building
Properties serialization
UTF-8
Reading
Writing
Atomic replacement
I/O exception translation
Stored-data validation
Domain classes should know none of these details।
Missing vs Failure
A critical distinction:
No file
may mean:
return null;
But:
Permission denied
Malformed number
Unknown enum
Missing required property
must not become:
null
Otherwise the application may claim:
Course not found
when the disk is actually broken।
Review Stored Data Corruption
Examples of corruption:
priceInPaisa=abc
status=UNKNOWN
lesson.count=-1
id=
These should result in:
StoredDataCorruptionException
rather than silently skipping the entity।
Why Fail on Corruption?
Suppose findAll() sees 20 course files and one is broken।
Silently skipping it would return 19 courses and make data appear lost।
Failing loudly communicates:
Storage requires attention.
This is safer by default।
Review Atomic Save
Current save flow:
Create temporary file
↓
Serialize completely
↓
Close writer
↓
Move over target
This reduces partial overwrite risk।
If:
ATOMIC_MOVE
is unsupported, fallback uses:
REPLACE_EXISTING
Temporary File Cleanup
One improvement worth checking:
If:
write succeeds
move fails
temporary file may remain unless cleanup runs।
Repository save logic should attempt:
Files.deleteIfExists(
temporary
);
on failure।
Cleanup failure should normally not hide the primary persistence failure।
Avoid Dangerous Cleanup
Do not write:
catch (
IOException exception
) {
Files.delete(
temporary
);
}
without considering that cleanup itself may throw and replace the original exception।
Prefer best-effort cleanup।
Review Path Safety
CourseCode only permits:
A-Z
0-9
-
so it already prevents values such as:
../../secret
Still, repository path protection is a useful second boundary:
Path target =
baseDirectory.resolve(
fileName
)
.normalize();
if (
!target.startsWith(
baseDirectory
)
) {
throw ...
}
Defense in depth is appropriate at filesystem boundaries।
Numeric IDs and Paths
For:
LearnerId
EnrollmentId
filenames are generated from validated positive numbers।
Example:
1.properties
1001.properties
These are naturally constrained identifiers।
Review UTF-8
Explicit:
StandardCharsets.UTF_8
should appear when reading and writing text।
This matters because course lesson content may include:
Bangla
English
Unicode symbols
Platform-default encoding should not determine persistence behavior।
Properties Caveat
Properties is convenient, but it is still a simple key-value format।
It is appropriate for this learning project।
It becomes awkward when:
Nested structures become large
Schema evolves significantly
Queries become complex
Many concurrent writers exist
At that point a database or another storage model becomes more suitable।
Review Concurrency Limitations
Atomic replacement does not provide:
Transactions
Locks
Optimistic concurrency
Version checking
Example:
Process A loads course
Process B loads same course
A saves
B saves
B can overwrite A。
This limitation should be understood, not hidden।
Review Multi-Entity Consistency
Enrollment use case checks:
Learner exists
Course published
Then enrollment saves
Between check and save, another process could theoretically change data।
File repositories provide no transaction across multiple entities।
For this console learning project, acceptable।
Production systems would need stronger transaction/concurrency strategies।
Review Console Boundary
Console should only:
Read
Parse
Call
Display
Good:
CourseCode code =
new CourseCode(
readLine(
"Course code: "
)
);
courseService.publishCourse(
code
);
Bad:
Files.writeString(...)
inside console code।
Review Top-Level Error Handling
We currently catch runtime failures around each menu operation।
This keeps interactive app running after expected errors।
But consider separating unexpected programming failures in larger applications।
For this project, displaying:
exception.getMessage()
is adequate।
Avoid Empty Error Messages
When throwing application exceptions, provide useful context।
Weak:
throw new IllegalStateException();
Better:
throw new IllegalStateException(
"Only active enrollment can be completed."
);
Messages help both user and developer।
Do Not Leak Sensitive Internals
Avoid console messages such as:
Could not write /home/user/private/storage/...
if path exposure matters।
This project is local, but production boundaries should carefully choose what internal details are user-visible।
Review Package Structure
A reasonable final structure:
io.liveklass
├── Main
├── console
│ └── ConsoleApplication
├── course
├── learner
├── enrollment
└── storage
Feature packages contain:
Domain
Repository contracts
Services
Feature exceptions
Storage package contains concrete file infrastructure।
Could Storage Live Inside Each Feature?
Yes।
Alternative:
course/storage
learner/storage
enrollment/storage
is also reasonable।
Package design does not have one universally correct answer।
The important thing is:
Responsibilities remain clear.
Review Dependency Direction
Good:
CourseService
→ CourseRepository
Then:
FileCourseRepository
implements CourseRepository
Business use cases do not depend on file APIs।
Avoid Concrete Dependency Leakage
Bad:
public CourseService(
FileCourseRepository repository
)
This prevents easy replacement with:
InMemoryCourseRepository
Better:
public CourseService(
CourseRepository repository
)
Review Composition Root
Main is allowed to know concrete implementations।
That is its job।
Example:
CourseRepository courseRepository =
new FileCourseRepository(
path
);
Then:
CourseService courseService =
new CourseService(
courseRepository
);
Dependency selection happens at application assembly, not inside business logic।
Potential Bug 1: Null Handling in Services
Suppose:
learnerService.findLearner(
null
);
If repository performs null validation, failure still occurs।
But public service methods should ideally own clear input expectations。
Boundary validation should be consistent rather than accidental।
Potential Bug 2: Duplicate ID vs Duplicate Relationship
These are different:
Enrollment ID 1001 already exists
and:
Learner 1 already enrolled in JAVA
Separate exceptions improve error meaning:
DuplicateEnrollmentIdException
DuplicateEnrollmentException
Do not collapse distinct failures into one vague:
RuntimeException(
"duplicate"
);
Potential Bug 3: Stored Course Filename Mismatch
Imagine filename:
java.properties
but property inside says:
code=BACKEND
Our current repository may reconstruct BACKEND from file content even though lookup was for JAVA।
A hardening improvement is to verify:
expectedCode.equals(
storedCode
)
when loading by known identifier।
This detects mismatched storage files।
Potential Bug 4: Learner Filename Mismatch
Similarly:
1.properties
could contain:
id=2
When loading ID 1, repository may validate:
Stored ID must match requested ID.
This catches accidental or manual file corruption।
Potential Bug 5: Enrollment References Missing Entities
An enrollment file may contain:
learnerId=999
courseCode=MISSING
Enrollment.restore() itself cannot check repositories।
Should repository reject this?
Usually no.
Repository reconstructs enrollment data.
Referential integrity across repositories belongs at a higher level or a dedicated integrity check।
A database would typically enforce foreign keys।
Our file model cannot guarantee that automatically।
Potential Bug 6: findAll() Ordering
File path sorting determines output order।
If learner filenames are:
1.properties
2.properties
10.properties
lexicographic sorting may produce:
1
10
2
If numeric ordering matters, sort loaded entities by ID instead।
Example:
return ...
.map(...)
.sorted(
Comparator.comparingLong(
learner ->
learner.id()
.value()
)
)
.toList();
This is presentation/application semantics, not necessarily storage correctness।
Potential Bug 7: Money Formatting
Domain uses:
long priceInPaisa
Good।
Avoid business calculation using:
double
because floating-point arithmetic may introduce precision issues।
If console only displays:
499000 → 4990.00
formatting can be handled carefully at the presentation layer।
Potential Bug 8: Very Large Lesson Content
Properties loads entire course file into memory।
For normal course content this may be okay।
If lesson content becomes massive:
Videos
Large binary files
Huge documents
they should not be embedded directly into a properties file।
Store media externally and persist references instead।
Potential Bug 9: Repository Save Semantics
Current:
save(...)
can overwrite existing entity file।
This is fine because service distinguishes create/update use cases।
But direct repository callers could overwrite data।
Repository is an internal persistence abstraction, not necessarily a public business API।
The service remains the business boundary।
Potential Bug 10: Email Validation
Our EmailAddress validation is intentionally simple।
Do not present it as:
Fully RFC-compliant email validation.
Its contract is:
Application-level simplified validation.
Accurate naming and documentation matter।
Refactoring Opportunity: Shared Property File Helper
Three file repositories share operations:
Load Properties
Store Properties
Atomic replace
Directory initialization
Required property
If duplication becomes difficult to maintain, introduce a focused helper such as:
PropertyFileStore
Example Responsibility
PropertyFileStore
could own:
Properties read(
Path path
);
void writeAtomically(
Path target,
Properties properties
);
Repositories would still own:
Domain serialization
Domain deserialization
Filename mapping
This is a better abstraction than a large inheritance hierarchy।
Do Not Refactor Too Early
Current duplication may still be acceptable for teaching clarity।
Rule:
Extract when repeated mechanics become a maintenance problem,
not merely because two methods look similar.
Refactoring Opportunity: Summary Mapping
Services currently do:
repository.findAll()
.stream()
.map(
Course::summary
)
.toList();
This is clean enough।
No mapper framework is needed।
Refactoring Opportunity: Input Commands
If service signatures become large:
createCourse(
code,
title,
price,
language,
duration,
description,
...
)
a record such as:
CreateCourseCommand
may improve readability।
Current three-parameter method does not yet require it।
Final Manual Verification Scenario
Before declaring project complete, run this sequence.
Step 1
Create:
Course JAVA-OOP
Expected:
DRAFT
Step 2
Attempt enrollment before publication।
Expected:
Rejected
Step 3
Attempt publication without lessons।
Expected:
Rejected
Step 4
Add one lesson।
Expected:
Success
Step 5
Add another lesson with same ID।
Expected:
Rejected
Step 6
Publish course।
Expected:
PUBLISHED
Step 7
Attempt adding another lesson.
Expected:
Rejected
Step 8
Register learner.
Expected:
Success
Step 9
Register same email with another learner ID.
Expected:
Rejected
Step 10
Enroll learner.
Expected:
ACTIVE
Step 11
Enroll same learner in same course again.
Expected:
Rejected
Step 12
Complete enrollment.
Expected:
COMPLETED
Step 13
Cancel completed enrollment.
Expected:
Rejected
Step 14
Stop application and restart it.
Expected:
Course
Learner
Enrollment
still exist with their latest states।
Step 15
Manually corrupt a stored enum value.
Expected:
StoredDataCorruptionException
or a user-facing error derived from it।
It must not appear as normal "not found."
Final Design Checklist
Domain
- IDs are validated strong types
- Value objects are immutable
-
Courseprotects publication lifecycle -
Enrollmentprotects completion/cancellation lifecycle - Lesson IDs cannot duplicate
- Internal collections are not directly exposed
- Entity mutation happens through meaningful methods
- Restore methods still validate invariants
Equality
- Value objects have stable equality
- Equal values have compatible hash codes
- Mutable entity fields are not blindly used as hash identity
- Repository keys use stable value objects
Application Services
- Services coordinate use cases
- Services do not duplicate entity invariants
- Cross-entity rules live in appropriate services
- Required dependencies are constructor-injected
- Duplicate failures are meaningful
- Missing entities produce meaningful exceptions
Persistence
- Base directories are created safely
- Paths are normalized
- UTF-8 is explicit
- Readers/writers use try-with-resources
- Missing file differs from corrupted data
- Stored values are validated
- Temporary files are used for replacement
- Atomic move has fallback
- Storage errors are translated
-
findAll()closes its stream
Console
- Console only reads/parses/displays
- Console calls services
- Console does not contain domain rules
- Numeric parsing errors are understandable
- Expected failures do not kill the menu loop
- Console does not know file serialization
Architecture
- Repository interfaces are cohesive
- Services depend on contracts
-
Mainchooses concrete implementations - No global mutable service locator exists
- No unnecessary inheritance exists
- Packages communicate responsibility
- Architecture remains proportional to project size
Final Assessment
Score each category:
0 = Missing
1 = Partially correct
2 = Strong
| Area | Score |
|---|---|
| Domain modeling | /2 |
| Encapsulation | /2 |
| Value objects | /2 |
| Immutability | /2 |
| Equality design | /2 |
| Collection ownership | /2 |
| Enum lifecycle design | /2 |
| Exception semantics | /2 |
| Repository contracts | /2 |
| Application services | /2 |
| Constructor injection | /2 |
| Cross-entity rules | /2 |
| File I/O | /2 |
| UTF-8 handling | /2 |
| Resource management | /2 |
| Corruption handling | /2 |
| Atomic replacement | /2 |
| Console boundary | /2 |
| Package organization | /2 |
| Overall readability | /2 |
Maximum:
40
Score Interpretation
35–40
Strong Java foundation
29–34
Good implementation with minor design issues
22–28
Functional but needs more design/refactoring practice
Below 22
Revisit domain ownership, repository boundaries, and encapsulation
Knowledge Check
Question 1
Why should value objects validate at construction?
Question 2
Why should Course expose publish() instead of setStatus()?
Question 3
Why use immutable values as repository keys?
Question 4
Why distinguish missing data from corrupted data?
Question 5
What problem does atomic replacement reduce?
Question 6
Does atomic replacement provide transactions?
Question 7
Why should file repositories translate IOException?
Question 8
What belongs in EnrollmentService rather than Enrollment?
Question 9
Why is Main allowed to know concrete repository classes?
Question 10
What is the most important final-project design principle?
Knowledge Check Answers
Answer 1
So once the object exists, downstream code can trust its basic invariants instead of repeatedly validating raw values।
Answer 2
publish() communicates domain intent and can enforce valid publication rules before changing state।
Answer 3
Their equality and hash-related state remains stable, making map/set behavior predictable।
Answer 4
Missing means no entity exists; corrupted means stored data exists but is invalid and should not be silently ignored।
Answer 5
It reduces the risk of leaving an existing target file partially overwritten when a save fails।
Answer 6
No. It only improves individual file replacement semantics।
Answer 7
To prevent low-level storage implementation details from leaking throughout application and domain code।
Answer 8
Rules requiring learner existence, course existence/state, and existing enrollment repository information।
Answer 9
Because Main is the composition root responsible for choosing implementations and wiring dependencies।
Answer 10
Responsibilities should live with the concept or boundary that actually owns them।
Final Project Summary
এই Final Project-এ আমরা পুরো Java foundation course-এর concepts combine করেছি।
We used:
Classes
Objects
Encapsulation
Composition
Interfaces
Collections
Enums
Exceptions
File I/O
Records
Value objects
Equality
Immutability
Defensive copying
Repository abstraction
Constructor injection
Clean methods
Package organization
Application architecture:
Console
→ Services
→ Domain + Repository Contracts
→ File Storage
আমরা এমন একটি design তৈরি করেছি যেখানে:
- Domain objects নিজেদের invariants protect করে
- Strong types primitive misuse কমায়
- Collections safely owned
- Application services use cases coordinate করে
- Repositories storage details hide করে
- File storage restart persistence দেয়
- Exceptions failure meaning preserve করে
- Concrete dependencies
Main-এ assembled হয় - Presentation logic business logic থেকে separate থাকে
The final engineering lesson is:
Good Java is not only about syntax.
Good Java makes responsibilities,
state,
dependencies,
and failure semantics explicit.
একজন student যদি এই project independently rebuild করতে পারে এবং প্রতিটি design decision explain করতে পারে, তাহলে তার Java/OOP foundation backend-development-এর next stage-এর জন্য যথেষ্ট strong।
Next Lesson
পরবর্তী lesson:
Final Project Assessment and Course Completion
শেষ lesson-এ থাকবে:
- Independent build challenge
- Required features
- Hidden edge cases
- Review questions
- Self-assessment rubric
- Course-wide knowledge checklist
- What to learn next before Spring Boot and backend development