Professional Java Practices

Practice and Assessment

ReadingPreview

You are viewing a free preview lesson.

Project Overview

এই module-এর final assessment-এ আমরা একটি small course-management application design করব।

এইবার focus file I/O নয়।

Focus হলো:

Professional Java design

আপনাকে এমন code লিখতে হবে যা:

  • Easy to understand
  • Easy to change
  • Difficult to misuse
  • Properly encapsulated
  • Clearly organized
  • Explicit about dependencies

এই assessment cover করবে:

  • Package organization
  • Strong value objects
  • Immutability
  • Defensive copying
  • equals() and hashCode()
  • Records
  • Clean methods
  • Domain behavior
  • Repository abstraction
  • Constructor injection
  • Composition
  • Code-smell identification
  • Refactoring decisions

Scenario

আমরা একটি simplified learning platform তৈরি করছি।

System support করবে:

Course creation
Lesson addition
Course publication
Course lookup
Course listing

A course has:

CourseCode
Title
Price
Status
Lessons

A lesson has:

LessonId
Title

Course lifecycle:

DRAFT
→
PUBLISHED

Publication rules:

Course must contain at least one lesson
Only DRAFT course can be published

Target Project Structure

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
        └── storage/
            └── InMemoryCourseRepository.java

এই structure deliberately simple।

আমাদের লক্ষ্য:

Clear boundaries without unnecessary architecture

Part 1: CourseCode

CourseCode একটি value object।

Requirements:

  • Cannot be null
  • Cannot be blank
  • Normalize using strip()
  • Normalize to uppercase
  • Allowed characters:
A-Z
0-9
-
  • Value-based equality
  • Stable hash code
  • Useful toString()

A record is a strong choice।


CourseCode.java

package io.liveklass.course;

import java.util.Locale;

public record CourseCode(
        String value
) {

    public CourseCode {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        value =
                value.strip()
                        .toUpperCase(
                                Locale.ROOT
                        );

        if (
                !value.matches(
                        "[A-Z0-9-]+"
                )
        ) {
            throw new IllegalArgumentException(
                    "Course code contains unsupported characters."
            );
        }
    }

    @Override
    public String toString() {
        return value;
    }
}

Why a Record Fits

CourseCode:

Represents a value
Has stable state
Has value equality
Should not mutate

This is exactly the kind of type where a record is useful।


Equality Check

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

CourseCode second =
        new CourseCode(
                " JAVA-OOP "
        );

System.out.println(
        first.equals(
                second
        )
);

Expected:

true

because both normalize to:

JAVA-OOP

Part 2: LessonId

A lesson identifier should not be an arbitrary long everywhere।

Create a strong value type।

LessonId.java

package io.liveklass.course;

public record LessonId(
        long value
) {

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

Why Use LessonId?

Compare:

findLesson(
        12L
);

with:

findLesson(
        new LessonId(
                12L
        )
);

The second communicates domain meaning more clearly।

It also prevents invalid:

0
negative IDs

from spreading through the application।


Part 3: Immutable Lesson

A lesson in this assessment will be immutable।

Lesson.java

package io.liveklass.course;

public final class Lesson {

    private final LessonId id;
    private final String title;

    public Lesson(
            LessonId id,
            String title
    ) {
        if (id == null) {
            throw new IllegalArgumentException(
                    "Lesson id is required."
            );
        }

        if (
                title == null
                || title.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Lesson title is required."
            );
        }

        this.id =
                id;

        this.title =
                title.strip();
    }

    public LessonId getId() {
        return id;
    }

    public String getTitle() {
        return title;
    }
}

Does Lesson Need equals()?

That depends on desired semantics।

For duplicate checking in this application, we will compare lessons using:

LessonId

rather than treating the entire lesson as a value object।

Therefore Lesson does not need custom equality for this assessment।

Important lesson:

Do not override equals() just because a class exists.

Equality is a design decision।


Part 4: CourseStatus

CourseStatus.java

package io.liveklass.course;

public enum CourseStatus {

    DRAFT,
    PUBLISHED
}

This is clearer than:

boolean published

because the state has an explicit domain name।


Part 5: Designing Course

Course is not a record।

Why?

Because it has:

Identity
Mutable lifecycle
Controlled state transitions
Owned collection
Domain behavior

A normal class communicates this better।


