Final Project

Final Project Assessment and Course Completion

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

এই lesson পুরো course-এর final assessment।

এখানে নতুন Java feature শেখানো হবে না।

বরং আপনাকে independently prove করতে হবে যে আপনি:

Java syntax
Object-oriented programming
Collections
Generics
Exceptions
File I/O
Value objects
Records
Clean design
Dependency design

একসঙ্গে ব্যবহার করতে পারেন।

Final challenge:

Build the LiveKlass Course Enrollment System Independently

আগের lessons reference হিসেবে ব্যবহার করা যাবে, কিন্তু goal হলো নিজের design reasoning দিয়ে applicationটি rebuild করা।

একটি professional foundation তৈরি হয়েছে কিনা তার সবচেয়ে ভালো test:

Can you build it again
without copying every previous implementation?

Final Project Requirements

Application must support:

1. Create course
2. Add lesson
3. Publish course
4. Archive course
5. List courses
6. Register learner
7. List learners
8. Enroll learner
9. Complete enrollment
10. Cancel enrollment
11. List enrollments
12. Persist data across restarts

Application interface:

Console

Persistence:

File-based

External frameworks:

None

Mandatory Domain Types

Your solution should contain equivalents of:

CourseCode
LessonId
Lesson
CourseStatus
Course

LearnerId
EmailAddress
Learner

EnrollmentId
EnrollmentStatus
Enrollment

You may rename types if the new names clearly communicate the same domain concepts।


Strong Value Types

These should not remain raw primitives everywhere:

Course identifier
Lesson identifier
Learner identifier
Enrollment identifier
Email address

Example:

public record LearnerId(
        long value
) {

    public LearnerId {
        if (value <= 0) {
            throw new IllegalArgumentException(
                    "Learner id must be positive."
            );
        }
    }
}

The purpose is not merely wrapping a long

The type should guarantee:

Meaning
Validation
Stable equality

Course Rules

A new course:

Starts as DRAFT

Course code:

Required
Normalized
Stable

Title:

Required
Non-blank

Price:

Cannot be negative

Lessons:

Must have unique LessonId values
Can only be added while DRAFT

Publication:

Only DRAFT can publish
At least one lesson is required

Archiving:

Only PUBLISHED can archive

Expected Course API

A strong design may expose:

course.addLesson(
        lesson
);

course.publish();

course.archive();

Avoid APIs such as:

course.setStatus(...);
course.setLessons(...);

that allow callers to bypass invariants।


Enrollment Rules

New enrollment:

ACTIVE

Transitions:

ACTIVE → COMPLETED
ACTIVE → CANCELLED

Invalid:

COMPLETED → CANCELLED
CANCELLED → COMPLETED
COMPLETED → COMPLETED

Expected API:

enrollment.complete();

enrollment.cancel();

Cross-Entity Enrollment Rules

Before enrollment:

Learner must exist
Course must exist
Course must be PUBLISHED
Enrollment ID must be unique
Learner cannot already be enrolled in the same course

These rules require multiple repositories।

Therefore they should normally be coordinated by:

EnrollmentService

not by Enrollment alone।


Repository Requirements

Implement:

CourseRepository
LearnerRepository
EnrollmentRepository

Example:

public interface CourseRepository {

    void save(
            Course course
    );

    Course findByCode(
            CourseCode courseCode
    );

    List<Course> findAll();
}

Services should depend on these interfaces rather than directly depending on:

FileCourseRepository
FileLearnerRepository
FileEnrollmentRepository

File Persistence Requirements

Store data under something like:

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

Use:

Path
Files
Properties
StandardCharsets.UTF_8

Resources must be managed with:

try-with-resources

File Save Requirements

A strong implementation should use:

Serialize
↓
Write temporary file
↓
Close writer
↓
Replace target file

Attempt:

ATOMIC_MOVE

when supported।

Fallback safely if atomic move is unavailable।


Restore Requirements

Repository must be able to reconstruct stored lifecycle state।

Example:

Course.restore(...)

and:

Enrollment.restore(...)

Restore should not mean:

Skip validation

