Final Project

Final Project Review and Hardening

ReadingPreview

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
  • Course protects publication lifecycle
  • Enrollment protects 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
  • Main chooses 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
AreaScore
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