Course Invariants

We want these rules:

Code is always present
Title is always present
Price is never negative
Lesson IDs are unique
Lessons cannot be added after publication
Course must contain at least one lesson before publication
Only DRAFT course can publish

Course.java

package io.liveklass.course;

import java.util.ArrayList;
import java.util.List;

public final class Course {

    private final CourseCode code;
    private final long priceInPaisa;
    private final List<Lesson> lessons;

    private String title;
    private CourseStatus status;

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

        if (
                title == null
                || title.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course title is required."
            );
        }

        if (priceInPaisa < 0) {
            throw new IllegalArgumentException(
                    "Course price cannot be negative."
            );
        }

        this.code =
                code;

        this.title =
                title.strip();

        this.priceInPaisa =
                priceInPaisa;

        this.status =
                CourseStatus.DRAFT;

        this.lessons =
                new ArrayList<>();
    }

    public void changeTitle(
            String newTitle
    ) {
        if (
                newTitle == null
                || newTitle.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course title is required."
            );
        }

        if (
                status
                == CourseStatus.PUBLISHED
        ) {
            throw new IllegalStateException(
                    "Published course title cannot be changed."
            );
        }

        title =
                newTitle.strip();
    }

    public void addLesson(
            Lesson lesson
    ) {
        if (lesson == null) {
            throw new IllegalArgumentException(
                    "Lesson is required."
            );
        }

        if (
                status
                == CourseStatus.PUBLISHED
        ) {
            throw new IllegalStateException(
                    "Cannot add lessons to a published course."
            );
        }

        boolean duplicate =
                lessons.stream()
                        .anyMatch(
                                existing ->
                                        existing.getId()
                                                .equals(
                                                        lesson.getId()
                                                )
                        );

        if (duplicate) {
            throw new IllegalArgumentException(
                    "Lesson id already exists: "
                    + lesson.getId()
                    + "."
            );
        }

        lessons.add(
                lesson
        );
    }

    public void publish() {
        if (
                status
                != CourseStatus.DRAFT
        ) {
            throw new IllegalStateException(
                    "Only draft courses can be published."
            );
        }

        if (lessons.isEmpty()) {
            throw new IllegalStateException(
                    "Course must have at least one lesson before publication."
            );
        }

        status =
                CourseStatus.PUBLISHED;
    }

    public boolean isPublished() {
        return status
                == CourseStatus.PUBLISHED;
    }

    public CourseSummary summary() {
        return new CourseSummary(
                code,
                title,
                priceInPaisa,
                status,
                lessons.size()
        );
    }

    public CourseCode getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    public long getPriceInPaisa() {
        return priceInPaisa;
    }

    public CourseStatus getStatus() {
        return status;
    }

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

Defensive Collection Design

Internal collection:

private final List<Lesson> lessons;

is mutable because Course needs to add lessons।

But callers receive:

List.copyOf(
        lessons
)

This gives us:

Mutable internally
Controlled through Course methods
Read-only snapshot externally

What Would Be Wrong Here?

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

Caller could execute:

course.getLessons()
        .clear();

and bypass every domain rule।


Part 6: CourseSummary

A caller listing courses does not necessarily need mutable domain objects।

Create an immutable snapshot।

CourseSummary.java

package io.liveklass.course;

public record CourseSummary(
        CourseCode code,
        String title,
        long priceInPaisa,
        CourseStatus status,
        int lessonCount
) {

    public CourseSummary {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        if (
                title == null
                || title.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course title is required."
            );
        }

        if (priceInPaisa < 0) {
            throw new IllegalArgumentException(
                    "Course price cannot be negative."
            );
        }

        if (status == null) {
            throw new IllegalArgumentException(
                    "Course status is required."
            );
        }

        if (lessonCount < 0) {
            throw new IllegalArgumentException(
                    "Lesson count cannot be negative."
            );
        }

        title =
                title.strip();
    }
}

Why Return lessonCount Instead of List<Lesson>?

For a summary, caller only needs:

How many lessons?

It does not need entire lesson objects।

A read model should expose only what its consumer needs।


Part 7: Repository Contract

CourseRepository.java

package io.liveklass.course;

import java.util.List;

public interface CourseRepository {

    Course findByCode(
            CourseCode courseCode
    );

    void save(
            Course course
    );