Corrupted stored state must still fail।


Missing vs Corrupted Data

Your repository should distinguish:

Entity file missing

from:

Entity file exists but contains invalid data

Missing entity may produce:

null

at repository level।

Corrupted data should produce something meaningful such as:

StoredDataCorruptionException

Console Requirements

The console should:

Read input
Parse input
Create strong domain values
Call services
Display results
Display errors

It should not:

Write files directly
Change entity state directly
Implement duplicate checks
Implement publication rules

Required Application Flow

A typical successful scenario:

Create course
↓
Add lesson
↓
Publish course
↓
Register learner
↓
Enroll learner
↓
Complete enrollment
↓
Restart application
↓
Verify persisted state

Hidden Edge Cases

Do not test only the happy path।

Your implementation should handle these cases correctly।


Edge Case 1: Equivalent Course Codes

Create:

java-oop

then:

 JAVA-OOP

Expected:

Duplicate course rejected

because normalized values represent the same CourseCode


Edge Case 2: Empty Course Publication

Create course and immediately publish।

Expected:

Rejected

because it has no lesson।


Edge Case 3: Duplicate Lesson ID

Add:

LessonId 1

twice with different titles।

Expected:

Rejected

because duplicate rule is based on ID।


Edge Case 4: Modify Published Course

Publish course, then call:

addLesson(...)

Expected:

Rejected

Edge Case 5: Archive Draft Course

Expected:

Rejected

Edge Case 6: Duplicate Learner Email

Register:

subu@example.com

then:

 SUBU@EXAMPLE.COM

Under this project's normalization policy:

Rejected

Edge Case 7: Enroll into Draft Course

Expected:

Rejected

Edge Case 8: Duplicate Enrollment ID

Two different enrollment requests use:

EnrollmentId 1001

Expected:

Rejected

Edge Case 9: Duplicate Learner-Course Pair

Same learner and course with another enrollment ID।

Expected:

Rejected

Edge Case 10: Complete Twice

enrollment.complete();
enrollment.complete();

Second call:

Rejected

Edge Case 11: Cancel Completed Enrollment

Expected:

Rejected

Edge Case 12: Missing Stored Entity

Request unknown course।

Expected application behavior:

CourseNotFoundException

or equivalent।


Edge Case 13: Corrupted Enum

Stored file:

status=SOMETHING

Expected:

Storage corruption failure

not:

Entity not found

Edge Case 14: Corrupted Number

Stored file:

priceInPaisa=hello

Expected:

Storage corruption failure

Edge Case 15: Restart

Create and mutate data।

Exit application।

Run it again।

Expected:

Latest state remains available

Design Review Challenge

For each code example, identify the problem।


Challenge 1

public class Course {

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

    public String status;
}

Problems:

Public mutable state
Weak String status
No encapsulation
Rules can be bypassed

Challenge 2

public void publishCourse(
        String code
) {
    FileCourseRepository repository =
            new FileCourseRepository(
                    Path.of(
                            "data"
                    )
            );

    ...
}

Problem:

Hard-coded dependency

Better:

private final CourseRepository repository;

supplied through constructor।


Challenge 3

course.setStatus(
        CourseStatus.PUBLISHED
);

Problem:

State transition rules can be bypassed

Better:

course.publish();

Challenge 4

public List<Lesson> getLessons() {
    return lessons;
}

Problem:

Internal mutable collection leaked

Better:

return List.copyOf(
        lessons
);

Challenge 5

catch (
    IOException exception
) {
    return null;
}

Problem:

Storage failure incorrectly appears as not-found

Challenge 6

public class CourseService
        extends FileCourseRepository {
}

Problem:

Incorrect inheritance relationship

Course service:

uses a repository

It is not a repository।


Independent Refactoring Challenge

Start with this intentionally weak code:

public class App {

    public List<Course> courses =
            new ArrayList<>();

    public void create(
            String code,
            String title,
            long price,
            boolean published
    ) {
        Course course =
                new Course();

        course.code =
                code;

        course.title =
                title;

        course.price =
                price;

        course.published =
                published;

        courses.add(
                course
        );
    }
}

Refactor it until:

  • Course has encapsulated state
  • Course code becomes a value type
  • Status becomes an enum
  • Publication becomes behavior
  • Collection ownership is protected
  • Persistence is behind a repository
  • Service coordinates creation
  • Dependencies are constructor-injected

Do this without copying the project code line by line।

The goal is reasoning।


Course-Wide Assessment

The following questions cover the entire course।


Java Fundamentals

Can you explain:

  • JVM, JDK, and JRE?
  • Variables and data types?
  • Primitive vs reference types?
  • Operators?
  • Conditionals?
  • Loops?
  • Methods?
  • Scope?

Object-Oriented Programming

Can you explain and use:

  • Class
  • Object
  • Field
  • Method
  • Constructor
  • Encapsulation
  • Inheritance
  • Composition
  • Polymorphism
  • Abstraction
  • Interface

Collections and Generics

Can you choose between:

List
Set
Map

based on requirements?

Can you explain:

Type safety
Generic classes
Generic methods

?

Can you explain why:

Map<CourseCode, Course>

is stronger than relying on unrelated raw strings?


Exceptions

Can you explain:

Exception propagation
try
catch
finally
throw
throws
checked
unchecked

?

Can you distinguish:

Validation failure
Domain failure
Not-found
Storage failure
Corrupted data

?


File I/O

Can you use:

Path
Files
BufferedReader
BufferedWriter
Properties

?

Can you explain:

UTF-8
try-with-resources
atomic replacement
path normalization

?


Equality

Can you explain:

==
equals()
hashCode()

?

Can you explain why equal objects must have equal hash codes?

Can you explain why mutable hash keys are dangerous?


Immutability

Can you explain why:

final List<T>

does not make the list immutable?

Can you use:

List.copyOf(...)

appropriately?

Can you distinguish:

Shallow immutability
Deep immutability

?


Records

Can you explain when:

record

is appropriate?

Strong candidates:

Value objects
DTOs
Snapshots
Validated IDs

Can you explain why lifecycle-heavy Course is better as a normal class in this project?


Clean Design

Can you recognize:

God class
Boolean blindness
Primitive obsession
Feature envy
Long method
Hidden dependency
Mutation leak

?

Can you refactor them without creating unnecessary architecture?


Dependency Design

Can you explain:

Dependency
Constructor injection
Composition
Repository boundary
Composition root

?

Can you perform dependency injection manually without Spring?

If yes, you understand the underlying principle Spring will later automate।


Final Self-Assessment Rubric

Score each category:

0 = Cannot explain or implement
1 = Understands concept but needs guidance
2 = Can implement independently
3 = Can explain design tradeoffs
AreaScore
Java fundamentals/3
Methods and control flow/3
Classes and objects/3
Encapsulation/3
Inheritance and composition/3
Interfaces and abstraction/3
Generics/3
Collections/3
Enums/3
Exceptions/3
File I/O/3
Resource management/3
Equality and hashing/3
Immutability/3
Records/3
Clean method design/3
Domain modeling/3
Repository design/3
Dependency injection/3
Final project implementation/3

Maximum:

60

Score Interpretation

52–60
Strong foundation. Ready to move into backend development.

43–51
Good foundation. Review weaker areas while moving forward.

32–42
Core understanding exists, but rebuild the final project once more.

Below 32
Repeat important modules before moving into framework-heavy backend work.

Final Project Pass Criteria

Do not judge success only by:

Application runs

A strong submission should also satisfy:

  • Important values use strong types
  • Invalid state is rejected early
  • Entity state is encapsulated
  • No public lifecycle setters
  • Collections are safely owned
  • Value equality is correct
  • Hash-based keys are stable
  • Domain rules live with appropriate owners
  • Cross-entity rules live in services
  • Repository contracts are focused
  • Services use constructor injection
  • Concrete storage stays outside services
  • File resources are closed
  • UTF-8 is explicit
  • Missing and corrupted data differ
  • Application survives restart
  • Console contains no storage/domain rules
  • Class and method names communicate intent
  • No unnecessary abstractions were introduced