    List<Course> findAll();
}

This contract says nothing about:

Files
SQL
HashMap
Network

That is intentional।


Part 8: Custom Exceptions

CourseNotFoundException.java

package io.liveklass.course;

public final class CourseNotFoundException
        extends RuntimeException {

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

DuplicateCourseException.java

package io.liveklass.course;

public final class DuplicateCourseException
        extends RuntimeException {

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

Why Not Use Generic RuntimeException?

Compare:

throw new RuntimeException(
        "exists"
);

with:

throw new DuplicateCourseException(
        courseCode
);

The second exposes a meaningful failure category।

Boundary code can later handle it specifically।


Part 9: In-Memory Repository

InMemoryCourseRepository.java

package io.liveklass.course.storage;

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

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

public final class InMemoryCourseRepository
        implements CourseRepository {

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

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

        return courses.get(
                courseCode
        );
    }

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

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

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

Why LinkedHashMap?

We want predictable insertion order for this small example।

A plain:

HashMap

would also be valid if ordering did not matter।


Part 10: Course Service

The service coordinates use cases।

It should not implement rules already owned by Course

CourseService.java

package io.liveklass.course;

import java.util.List;

public final class CourseService {

    private final CourseRepository repository;

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

        this.repository =
                repository;
    }

    public void createCourse(
            CourseCode code,
            String title,
            long priceInPaisa
    ) {
        if (
                repository.findByCode(
                        code
                )
                != null
        ) {
            throw new DuplicateCourseException(
                    code
            );
        }

        Course course =
                new Course(
                        code,
                        title,
                        priceInPaisa
                );

        repository.save(
                course
        );
    }

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

        course.addLesson(
                lesson
        );

        repository.save(
                course
        );
    }

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

        course.publish();

        repository.save(
                course
        );
    }

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

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

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

        Course course =
                repository.findByCode(
                        courseCode
                );

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

        return course;
    }
}

Design Review: Service Responsibility

CourseService handles:

Use-case coordination
Duplicate lookup
Required lookup
Persistence coordination

It does not handle:

How CourseCode is normalized
Whether a lesson ID is valid
Whether course can publish
How repository stores data

Those responsibilities already belong elsewhere।


Part 11: Composition Root

Main.java

package io.liveklass;

import io.liveklass.course.CourseCode;
import io.liveklass.course.CourseRepository;
import io.liveklass.course.CourseService;
import io.liveklass.course.CourseSummary;
import io.liveklass.course.Lesson;
import io.liveklass.course.LessonId;
import io.liveklass.course.storage.InMemoryCourseRepository;

public class Main {

    public static void main(
            String[] args
    ) {
        CourseRepository repository =
                new InMemoryCourseRepository();

        CourseService service =
                new CourseService(
                        repository
                );

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

        service.createCourse(
                javaCourse,
                "Java and OOP Foundation",
                499_000L
        );

        service.addLesson(
                javaCourse,
                new Lesson(
                        new LessonId(
                                1L
                        ),
                        "Introduction to Java"
                )
        );

        service.addLesson(
                javaCourse,
                new Lesson(
                        new LessonId(
                                2L
                        ),
                        "Classes and Objects"
                )
        );

        service.publishCourse(
                javaCourse
        );

        for (
                CourseSummary summary
                : service.findAllCourses()
        ) {
            System.out.println(
                    summary
            );
        }
    }
}

Dependency Graph

Conceptually:

Main
 │
 ├── InMemoryCourseRepository
 │
 └── CourseService
       │
       └── CourseRepository

Main knows the concrete implementation।

CourseService knows only:

CourseRepository

This is intentional dependency design।


Assessment Part 1: Package Design

Given these classes:

Course
CourseService
CourseCode
FileCourseRepository
Enrollment
EnrollmentService
Main

Which structure is stronger?

Option A

model/
service/
repository/
util/

Option B

course/
enrollment/

with feature-related code grouped together।

Answer

For this small domain-focused application, Option B is generally clearer।

It keeps feature-related code close together।


Assessment Part 2: Record or Class?

Choose the stronger default।

CourseCode

Answer:

Record

It represents an immutable value।


CourseSummary

Answer:

Record

It represents an immutable snapshot।


Course

Answer:

Normal class

It owns mutable lifecycle and behavior।


LessonId

Answer:

Record

It is a validated immutable identifier।


ShoppingCart

Assume it supports:

addItem()
removeItem()
checkout()

Answer:

Normal class

It owns lifecycle and controlled mutation।


Assessment Part 3: Find the Mutation Leak

public final class Course {

    private final List<Lesson> lessons;

    public Course(
            List<Lesson> lessons
    ) {
        this.lessons =
                lessons;
    }

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

There are two obvious mutation leaks।


Problem 1: Constructor Input

Caller retains original list reference।

Fix:

this.lessons =
        new ArrayList<>(
                lessons
        );

Problem 2: Getter Output

Caller receives internal mutable list।

Fix:

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

Strong Pattern

Input
→ defensive copy

Internal state
→ controlled mutation

Output
→ immutable snapshot

Assessment Part 4: Equality

Consider:

CourseCode first =
        new CourseCode(
                "java"
        );

CourseCode second =
        new CourseCode(
                " JAVA "
        );

Expected:

first.equals(
        second
)

Result:

true

Why?

Both normalize to:

JAVA

and record equality compares the normalized component।


Assessment Part 5: Hash-Based Collections

Set<CourseCode> codes =
        new HashSet<>();

codes.add(
        new CourseCode(
                "java"
        )
);

codes.add(
        new CourseCode(
                "JAVA"
        )
);

Expected size:

1

because:

equals() → true
hashCode() → same

Assessment Part 6: Identify the Equality Bug

public final class EmailAddress {

    private final String value;

    @Override
    public boolean equals(
            Object other
    ) {
        if (
                !(other
                instanceof EmailAddress email)
        ) {
            return false;
        }

        return value.equals(
                email.value
        );
    }
}

What's missing?

Answer

hashCode()

If equality is value-based, matching hash code semantics are required for hash collections।


Assessment Part 7: Mutable Hash Key

Suppose:

Map<CourseCode, Course>

uses immutable CourseCode keys।

Good।

Now imagine CourseCode had:

setValue(...)

and its hash changed after insertion।

What could happen?

The map may no longer find the key correctly.

Therefore stable immutable keys are preferable।


Assessment Part 8: Clean Methods

Which is stronger?

Version A

public void process(
        Course course,
        boolean x
) {
}

Version B

public void publishCourse(
        CourseCode courseCode
) {
}

Answer

Version B communicates intent much more clearly।


Assessment Part 9: Boolean Blindness

Bad:

exportCourse(
        course,
        true
);

Better:

exportCourse(
        course,
        ExportMode.FULL
);

Why?

At the call site:

FULL

communicates meaning।

true does not।


Assessment Part 10: Guard Clauses

Refactor:

if (course != null) {
    if (
            course.getStatus()
            == CourseStatus.DRAFT
    ) {
        course.publish();
    }
}

A clearer version:

if (course == null) {
    throw new IllegalArgumentException(
            "Course is required."
    );
}

if (
        course.getStatus()
        != CourseStatus.DRAFT
) {
    throw new IllegalStateException(
            "Course must be draft."
    );
}

course.publish();

Though if publish() already validates state, the second check should not be duplicated outside the entity।

Even better:

if (course == null) {
    throw new IllegalArgumentException(
            "Course is required."
    );
}

course.publish();

Assessment Part 11: Move Behavior to the Owner

Given:

if (
        course.getStatus()
        == CourseStatus.DRAFT
        && !course.getLessons()
                .isEmpty()
) {
    course.setStatus(
            CourseStatus.PUBLISHED
    );
}

Better:

course.publish();

Why?

Course owns:

Status
Lessons
Publication invariant

Assessment Part 12: Wrong Responsibility

Should this be inside Course?

Files.writeString(
        path,
        content
);

Answer

Usually no।

That is persistence/infrastructure behavior।


Should this be inside Course?

public void publish()

Answer

Yes, if publication rules depend on course state and invariants।


Assessment Part 13: Hard-Coded Dependency

Bad:

public final class CourseService {

    private final CourseRepository repository =
            new FileCourseRepository(
                    Path.of(
                            "data"
                    )
            );
}

Better:

public final class CourseService {

    private final CourseRepository repository;