When Are You Ready for Spring Boot?

You are ready to start Spring Boot when these ideas make sense without a framework:

CourseRepository repository =
        new FileCourseRepository(
                path
        );

CourseService service =
        new CourseService(
                repository
        );

You should understand:

Why repository exists
Why service receives it
Why Course does not save itself
Why Main wires dependencies
Why constructor injection is useful

Spring should automate wiring you already understand।

It should not hide concepts you never learned।


What Comes Next?

The natural next course is:

Backend Development with Java and Spring Boot

There you can build on this foundation with:

Spring Boot
REST APIs
HTTP
JSON
Controllers
Dependency Injection Container
Configuration
Validation
PostgreSQL
SQL
Transactions
JPA / persistence choices
Authentication
Testing
Docker
Observability
Production concerns

Do Not Throw Away What You Learned

Framework code can tempt developers into writing:

Controller
→ Service
→ Repository

mechanically without thinking।

The important question remains:

Who owns this rule?

A framework does not answer that for you।


Example

Even in Spring Boot, this:

course.publish();

is usually stronger than placing all publication rules inside:

CourseController

or:

CourseRepository

The Java/OOP foundation still matters।


Backend Learning Mindset

When entering Spring Boot, do not ask only:

Which annotation do I need?

Also ask:

What Java object is being created?
Who owns it?
What dependency is being injected?
Where should this rule live?
What happens when this operation fails?
What boundary am I crossing?

That is how framework knowledge becomes engineering knowledge।


Final Knowledge Check

Question 1

What is the difference between an entity and value object?

Question 2

Why should entities expose behavior instead of arbitrary setters?

Question 3

Why are immutable value objects good HashMap keys?

Question 4

Why should repositories hide storage mechanisms?

Question 5

Why should application services not duplicate entity rules?

Question 6

Why should infrastructure exceptions be translated?

Question 7

What does defensive copying protect?

Question 8

Why is constructor injection useful even without Spring?

Question 9

What is the purpose of a composition root?

Question 10

What is the most important question when designing Java software?


Final Knowledge Check Answers

Answer 1

An entity has identity and usually lifecycle; a value object is primarily defined by its value and usually uses immutable value equality।

Answer 2

Behavior methods express intent and protect valid state transitions, while arbitrary setters allow callers to bypass invariants।

Answer 3

Their equality and hash-related state stays stable after insertion।

Answer 4

So application logic depends on required capabilities instead of being coupled to files, databases, or another concrete mechanism।

Answer 5

Entity invariants should have one authoritative owner; duplication can cause inconsistent rules।

Answer 6

To preserve meaningful failure categories without leaking low-level implementation details everywhere।

Answer 7

It prevents external mutable references from unexpectedly changing an object's internal state।

Answer 8

It makes dependencies explicit, required, replaceable, and easier to test।

Answer 9

It is the place where concrete application dependencies are created and wired together।

Answer 10

A powerful recurring question is:

Who should own this responsibility?

Course Completion

You have now covered the foundation required to move from:

Writing Java statements

to:

Designing small maintainable Java applications

Throughout the course we moved from:

Variables
Conditionals
Loops
Methods

into:

Classes
Objects
Encapsulation
Interfaces
Collections
Generics
Exceptions
File I/O

and finally into:

Immutability
Equality
Records
Clean design
Repositories
Dependency injection
Application architecture

The Final Project combined those concepts into one working system।


Final Takeaway

Java expertise does not begin with memorizing every API।

A strong foundation comes from understanding:

State
Behavior
Ownership
Types
Dependencies
Boundaries
Failures

When these are clear, frameworks become much easier to understand।

The goal of this course was not only:

Learn Java syntax

It was to reach the point where you can look at a requirement and ask:

What objects exist here?
What should each object own?
What should remain immutable?
Which state transitions are valid?
Which dependency belongs behind a boundary?
How should failure be represented?

If you can answer those questions and independently rebuild the Final Project, you have completed the Java and Object-Oriented Programming Foundation.

Course Complete