    public CourseService(
            CourseRepository repository
    ) {
        this.repository =
                repository;
    }
}

Now caller chooses the implementation।


Assessment Part 14: Does This Require Spring?

new CourseService(
        repository
);

No।

This is plain Java constructor injection।


Assessment Part 15: Choose the Interface

Which is a stronger interface?

Option A

public interface ApplicationServices {

    void saveCourse();

    void sendEmail();

    void chargeCard();

    void uploadFile();

    void enrollUser();
}

Option B

public interface CourseRepository {

    Course findByCode(
            CourseCode code
    );

    void save(
            Course course
    );
}

Answer

Option B।

It represents one cohesive capability।


Assessment Part 16: Composition vs Inheritance

Bad:

public class CourseService
        extends FileCourseRepository {
}

Why is this weak?

Because:

CourseService is not a FileCourseRepository.

It uses a repository।

Use composition:

public final class CourseService {

    private final CourseRepository repository;
}

Assessment Part 17: God Class

Consider:

public class PlatformManager {

    void createCourse() {
    }

    void publishCourse() {
    }

    void enrollUser() {
    }

    void processPayment() {
    }

    void sendEmail() {
    }

    void uploadVideo() {
    }

    void generateInvoice() {
    }
}

This is a likely god class।

Possible split:

CourseService
EnrollmentService
PaymentService
NotificationService
MediaStorage
InvoiceService

Only introduce those boundaries if the application actually has those responsibilities।


Assessment Part 18: Comments

Which is better?

// Check if course can publish
if (
        course.getStatus()
        == DRAFT
        && course.getLessons()
                .size()
        > 0
) {
}

or:

if (
        course.canPublish()
) {
}

The second is more expressive।

Comments should usually explain:

Why

not repeat obvious:

What

Assessment Part 19: toString()

A good toString() may help logging and debugging।

But should this appear?

password
accessToken
apiKey

No।

Sensitive values should not be exposed through toString()


Assessment Part 20: Records and Collections

Consider:

public record CoursePlan(
        List<Lesson> lessons
) {
}

Is this automatically deeply immutable?

Answer

No।

The list may be mutable।

The Lesson elements may also be mutable।

Better structural protection:

public CoursePlan {
    lessons =
            List.copyOf(
                    lessons
            );
}

But that still does not deep-copy mutable elements।


Code Smell Challenge

Identify the problems:

public class CourseManager {

    public static FileCourseRepository repo =
            new FileCourseRepository(
                    Path.of(
                            "data"
                    )
            );

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

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

        course.code =
                code;

        course.title =
                title;

        course.published =
                published;

        items.add(
                course
        );

        repo.save(
                course
        );
    }
}

Possible answers:

Global mutable dependency
Public mutable collection
Vague method name
Raw String domain identifier
Boolean state
Boolean parameter
Public mutable entity fields
Invalid object states possible
Hard-coded concrete repository
No constructor injection
Service and collection ownership mixed
No duplicate rule
No meaningful exceptions

Independent Practice Project

Build a small:

Enrollment Management Application

using the same principles।


Domain Requirements

An enrollment has:

EnrollmentId
CourseCode
LearnerId
EnrollmentStatus

Statuses:

ACTIVE
COMPLETED
CANCELLED

Rules:

New enrollment starts ACTIVE
ACTIVE enrollment may complete
ACTIVE enrollment may cancel
COMPLETED cannot cancel
CANCELLED cannot complete

Suggested Types

EnrollmentId
LearnerId
EnrollmentStatus
Enrollment
EnrollmentSummary
EnrollmentRepository
InMemoryEnrollmentRepository
EnrollmentService
EnrollmentNotFoundException

Challenge 1: Strong IDs

Create:

EnrollmentId

and:

LearnerId

as validated records।

For example:

value > 0

Challenge 2: Entity

Create:

Enrollment

as a normal class।

Methods:

complete()
cancel()

Do not expose:

setStatus(...)

publicly।


Challenge 3: Read Model

Create:

EnrollmentSummary

as a record।


Challenge 4: Repository

Create:

EnrollmentRepository

with:

Enrollment findById(
        EnrollmentId id
);

void save(
        Enrollment enrollment
);

List<Enrollment> findAll();

Challenge 5: In-Memory Implementation

Use:

Map<EnrollmentId, Enrollment>

Why is immutable EnrollmentId useful here?

Because its:

equals()
hashCode()

remain stable।


Challenge 6: Service

Create:

EnrollmentService

with:

completeEnrollment(
        EnrollmentId id
);

cancelEnrollment(
        EnrollmentId id
);

Service should:

Find entity
Call domain behavior
Save entity

It should not duplicate entity transition rules।


Challenge 7: Composition Root

In:

Main

create:

InMemoryEnrollmentRepository
EnrollmentService

and wire them manually।

No framework required।


Final Design Review Questions

Before considering the exercise complete, answer these questions.

1

Can callers create an invalid identifier?

2

Can callers directly change entity status?

3

Can callers mutate internal collections?

4

Are value objects immutable?

5

Are equals() and hashCode() stable?

6

Are records being used primarily for values/snapshots?

7

Are entities responsible for their own state transitions?

8

Does the service duplicate domain rules?

9

Does the service know persistence mechanics?

10

Are required dependencies visible in constructors?

11

Can the repository implementation be replaced?

12

Are package names meaningful?

13

Are class responsibilities cohesive?

14

Are method names intention-revealing?

15

Are there unnecessary abstractions?


Professional Java Design Assessment

For each item, score:

0 = Missing or seriously incorrect
1 = Partially correct
2 = Strong
AreaScore
Package organization/2
Strong domain types/2
Constructor validation/2
Immutability/2
Defensive copying/2
Stable equality/2
Correct hashCode()/2
Appropriate use of records/2
Encapsulation/2
Domain behavior placement/2
Clean method naming/2
Guard clauses / readable flow/2
Repository abstraction/2
Constructor injection/2
Composition over unnecessary inheritance/2
Meaningful exceptions/2
No global mutable dependencies/2
Cohesive classes/2
No unnecessary architecture/2
Overall readability/2

Maximum:

40

Score Interpretation

35–40
Strong professional Java foundation

29–34
Good foundation with minor design gaps

21–28
Understands the concepts but needs more refactoring practice

Below 21
Review Module 7 and rebuild the exercise

True or False

  1. Every domain type should be a record.
  2. Every class should have an interface.
  3. Immutable value objects are usually safe hash keys.
  4. final List<T> automatically makes the list immutable.
  5. Defensive copying can prevent mutation leaks.
  6. Records generate value-based equality.
  7. Mutable entity fields should automatically all participate in hashCode().
  8. Domain methods can protect invariants.
  9. Repository abstraction can hide persistence mechanics.
  10. Constructor injection requires Spring.
  11. Composition often works better than inheritance for collaborators.
  12. Global mutable dependencies make dependencies less explicit.
  13. Small focused methods are useful even if never reused.
  14. Boolean arguments can make call sites ambiguous.
  15. Clean architecture means creating the maximum possible abstractions.

Answers

1. False
2. False
3. True
4. False
5. True
6. True
7. False
8. True
9. True
10. False
11. True
12. True
13. True
14. True
15. False

Knowledge Check

Question 1

Why is CourseCode a strong record candidate?

Question 2

Why is Course better represented as a normal class?

Question 3

What does defensive copying protect against?

Question 4

Why should mutable internal collections not be returned directly?

Question 5

What equality rule must always hold with hashCode()?

Question 6

Why are immutable keys useful in HashMap?

Question 7

What does a record automatically generate?

Question 8

Why doesn't a record guarantee deep immutability?

Question 9

What is a god class?

Question 10

What does constructor injection make explicit?

Question 11

Why depend on CourseRepository instead of FileCourseRepository inside CourseService?

Question 12

What is composition?

Question 13

What is a composition root?

Question 14

What should usually own a rule based entirely on course state?

Question 15

What should drive abstraction decisions?


Knowledge Check Answers

Answer 1

It represents one validated immutable value and naturally benefits from generated value equality and hashing।

Answer 2

Course owns identity, mutable lifecycle, controlled behavior, and invariants।

Answer 3

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

Answer 4

Callers could bypass the object's validation and mutation rules।

Answer 5

If two objects are equal according to equals(), they must return the same hashCode()

Answer 6

Their equality and hash-related state does not change after insertion।

Answer 7

A canonical constructor, component accessors, equals(), hashCode(), and toString()

Answer 8

A record component can reference mutable objects such as List, arrays, or mutable classes।

Answer 9

A class that owns too many unrelated responsibilities and becomes excessively coupled।

Answer 10

Which collaborators are required for an object to perform its responsibility।

Answer 11

The service should depend on the persistence capability rather than a specific storage mechanism।

Answer 12

Building behavior by collaborating with contained/dependent objects rather than inheriting from them unnecessarily।

Answer 13

The place where concrete application dependencies are created and wired together।

Answer 14

Usually the Course entity itself because it owns the state and invariant involved।

Answer 15

Real domain meaning, observed complexity, useful boundaries, and maintainability needs—not fashion or rules applied mechanically।


Module Completion Checklist

Before completing Module 7, verify that you can:

  • Organize Java code into meaningful packages
  • Explain package-private visibility
  • Avoid making every type public
  • Distinguish mutable and immutable objects
  • Explain what final does and does not guarantee
  • Defensively copy mutable constructor inputs
  • Return safe collection snapshots
  • Explain shallow vs deep immutability
  • Implement stable logical equality
  • Explain the equals() contract
  • Explain the hashCode() contract
  • Use immutable values safely as Map keys
  • Choose between a record and normal class
  • Validate and normalize record components
  • Write intention-revealing method names
  • Use guard clauses
  • Recognize boolean blindness
  • Identify god classes
  • Move behavior to the object that owns the relevant state
  • Separate infrastructure from domain behavior
  • Use constructor injection
  • Explain dependency injection without frameworks
  • Use interfaces at meaningful boundaries
  • Prefer composition over unnecessary inheritance
  • Recognize hidden global dependencies
  • Assemble a small object graph in Main
  • Refactor messy procedural Java into cohesive types
  • Avoid unnecessary abstraction

Module Summary

এই module-এ আমরা Java syntax-এর বাইরে professional code design-এর foundation তৈরি করেছি।

আমরা শিখেছি:

  • Packages code organization এবং visibility boundaries তৈরি করে
  • Feature-oriented packages related code কাছাকাছি রাখতে পারে
  • Package-private access unnecessary public API reduce করে
  • Immutability unexpected state changes reduce করে
  • final references referenced objectকে immutable করে না
  • Defensive copying ownership boundaries protect করে
  • List.copyOf() immutable snapshots তৈরি করতে useful
  • Mutable nested objects shallow immutability-এর limitation তৈরি করে
  • equals() logical equality define করে
  • hashCode() hash-based collections-এর জন্য essential
  • Equal objects must have equal hash codes
  • Mutable equality state HashMap এবং HashSet-এ dangerous
  • Records concise immutable data-oriented types provide করে
  • Records value objects এবং snapshots-এর strong candidates
  • Records every domain entity-এর replacement নয়
  • Clean methods clear intentions communicate করে
  • Guard clauses nesting reduce করে
  • Boolean parameters call-site meaning hide করতে পারে
  • Domain behavior should stay close to the state it protects
  • God classes reduce cohesion
  • Infrastructure should not leak into domain objects
  • Constructor injection dependencies explicit করে
  • Dependency injection does not require a framework
  • Interfaces are useful at real boundaries, not everywhere
  • Composition models collaboration better than fake inheritance
  • Global mutable dependencies create hidden coupling
  • Main can act as a composition root
  • Refactoring should improve boundaries while preserving behavior
  • More classes, interfaces, or layers do not automatically mean better software

The central principle of Module 7 is:

Make important concepts explicit,
keep responsibilities cohesive,
protect object state,
and make dependencies visible.

Module Complete

At this point, a student should no longer think only in terms of:

How do I make this Java code compile?

The better question becomes:

Where should this responsibility live?
Which state should this object protect?
What should be immutable?
What should callers be allowed to change?
Which dependencies should be explicit?
How can another developer understand this code quickly?

That shift is one of the most important transitions from learning Java syntax to writing maintainable Java software.


Next Module

পরবর্তী module হবে:

Final Project

এই project-এ আমরা পুরো course-এর concepts combine করব:

  • Java fundamentals
  • Classes and objects
  • Encapsulation
  • Inheritance and composition
  • Interfaces
  • Generics
  • Collections
  • Enums
  • Exceptions
  • File I/O
  • Repository abstraction
  • Value objects
  • Records
  • Immutability
  • Equality
  • Clean methods
  • Dependency design

Student একটি complete console-based Java application design এবং implement করবে from scratch.